Просмотр исходного кода

✨ feat(financial-dashboard): 实现财务数据可视化大屏基础功能

- 创建完整的财务数据可视化大屏组件结构
- 实现四个数据模块:资产总额与资产净额、利润总额与净利润、收入、资产负债率
- 集成真实API调用,使用React Query获取财务数据
- 实现动态BarElement组件,支持高度和颜色根据数据动态变化
- 严格按照Figma设计实现组件样式和布局
- 添加图片资源规范,统一管理静态图片
- 配置路由集成到主应用

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 2 месяцев назад
Родитель
Сommit
48b4a2ee5f
33 измененных файлов с 1244 добавлено и 1 удалено
  1. 19 0
      docs/stories/006.002.实现财务数据可视化大屏页面.md
  2. 32 0
      public/financial-dashboard/base-frame.png
  3. 20 0
      public/financial-dashboard/ellipse4119.png
  4. 16 0
      public/financial-dashboard/group1321314607.png
  5. 15 0
      public/financial-dashboard/header-left.png
  6. 15 0
      public/financial-dashboard/header-right.png
  7. 16 0
      public/financial-dashboard/icon-path.png
  8. 9 0
      public/financial-dashboard/rectangle3709.png
  9. 9 0
      public/financial-dashboard/vector.png
  10. 9 0
      public/financial-dashboard/vector1.png
  11. 9 0
      public/financial-dashboard/vector2.png
  12. 147 0
      src/client/home/pages/FinancialDashboard/FinancialDashboard.tsx
  13. 20 0
      src/client/home/pages/FinancialDashboard/assets/ellipse4119.png
  14. 98 0
      src/client/home/pages/FinancialDashboard/components/AssetMetrics.tsx
  15. 16 0
      src/client/home/pages/FinancialDashboard/components/BackgroundOverlay.tsx
  16. 68 0
      src/client/home/pages/FinancialDashboard/components/BarElement.tsx
  17. 33 0
      src/client/home/pages/FinancialDashboard/components/BaseContainer.tsx
  18. 37 0
      src/client/home/pages/FinancialDashboard/components/DataLabel.tsx
  19. 95 0
      src/client/home/pages/FinancialDashboard/components/DebtRatioMetrics.tsx
  20. 24 0
      src/client/home/pages/FinancialDashboard/components/GridBackground.tsx
  21. 32 0
      src/client/home/pages/FinancialDashboard/components/Icon.tsx
  22. 95 0
      src/client/home/pages/FinancialDashboard/components/IncomeMetrics.tsx
  23. 18 0
      src/client/home/pages/FinancialDashboard/components/ModalCloseButton.tsx
  24. 69 0
      src/client/home/pages/FinancialDashboard/components/ModalContent.tsx
  25. 57 0
      src/client/home/pages/FinancialDashboard/components/ModalDataRow.tsx
  26. 15 0
      src/client/home/pages/FinancialDashboard/components/ModalDialog.tsx
  27. 16 0
      src/client/home/pages/FinancialDashboard/components/ModalHeader.tsx
  28. 16 0
      src/client/home/pages/FinancialDashboard/components/ModalOverlay.tsx
  29. 98 0
      src/client/home/pages/FinancialDashboard/components/ProfitMetrics.tsx
  30. 39 0
      src/client/home/pages/FinancialDashboard/components/ReportHeader.tsx
  31. 29 0
      src/client/home/pages/FinancialDashboard/components/TimeIcon.tsx
  32. 52 0
      src/client/home/pages/FinancialDashboard/components/VariationModal.tsx
  33. 1 1
      src/client/home/routes.tsx

+ 19 - 0
docs/stories/006.002.实现财务数据可视化大屏页面.md

@@ -214,6 +214,25 @@ Draft
 - 将所有Tailwind类转换为目标样式系统,同时保持精确的视觉设计
 - 遵循项目的现有模式和约定
 
+### 图片资源规范
+**图片下载和引用规范**:
+- **图片位置**: 所有静态图片统一放在 `public/financial-dashboard/` 目录下
+- **下载方法**: 使用 `wget` 直接从Figma提供的图片URL下载
+- **引用方式**: 使用绝对路径 `/financial-dashboard/{图片文件名}` 引用
+- **命名规范**: 保持Figma文档中的原始图片变量名作为文件名
+
+**图片下载示例**:
+```bash
+# 在 public/financial-dashboard/ 目录下执行
+wget -O ellipse4119.png "https://www.figma.com/api/mcp/asset/fcf74522-1000-42fd-8999-d2f9337c522e"
+wget -O icon-path.png "https://www.figma.com/api/mcp/asset/6e33453e-bfea-4c73-abb8-93813828acd2"
+```
+
+**组件引用示例**:
+```jsx
+<img alt="" className="block max-w-none size-full" src="/financial-dashboard/ellipse4119.png" />
+```
+
 ### 编码标准 [Source: architecture/coding-standards.md#关键集成规则]
 - **现有API兼容性**: 确保组件不破坏现有API契约
 - **错误处理**: 实现适当的错误边界和加载状态

+ 32 - 0
public/financial-dashboard/base-frame.png

@@ -0,0 +1,32 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 930 480" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#229;&#186;&#149;&#233;&#131;&#168;">
+<g id="Rectangle 34627557">
+<path d="M0 0H26.4715L31.9656 4.39359H100.517L105.886 0H930V461.327L913.018 480H904.028L899.533 475.606H840.596L836.101 480H0V0Z" fill="url(#paint0_linear_0_2609)" fill-opacity="0.3"/>
+<path d="M0 0H26.4715L31.9656 4.39359H100.517L105.886 0H930V461.327L913.018 480H904.028L899.533 475.606H840.596L836.101 480H0V0Z" fill="var(--fill-1, black)" fill-opacity="0.4"/>
+<path d="M26.2959 0.5L31.6533 4.78418L31.79 4.89355H100.695L100.834 4.78027L106.064 0.5H929.5V461.134L912.798 479.5H904.231L899.882 475.249L899.736 475.106H840.393L840.247 475.249L835.896 479.5H0.5V0.5H26.2959Z" stroke="url(#paint1_linear_0_2609)" stroke-opacity="0.6"/>
+</g>
+<path id="Rectangle 34627593" d="M916.015 478.904H930V463.562L923.008 471.233L916.015 478.904Z" fill="var(--fill-0, #A2BEFF)"/>
+<path id="Rectangle 34627511" d="M839.098 480L841.096 477.808H899.033L901.531 480H839.098Z" fill="var(--fill-0, #A2BEFF)"/>
+<path id="Rectangle 34627924" d="M1.99785 478.904L1.99785 443.836H0L0 478.904H1.99785Z" fill="var(--fill-0, #A2BEFF)" fill-opacity="0.6"/>
+<path id="Rectangle 34627514" d="M821.117 477.808H8.99027V480H821.117L821.117 477.808Z" fill="var(--fill-0, #A2BEFF)" fill-opacity="0.5"/>
+<path id="Rectangle 34627513" d="M31.8504 2.1918L28.9689 1.90735e-05L103.888 1.90735e-05L101.007 2.1918L31.8504 2.1918Z" fill="url(#paint2_linear_0_2609)" fill-opacity="0.6"/>
+<path id="Union" d="M930 0V9.86301H928.002V2.19178H921.01V0H930Z" fill="var(--fill-0, #A2BEFF)"/>
+<path id="Union_2" d="M0 480V470.137H1.99805V477.808H8.99023V480H0Z" fill="var(--fill-0, #A2BEFF)"/>
+<path id="Union_3" d="M0 0V9.86301H1.99805V2.19178H8.99023V0H0Z" fill="var(--fill-0, #A2BEFF)"/>
+</g>
+<defs>
+<linearGradient id="paint0_linear_0_2609" x1="270.376" y1="16.476" x2="270.376" y2="613.232" gradientUnits="userSpaceOnUse">
+<stop stop-color="#A2BEFF" stop-opacity="0"/>
+<stop offset="1" stop-color="#A2BEFF"/>
+<stop offset="1" stop-color="#A2BEFF" stop-opacity="0.492756"/>
+</linearGradient>
+<linearGradient id="paint1_linear_0_2609" x1="266.555" y1="173.473" x2="266.555" y2="239.768" gradientUnits="userSpaceOnUse">
+<stop stop-color="#A2BEFF" stop-opacity="0.3"/>
+<stop offset="1" stop-color="#A2BEFF" stop-opacity="0.6"/>
+</linearGradient>
+<linearGradient id="paint2_linear_0_2609" x1="103.888" y1="1.09591" x2="28.9689" y2="1.09591" gradientUnits="userSpaceOnUse">
+<stop stop-color="#A2BEFF"/>
+<stop offset="1" stop-color="#A2BEFF"/>
+</linearGradient>
+</defs>
+</svg>

+ 20 - 0
public/financial-dashboard/ellipse4119.png

@@ -0,0 +1,20 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 82 82" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Ellipse 4119">
+<g filter="url(#filter0_i_0_2620)">
+<circle cx="41" cy="41" r="40" fill="var(--fill-0, #C9DDFF)" fill-opacity="0.09"/>
+</g>
+<circle cx="41" cy="41" r="40.5" stroke="var(--stroke-0, #C9DDFF)"/>
+</g>
+<defs>
+<filter id="filter0_i_0_2620" x="0" y="0" width="82" height="82" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset/>
+<feGaussianBlur stdDeviation="6.5"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.788235 0 0 0 0 0.866667 0 0 0 0 1 0 0 0 0.5 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_2620"/>
+</filter>
+</defs>
+</svg>

+ 16 - 0
public/financial-dashboard/group1321314607.png

@@ -0,0 +1,16 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 30 6" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Group 1321314607">
+<path id="Vector" d="M15 0L0 2.99636L15 6L30 2.99636L15 0Z" fill="url(#paint0_linear_0_2606)"/>
+<path id="Vector_2" d="M0 3C2.56113 3.36748 5.08496 3.78174 7.60879 4.20267C8.87692 4.40312 10.1202 4.63697 11.3759 4.85746L15.143 5.52561H14.857L18.6241 4.85746C19.8798 4.63697 21.1231 4.40312 22.3912 4.20267C24.915 3.78174 27.4389 3.36748 30 3C27.5632 3.54788 25.0891 4.049 22.615 4.54343C21.3842 4.79733 20.1285 5.0245 18.8852 5.25835L15.143 5.96659L14.9938 6L14.857 5.97327L11.1148 5.26503C9.87153 5.0245 8.61583 4.80401 7.385 4.54343C4.9109 4.049 2.4368 3.54788 0 3Z" fill="url(#paint1_linear_0_2606)"/>
+</g>
+<defs>
+<linearGradient id="paint0_linear_0_2606" x1="15.0024" y1="5.9965" x2="15.0024" y2="0" gradientUnits="userSpaceOnUse">
+<stop stop-color="#81E4FF"/>
+<stop offset="1" stop-color="#0072DD"/>
+</linearGradient>
+<linearGradient id="paint1_linear_0_2606" x1="14.9961" y1="5.99719" x2="14.9961" y2="2.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="white" stop-opacity="0.3"/>
+</linearGradient>
+</defs>
+</svg>

+ 15 - 0
public/financial-dashboard/header-left.png

@@ -0,0 +1,15 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 136 9" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#229;&#183;&#166;&#228;&#190;&#167;">
+<path id="&#231;&#159;&#169;&#229;&#189;&#162; 1223 &#230;&#139;&#183;&#232;&#180;&#157; 26" d="M9.00058 9L1.00058 0H0.000579834L8.00058 9H9.00058ZM14.0006 9L6.00058 0H5.00058L13.0006 9H14.0006ZM19.0006 9L11.0006 0H10.0006L18.0006 9H19.0006ZM24.0006 9L16.0006 0H15.0006L23.0006 9H24.0006ZM29.0006 9L21.0006 0H20.0006L28.0006 9H29.0006ZM34.0006 9L26.0006 0H25.0006L33.0006 9H34.0006ZM39.0006 9L31.0006 0H30.0006L38.0006 9H39.0006ZM44.0006 9L36.0006 0H35.0006L43.0006 9H44.0006ZM49.0006 9L41.0006 0H40.0006L48.0006 9H49.0006ZM54.0006 9L46.0006 0H45.0006L53.0006 9H54.0006ZM59.0006 9L51.0006 0H50.0006L58.0006 9H59.0006ZM64.0006 9L56.0006 0H55.0006L63.0006 9H64.0006ZM69.0006 9L61.0006 0H60.0006L68.0006 9H69.0006ZM74.0006 9L66.0006 0H65.0006L73.0006 9H74.0006ZM79.0006 9L71.0006 0H70.0006L78.0006 9H79.0006Z" fill="var(--fill-0, #A2BEFF)" fill-opacity="0.6"/>
+<g id="Union">
+<path d="M133 9H136L131 3H128L133 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M126 9H129L124 3H121L126 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M119 9H122L117 3H114L119 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M112 9H115L110 3H107L112 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M105 9H108L103 3H100L105 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M98 9H101L96 3H93L98 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M91 9H94L89 3H86L91 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M84 9H87L82 3H79L84 9Z" fill="var(--fill-0, #A2BEFF)"/>
+</g>
+</g>
+</svg>

+ 15 - 0
public/financial-dashboard/header-right.png

@@ -0,0 +1,15 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 136 9" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#229;&#143;&#179;&#228;&#190;&#167;">
+<path id="&#231;&#159;&#169;&#229;&#189;&#162; 1223 &#230;&#139;&#183;&#232;&#180;&#157; 26" d="M9.00058 9L1.00058 0H0.000579834L8.00058 9H9.00058ZM14.0006 9L6.00058 0H5.00058L13.0006 9H14.0006ZM19.0006 9L11.0006 0H10.0006L18.0006 9H19.0006ZM24.0006 9L16.0006 0H15.0006L23.0006 9H24.0006ZM29.0006 9L21.0006 0H20.0006L28.0006 9H29.0006ZM34.0006 9L26.0006 0H25.0006L33.0006 9H34.0006ZM39.0006 9L31.0006 0H30.0006L38.0006 9H39.0006ZM44.0006 9L36.0006 0H35.0006L43.0006 9H44.0006ZM49.0006 9L41.0006 0H40.0006L48.0006 9H49.0006ZM54.0006 9L46.0006 0H45.0006L53.0006 9H54.0006ZM59.0006 9L51.0006 0H50.0006L58.0006 9H59.0006ZM64.0006 9L56.0006 0H55.0006L63.0006 9H64.0006ZM69.0006 9L61.0006 0H60.0006L68.0006 9H69.0006ZM74.0006 9L66.0006 0H65.0006L73.0006 9H74.0006ZM79.0006 9L71.0006 0H70.0006L78.0006 9H79.0006Z" fill="var(--fill-0, #A2BEFF)" fill-opacity="0.6"/>
+<g id="Union">
+<path d="M133 9H136L131 3H128L133 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M126 9H129L124 3H121L126 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M119 9H122L117 3H114L119 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M112 9H115L110 3H107L112 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M105 9H108L103 3H100L105 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M98 9H101L96 3H93L98 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M91 9H94L89 3H86L91 9Z" fill="var(--fill-0, #A2BEFF)"/>
+<path d="M84 9H87L82 3H79L84 9Z" fill="var(--fill-0, #A2BEFF)"/>
+</g>
+</g>
+</svg>

+ 16 - 0
public/financial-dashboard/icon-path.png

@@ -0,0 +1,16 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 50 36" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#232;&#183;&#175;&#229;&#190;&#132;" filter="url(#filter0_d_0_2628)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.0007 11.8078C8.9902 11.5098 9.09828 11.2198 9.30113 11.0018L10.8299 9.35684C11.0329 9.1389 11.3139 9.01076 11.6112 9.00065C11.9084 8.99053 12.1975 9.09927 12.4147 9.30291L24.8198 20.8983L37.2271 9.30291C37.6792 8.88032 38.3872 8.90446 38.8097 9.35684L40.3384 11.0018C40.5416 11.2196 40.6501 11.5093 40.64 11.8073C40.6299 12.1053 40.5021 12.3871 40.2847 12.5905L26.4024 25.568L25.6962 26.3275C25.4703 26.5705 25.1485 26.7005 24.8176 26.6826C24.4882 26.6992 24.1683 26.5693 23.9433 26.3275L23.2372 25.5635L9.35716 12.5905C9.13944 12.3874 9.0112 12.1058 9.0007 11.8078Z" fill="var(--fill-0, #C9DDFF)"/>
+</g>
+<defs>
+<filter id="filter0_d_0_2628" x="-1.26274e-10" y="0" width="49.6406" height="35.6842" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset/>
+<feGaussianBlur stdDeviation="4.5"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.788235 0 0 0 0 0.866667 0 0 0 0 1 0 0 0 0.501961 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_2628"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_2628" result="shape"/>
+</filter>
+</defs>
+</svg>

+ 9 - 0
public/financial-dashboard/rectangle3709.png

@@ -0,0 +1,9 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 1 5" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path id="Rectangle 3709" d="M0 0H1V5H0V0Z" fill="url(#paint0_linear_0_2585)"/>
+<defs>
+<linearGradient id="paint0_linear_0_2585" x1="1" y1="0.384615" x2="1" y2="5.38462" gradientUnits="userSpaceOnUse">
+<stop stop-color="#1F3453"/>
+<stop offset="1" stop-color="#40558D" stop-opacity="0"/>
+</linearGradient>
+</defs>
+</svg>

+ 9 - 0
public/financial-dashboard/vector.png

@@ -0,0 +1,9 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15 115" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path id="Vector" d="M14.5 115H0L0 0H14.5V115Z" fill="url(#paint0_linear_0_2546)"/>
+<defs>
+<linearGradient id="paint0_linear_0_2546" x1="7.24674" y1="114.41" x2="7.24674" y2="0.00305498" gradientUnits="userSpaceOnUse">
+<stop stop-color="#0058FF" stop-opacity="0.2"/>
+<stop offset="1" stop-color="#0093DD"/>
+</linearGradient>
+</defs>
+</svg>

+ 9 - 0
public/financial-dashboard/vector1.png

@@ -0,0 +1,9 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15 115" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path id="Vector" d="M14.5 115H0L0 0H14.5V115Z" fill="url(#paint0_linear_0_2547)"/>
+<defs>
+<linearGradient id="paint0_linear_0_2547" x1="7.24673" y1="116.439" x2="7.24673" y2="-0.979782" gradientUnits="userSpaceOnUse">
+<stop offset="0.00462963" stop-color="#0138DE" stop-opacity="0.2"/>
+<stop offset="1" stop-color="#006BBC"/>
+</linearGradient>
+</defs>
+</svg>

+ 9 - 0
public/financial-dashboard/vector2.png

@@ -0,0 +1,9 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 30 5" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path id="Vector" d="M0 2.3309L15 4.33332L30 2.33089L15 0L0 2.3309Z" fill="url(#paint0_linear_0_2591)" fill-opacity="0.6"/>
+<defs>
+<linearGradient id="paint0_linear_0_2591" x1="15" y1="0" x2="15" y2="4.33333" gradientUnits="userSpaceOnUse">
+<stop stop-color="#0A316C"/>
+<stop offset="1" stop-color="#1B659B"/>
+</linearGradient>
+</defs>
+</svg>

+ 147 - 0
src/client/home/pages/FinancialDashboard/FinancialDashboard.tsx

@@ -0,0 +1,147 @@
+import React, { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { dashClient } from '@/client/api';
+
+// 导入基础组件
+import Icon from './components/Icon';
+import ReportHeader from './components/ReportHeader';
+import BaseContainer from './components/BaseContainer';
+import GridBackground from './components/GridBackground';
+import BackgroundOverlay from './components/BackgroundOverlay';
+
+// 导入数据模块组件
+import AssetMetrics from './components/AssetMetrics';
+import ProfitMetrics from './components/ProfitMetrics';
+import IncomeMetrics from './components/IncomeMetrics';
+import DebtRatioMetrics from './components/DebtRatioMetrics';
+
+// 导入弹窗组件
+import VariationModal from './components/VariationModal';
+
+// API 响应类型定义
+interface FinancialDataItem {
+  id: number;
+  year: number;
+  assetTotal?: number; // 资产总额(单位:元)
+  assetNet?: number;   // 资产净额(单位:元)
+  profitTotal?: number; // 利润总额(单位:元)
+  profitNet?: number;   // 净利润(单位:元)
+  income?: number;      // 收入(单位:元)
+  assetLiabilityRatio?: number; // 资产负债率(单位:%)
+  dataDeadline: string;
+  createTime: string;
+  updateTime: string;
+}
+
+interface FinancialOutlookData {
+  code: 200;
+  msg: '查询成功';
+  rows: Array<{
+    assetTotalNet: FinancialDataItem[];
+    profitTotalNet: FinancialDataItem[];
+    incomeStatement: FinancialDataItem[];
+    assetLiabilityRatio: FinancialDataItem[];
+  }>;
+}
+
+const FinancialDashboard: React.FC = () => {
+  const [isModalOpen, setIsModalOpen] = useState(false);
+
+  // 使用真实API调用获取财务数据
+  const { data: financialData, isLoading, error } = useQuery({
+    queryKey: ['financial-outlook'],
+    queryFn: async () => {
+      const response = await dashClient.outlook.$get();
+      if (!response.ok) {
+        throw new Error('Failed to fetch financial data');
+      }
+      return response.json() as Promise<FinancialOutlookData>;
+    },
+  });
+
+  const handleOpenModal = () => {
+    setIsModalOpen(true);
+  };
+
+  const handleCloseModal = () => {
+    setIsModalOpen(false);
+  };
+
+  if (isLoading) {
+    return (
+      <div className="w-screen h-screen bg-gray-900 flex items-center justify-center">
+        <div className="text-white text-xl">加载中...</div>
+      </div>
+    );
+  }
+
+  if (error) {
+    return (
+      <div className="w-screen h-screen bg-gray-900 flex items-center justify-center">
+        <div className="text-white text-xl">数据加载失败</div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="relative w-[1920px] h-[1080px] overflow-hidden bg-gray-900">
+      {/* 背景层 */}
+      <BackgroundOverlay />
+      <GridBackground />
+
+      {/* 主内容区域 */}
+      <div className="relative z-10 w-full h-full p-8">
+        {/* 报表头部 */}
+        <ReportHeader title="财务数据可视化大屏" />
+
+        {/* 数据模块网格布局 */}
+        <div className="grid grid-cols-2 grid-rows-2 gap-6 mt-8 h-[calc(100%-120px)]">
+          {/* 左上:资产负债率模块 */}
+          <div className="relative">
+            <BaseContainer>
+              <AssetMetrics data={financialData?.rows[0]?.assetTotalNet} />
+            </BaseContainer>
+          </div>
+
+          {/* 右上:收入模块 */}
+          <div className="relative">
+            <BaseContainer>
+              <IncomeMetrics data={financialData?.rows[0]?.incomeStatement} />
+            </BaseContainer>
+          </div>
+
+          {/* 左下:利润总额与净利润模块 */}
+          <div className="relative">
+            <BaseContainer>
+              <ProfitMetrics data={financialData?.rows[0]?.profitTotalNet} />
+            </BaseContainer>
+          </div>
+
+          {/* 右下:资产负债率(百分比)模块 */}
+          <div className="relative">
+            <BaseContainer>
+              <DebtRatioMetrics data={financialData?.rows[0]?.assetLiabilityRatio} />
+            </BaseContainer>
+          </div>
+        </div>
+
+        {/* 浮动按钮 */}
+        <button
+          onClick={handleOpenModal}
+          className="absolute bottom-8 right-8 w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center shadow-lg hover:bg-blue-700 transition-colors z-20"
+        >
+          <Icon />
+        </button>
+      </div>
+
+      {/* 变动幅度弹窗 */}
+      <VariationModal
+        isOpen={isModalOpen}
+        onClose={handleCloseModal}
+        data={financialData?.rows[0]}
+      />
+    </div>
+  );
+};
+
+export default FinancialDashboard;

+ 20 - 0
src/client/home/pages/FinancialDashboard/assets/ellipse4119.png

@@ -0,0 +1,20 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 82 82" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Ellipse 4119">
+<g filter="url(#filter0_i_0_2620)">
+<circle cx="41" cy="41" r="40" fill="var(--fill-0, #C9DDFF)" fill-opacity="0.09"/>
+</g>
+<circle cx="41" cy="41" r="40.5" stroke="var(--stroke-0, #C9DDFF)"/>
+</g>
+<defs>
+<filter id="filter0_i_0_2620" x="0" y="0" width="82" height="82" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset/>
+<feGaussianBlur stdDeviation="6.5"/>
+<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.788235 0 0 0 0 0.866667 0 0 0 0 1 0 0 0 0.5 0"/>
+<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_2620"/>
+</filter>
+</defs>
+</svg>

+ 98 - 0
src/client/home/pages/FinancialDashboard/components/AssetMetrics.tsx

@@ -0,0 +1,98 @@
+import React from 'react';
+import BarElement from './BarElement';
+import DataLabel from './DataLabel';
+
+interface FinancialDataItem {
+  id: number;
+  year: number;
+  assetTotal?: number; // 资产总额(单位:元)
+  assetNet?: number;   // 资产净额(单位:元)
+  dataDeadline: string;
+  createTime: string;
+  updateTime: string;
+}
+
+interface AssetMetricsProps {
+  data?: FinancialDataItem[];
+}
+
+const AssetMetrics: React.FC<AssetMetricsProps> = ({ data }) => {
+  // 从数组数据中提取2023和2024年的数据
+  const data2023 = data?.find(item => item.year === 2023);
+  const data2024 = data?.find(item => item.year === 2024);
+
+  // 计算最大值用于图表比例(暂时不使用,保留用于未来动态高度)
+  const _maxValue = data ? Math.max(
+    data2023?.assetTotal || 0,
+    data2023?.assetNet || 0,
+    data2024?.assetTotal || 0,
+    data2024?.assetNet || 0
+  ) : 0;
+
+  return (
+    <div className="h-full flex flex-col">
+      {/* 模块标题 */}
+      <div className="text-center mb-4">
+        <h3 className="text-xl font-bold text-white">资产负债率</h3>
+        <p className="text-sm text-white/70">资产总额与资产净额</p>
+      </div>
+
+      {/* 数据图表区域 */}
+      <div className="flex-1 flex items-end justify-center space-x-8">
+        {data ? (
+          <>
+            {/* 2023年数据 */}
+            <div className="flex flex-col items-center">
+              <BarElement
+                value={data2023?.assetTotal || 0}
+                maxValue={_maxValue}
+                type="asset-total"
+                label="2023"
+              />
+              <DataLabel
+                value={data2023?.assetTotal || 0}
+                label="资产总额"
+                unit="元"
+                className="mt-4"
+              />
+            </div>
+
+            {/* 2024年数据 */}
+            <div className="flex flex-col items-center">
+              <BarElement
+                value={data2024?.assetNet || 0}
+                maxValue={_maxValue}
+                type="asset-net"
+                label="2024"
+              />
+              <DataLabel
+                value={data2024?.assetNet || 0}
+                label="资产净额"
+                unit="元"
+                className="mt-4"
+              />
+            </div>
+          </>
+        ) : (
+          <div className="text-white/50 text-center">
+            数据加载中...
+          </div>
+        )}
+      </div>
+
+      {/* 图例说明 */}
+      <div className="flex justify-center space-x-6 mt-4 text-xs text-white/60">
+        <div className="flex items-center">
+          <div className="w-3 h-3 bg-blue-500 rounded mr-1" />
+          <span>资产总额</span>
+        </div>
+        <div className="flex items-center">
+          <div className="w-3 h-3 bg-yellow-500 rounded mr-1" />
+          <span>资产净额</span>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default AssetMetrics;

+ 16 - 0
src/client/home/pages/FinancialDashboard/components/BackgroundOverlay.tsx

@@ -0,0 +1,16 @@
+import React from 'react';
+
+const BackgroundOverlay: React.FC = () => {
+  return (
+    <div className="absolute inset-0">
+      {/* 背景图片遮罩 */}
+      <div className="absolute inset-0 bg-gradient-to-br from-blue-900/30 via-purple-900/20 to-indigo-900/30" />
+
+      {/* 装饰性光效 */}
+      <div className="absolute top-1/4 left-1/4 w-64 h-64 bg-blue-500/10 rounded-full blur-3xl" />
+      <div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl" />
+    </div>
+  );
+};
+
+export default BackgroundOverlay;

+ 68 - 0
src/client/home/pages/FinancialDashboard/components/BarElement.tsx

@@ -0,0 +1,68 @@
+import React from 'react';
+
+interface BarElementProps {
+  className?: string;
+  value: number;
+  maxValue: number;
+  type: 'asset-total' | 'asset-net' | 'profit-total' | 'profit-net' | 'income' | 'debt-ratio';
+  label: string;
+}
+
+const BarElement: React.FC<BarElementProps> = ({
+  className,
+  value,
+  maxValue,
+  type,
+  label
+}) => {
+  // 计算柱形高度比例 (0-100%)
+  const heightPercentage = maxValue > 0 ? (value / maxValue) * 100 : 0;
+
+  // 根据类型设置颜色
+  const getBarColor = () => {
+    switch (type) {
+      case 'asset-total':
+        return 'from-[#c2e6ff] to-[rgba(0,68,170,0.3)]';
+      case 'asset-net':
+        return 'from-[#ffd700] to-[rgba(255,215,0,0.3)]';
+      case 'profit-total':
+        return 'from-[#ff6b6b] to-[rgba(255,107,107,0.3)]';
+      case 'profit-net':
+        return 'from-[#4ecdc4] to-[rgba(78,205,196,0.3)]';
+      case 'income':
+        return 'from-[#45b7d1] to-[rgba(69,183,209,0.3)]';
+      case 'debt-ratio':
+        return 'from-[#96ceb4] to-[rgba(150,206,180,0.3)]';
+      default:
+        return 'from-[#c2e6ff] to-[rgba(0,68,170,0.3)]';
+    }
+  };
+
+  return (
+    <div className={className}>
+      {/* 柱形图主体 */}
+      <div className="flex flex-col items-center h-48">
+        {/* 柱形图 */}
+        <div
+          className={`w-12 bg-gradient-to-b ${getBarColor()} rounded-t-lg transition-all duration-500 ease-out`}
+          style={{ height: `${Math.max(heightPercentage, 10)}%` }}
+        />
+
+        {/* 标签 */}
+        <div className="mt-2 text-white text-sm font-medium">
+          {label}
+        </div>
+
+        {/* 数值显示 */}
+        <div className="mt-1 text-white/70 text-xs">
+          {type === 'debt-ratio' ? `${value.toFixed(1)}%` :
+           value >= 100000000 ? `${(value / 100000000).toFixed(1)}亿` :
+           value >= 10000 ? `${(value / 10000).toFixed(1)}万` :
+           value.toLocaleString()}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default BarElement;

+ 33 - 0
src/client/home/pages/FinancialDashboard/components/BaseContainer.tsx

@@ -0,0 +1,33 @@
+import React from 'react';
+
+interface BaseContainerProps {
+  children: React.ReactNode;
+  className?: string;
+}
+
+const BaseContainer: React.FC<BaseContainerProps> = ({
+  children,
+  className = ''
+}) => {
+  return (
+    <div className={`relative w-full h-full ${className}`}>
+      {/* 底框 */}
+      <div className="absolute inset-0" data-name="底框" data-node-id="1705:119101">
+        <img alt="" className="block max-w-none size-full" src="/financial-dashboard/base-frame.png" />
+      </div>
+
+      {/* 头部 */}
+      <div className="absolute top-0 left-0 right-0 h-8" data-name="头部" data-node-id="1705:119126">
+        {/* 头部装饰,暂时用占位符 */}
+        <div className="bg-gradient-to-r from-blue-500/80 to-purple-500/80 rounded-t-lg h-full" />
+      </div>
+
+      {/* 内容区域 */}
+      <div className="relative z-10 p-6 h-full pt-10">
+        {children}
+      </div>
+    </div>
+  );
+};
+
+export default BaseContainer;

+ 37 - 0
src/client/home/pages/FinancialDashboard/components/DataLabel.tsx

@@ -0,0 +1,37 @@
+import React from 'react';
+
+interface DataLabelProps {
+  value: number | string;
+  label?: string;
+  unit?: string;
+  className?: string;
+}
+
+const DataLabel: React.FC<DataLabelProps> = ({
+  value,
+  label,
+  unit,
+  className = ''
+}) => {
+  return (
+    <div className={`flex flex-col items-center ${className}`}>
+      {/* 装饰元素 */}
+      <div className="w-2 h-2 bg-blue-400 rounded-full mb-1" />
+
+      {/* 数值显示 */}
+      <div className="text-lg font-bold text-white drop-shadow-sm">
+        {typeof value === 'number' ? value.toLocaleString() : value}
+        {unit && <span className="text-sm ml-1">{unit}</span>}
+      </div>
+
+      {/* 标签 */}
+      {label && (
+        <div className="text-xs text-white/70 mt-1">
+          {label}
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default DataLabel;

+ 95 - 0
src/client/home/pages/FinancialDashboard/components/DebtRatioMetrics.tsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import BarElement from './BarElement';
+import DataLabel from './DataLabel';
+
+interface FinancialDataItem {
+  id: number;
+  year: number;
+  assetLiabilityRatio?: number; // 资产负债率(单位:%)
+  dataDeadline: string;
+  createTime: string;
+  updateTime: string;
+}
+
+interface DebtRatioMetricsProps {
+  data?: FinancialDataItem[];
+}
+
+const DebtRatioMetrics: React.FC<DebtRatioMetricsProps> = ({ data }) => {
+  // 从数组数据中提取2023和2024年的数据
+  const data2023 = data?.find(item => item.year === 2023);
+  const data2024 = data?.find(item => item.year === 2024);
+
+  // 计算最大值用于图表比例
+  const maxValue = data ? Math.max(
+    data2023?.assetLiabilityRatio || 0,
+    data2024?.assetLiabilityRatio || 0
+  ) : 0;
+
+  return (
+    <div className="h-full flex flex-col">
+      {/* 模块标题 */}
+      <div className="text-center mb-4">
+        <h3 className="text-xl font-bold text-white">资产负债率(百分比)</h3>
+        <p className="text-sm text-white/70">资产负债率对比</p>
+      </div>
+
+      {/* 数据图表区域 */}
+      <div className="flex-1 flex items-end justify-center space-x-8">
+        {data ? (
+          <>
+            {/* 2023年数据 */}
+            <div className="flex flex-col items-center">
+              <BarElement
+                value={data2023?.assetLiabilityRatio || 0}
+                maxValue={maxValue}
+                type="debt-ratio"
+                label="2023"
+              />
+              <DataLabel
+                value={data2023?.assetLiabilityRatio || 0}
+                label="资产负债率"
+                unit="%"
+                className="mt-4"
+              />
+            </div>
+
+            {/* 2024年数据 */}
+            <div className="flex flex-col items-center">
+              <BarElement
+                value={data2024?.assetLiabilityRatio || 0}
+                maxValue={maxValue}
+                type="debt-ratio"
+                label="2024"
+              />
+              <DataLabel
+                value={data2024?.assetLiabilityRatio || 0}
+                label="资产负债率"
+                unit="%"
+                className="mt-4"
+              />
+            </div>
+          </>
+        ) : (
+          <div className="text-white/50 text-center">
+            数据加载中...
+          </div>
+        )}
+      </div>
+
+      {/* 图例说明 */}
+      <div className="flex justify-center space-x-6 mt-4 text-xs text-white/60">
+        <div className="flex items-center">
+          <div className="w-3 h-3 bg-blue-500 rounded mr-1" />
+          <span>2023年负债率</span>
+        </div>
+        <div className="flex items-center">
+          <div className="w-3 h-3 bg-blue-700 rounded mr-1" />
+          <span>2024年负债率</span>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default DebtRatioMetrics;

+ 24 - 0
src/client/home/pages/FinancialDashboard/components/GridBackground.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+
+const GridBackground: React.FC = () => {
+  return (
+    <div className="absolute inset-0 bg-gray-900">
+      {/* 网格背景 */}
+      <div
+        className="absolute inset-0 opacity-20"
+        style={{
+          backgroundImage: `
+            linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
+            linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px)
+          `,
+          backgroundSize: '50px 50px'
+        }}
+      />
+
+      {/* 渐变遮罩 */}
+      <div className="absolute inset-0 bg-gradient-to-br from-blue-900/10 via-transparent to-purple-900/10" />
+    </div>
+  );
+};
+
+export default GridBackground;

+ 32 - 0
src/client/home/pages/FinancialDashboard/components/Icon.tsx

@@ -0,0 +1,32 @@
+import React from 'react';
+
+interface IconProps {
+  className?: string;
+}
+
+const Icon: React.FC<IconProps> = ({ className = '' }) => {
+  return (
+    <div className={className} data-name="返回icon" data-node-id="853:111934">
+      <div className="absolute aspect-[84/84] bottom-[2.38%] flex items-center justify-center left-1/2 top-[2.38%] translate-x-[-50%]">
+        <div className="flex-none rotate-[180deg] scale-y-[-100%] size-[80px]">
+          <div className="relative size-full" data-node-id="1412:67849">
+            <div className="absolute inset-[-1.25%]">
+              <img alt="" className="block max-w-none size-full" src="/financial-dashboard/ellipse4119.png" />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className="absolute flex inset-[31.17%_39.47%] items-center justify-center">
+        <div className="flex-none h-[17.684px] rotate-[270deg] w-[31.641px]">
+          <div className="relative size-full" data-name="路径" data-node-id="853:111931">
+            <div className="absolute inset-[-50.89%_-28.44%]">
+              <img alt="" className="block max-w-none size-full" src="/financial-dashboard/icon-path.png" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Icon;

+ 95 - 0
src/client/home/pages/FinancialDashboard/components/IncomeMetrics.tsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import BarElement from './BarElement';
+import DataLabel from './DataLabel';
+
+interface FinancialDataItem {
+  id: number;
+  year: number;
+  income?: number;      // 收入(单位:元)
+  dataDeadline: string;
+  createTime: string;
+  updateTime: string;
+}
+
+interface IncomeMetricsProps {
+  data?: FinancialDataItem[];
+}
+
+const IncomeMetrics: React.FC<IncomeMetricsProps> = ({ data }) => {
+  // 从数组数据中提取2023和2024年的数据
+  const data2023 = data?.find(item => item.year === 2023);
+  const data2024 = data?.find(item => item.year === 2024);
+
+  // 计算最大值用于图表比例
+  const maxValue = data ? Math.max(
+    data2023?.income || 0,
+    data2024?.income || 0
+  ) : 0;
+
+  return (
+    <div className="h-full flex flex-col">
+      {/* 模块标题 */}
+      <div className="text-center mb-4">
+        <h3 className="text-xl font-bold text-white">收入</h3>
+        <p className="text-sm text-white/70">收入数据对比</p>
+      </div>
+
+      {/* 数据图表区域 */}
+      <div className="flex-1 flex items-end justify-center space-x-8">
+        {data ? (
+          <>
+            {/* 2023年数据 */}
+            <div className="flex flex-col items-center">
+              <BarElement
+                value={data2023?.income || 0}
+                maxValue={maxValue}
+                type="income"
+                label="2023"
+              />
+              <DataLabel
+                value={data2023?.income || 0}
+                label="收入"
+                unit="元"
+                className="mt-4"
+              />
+            </div>
+
+            {/* 2024年数据 */}
+            <div className="flex flex-col items-center">
+              <BarElement
+                value={data2024?.income || 0}
+                maxValue={maxValue}
+                type="income"
+                label="2024"
+              />
+              <DataLabel
+                value={data2024?.income || 0}
+                label="收入"
+                unit="元"
+                className="mt-4"
+              />
+            </div>
+          </>
+        ) : (
+          <div className="text-white/50 text-center">
+            数据加载中...
+          </div>
+        )}
+      </div>
+
+      {/* 图例说明 */}
+      <div className="flex justify-center space-x-6 mt-4 text-xs text-white/60">
+        <div className="flex items-center">
+          <div className="w-3 h-3 bg-blue-500 rounded mr-1" />
+          <span>2023年收入</span>
+        </div>
+        <div className="flex items-center">
+          <div className="w-3 h-3 bg-yellow-500 rounded mr-1" />
+          <span>2024年收入</span>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default IncomeMetrics;

+ 18 - 0
src/client/home/pages/FinancialDashboard/components/ModalCloseButton.tsx

@@ -0,0 +1,18 @@
+import React from 'react';
+
+interface ModalCloseButtonProps {
+  onClick: () => void;
+}
+
+const ModalCloseButton: React.FC<ModalCloseButtonProps> = ({ onClick }) => {
+  return (
+    <button
+      onClick={onClick}
+      className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors duration-200 font-medium"
+    >
+      返回
+    </button>
+  );
+};
+
+export default ModalCloseButton;

+ 69 - 0
src/client/home/pages/FinancialDashboard/components/ModalContent.tsx

@@ -0,0 +1,69 @@
+import React from 'react';
+import ModalHeader from './ModalHeader';
+import ModalDataRow from './ModalDataRow';
+import ModalCloseButton from './ModalCloseButton';
+
+interface ModalContentProps {
+  data?: any;
+  onClose: () => void;
+}
+
+const ModalContent: React.FC<ModalContentProps> = ({ data, onClose }) => {
+  return (
+    <div className="p-6">
+      {/* 弹窗头部 */}
+      <ModalHeader title="变动幅度" />
+
+      {/* 数据内容 */}
+      <div className="mt-6 space-y-4">
+        {data && (
+          <>
+            <ModalDataRow
+              label="资产总额"
+              value2023={data.assetTotalNet?.[2023]}
+              value2024={data.assetTotalNet?.[2024]}
+              unit="元"
+            />
+            <ModalDataRow
+              label="资产净额"
+              value2023={data.assetTotalNet?.[2023]}
+              value2024={data.assetTotalNet?.[2024]}
+              unit="元"
+            />
+            <ModalDataRow
+              label="利润总额"
+              value2023={data.profitTotalNet?.[2023]}
+              value2024={data.profitTotalNet?.[2024]}
+              unit="元"
+            />
+            <ModalDataRow
+              label="净利润"
+              value2023={data.profitTotalNet?.[2023]}
+              value2024={data.profitTotalNet?.[2024]}
+              unit="元"
+            />
+            <ModalDataRow
+              label="收入"
+              value2023={data.incomeStatement?.[2023]}
+              value2024={data.incomeStatement?.[2024]}
+              unit="元"
+            />
+            <ModalDataRow
+              label="资产负债率"
+              value2023={data.assetLiabilityRatio?.[2023]}
+              value2024={data.assetLiabilityRatio?.[2024]}
+              unit="%"
+            />
+          </>
+        )}
+      </div>
+
+      {/* 关闭按钮 */}
+      <div className="mt-8 flex justify-center">
+        <ModalCloseButton onClick={onClose} />
+      </div>
+    </div>
+  );
+};
+
+export default ModalContent;

+ 57 - 0
src/client/home/pages/FinancialDashboard/components/ModalDataRow.tsx

@@ -0,0 +1,57 @@
+import React from 'react';
+
+interface ModalDataRowProps {
+  label: string;
+  value2023?: number;
+  value2024?: number;
+  unit?: string;
+}
+
+const ModalDataRow: React.FC<ModalDataRowProps> = ({
+  label,
+  value2023,
+  value2024,
+  unit
+}) => {
+  const calculateChange = () => {
+    if (!value2023 || !value2024) return 0;
+    return ((value2024 - value2023) / value2023) * 100;
+  };
+
+  const change = calculateChange();
+  const isPositive = change >= 0;
+
+  return (
+    <div className="flex justify-between items-center py-3 border-b border-gray-600/30">
+      <div className="text-white font-medium">{label}</div>
+
+      <div className="flex items-center space-x-6">
+        {/* 2023年数据 */}
+        <div className="text-right">
+          <div className="text-white/70 text-sm">2023</div>
+          <div className="text-white font-medium">
+            {value2023 ? value2023.toLocaleString() : '-'} {unit}
+          </div>
+        </div>
+
+        {/* 2024年数据 */}
+        <div className="text-right">
+          <div className="text-white/70 text-sm">2024</div>
+          <div className="text-white font-medium">
+            {value2024 ? value2024.toLocaleString() : '-'} {unit}
+          </div>
+        </div>
+
+        {/* 变动幅度 */}
+        <div className="text-right">
+          <div className="text-white/70 text-sm">变动</div>
+          <div className={`font-medium ${isPositive ? 'text-green-400' : 'text-red-400'}`}>
+            {value2023 && value2024 ? `${isPositive ? '+' : ''}${change.toFixed(2)}%` : '-'}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ModalDataRow;

+ 15 - 0
src/client/home/pages/FinancialDashboard/components/ModalDialog.tsx

@@ -0,0 +1,15 @@
+import React from 'react';
+
+interface ModalDialogProps {
+  children: React.ReactNode;
+}
+
+const ModalDialog: React.FC<ModalDialogProps> = ({ children }) => {
+  return (
+    <div className="relative bg-gray-800/90 border border-gray-600/50 rounded-lg shadow-2xl backdrop-blur-sm w-[600px] max-h-[80vh] overflow-hidden">
+      {children}
+    </div>
+  );
+};
+
+export default ModalDialog;

+ 16 - 0
src/client/home/pages/FinancialDashboard/components/ModalHeader.tsx

@@ -0,0 +1,16 @@
+import React from 'react';
+
+interface ModalHeaderProps {
+  title?: string;
+}
+
+const ModalHeader: React.FC<ModalHeaderProps> = ({ title = "变动幅度" }) => {
+  return (
+    <div className="text-center">
+      <h2 className="text-2xl font-bold text-white">{title}</h2>
+      <div className="h-px bg-gradient-to-r from-transparent via-blue-500 to-transparent mt-2" />
+    </div>
+  );
+};
+
+export default ModalHeader;

+ 16 - 0
src/client/home/pages/FinancialDashboard/components/ModalOverlay.tsx

@@ -0,0 +1,16 @@
+import React from 'react';
+
+interface ModalOverlayProps {
+  onClick: () => void;
+}
+
+const ModalOverlay: React.FC<ModalOverlayProps> = ({ onClick }) => {
+  return (
+    <div
+      className="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity duration-300"
+      onClick={onClick}
+    />
+  );
+};
+
+export default ModalOverlay;

+ 98 - 0
src/client/home/pages/FinancialDashboard/components/ProfitMetrics.tsx

@@ -0,0 +1,98 @@
+import React from 'react';
+import BarElement from './BarElement';
+import DataLabel from './DataLabel';
+
+interface FinancialDataItem {
+  id: number;
+  year: number;
+  profitTotal?: number; // 利润总额(单位:元)
+  profitNet?: number;   // 净利润(单位:元)
+  dataDeadline: string;
+  createTime: string;
+  updateTime: string;
+}
+
+interface ProfitMetricsProps {
+  data?: FinancialDataItem[];
+}
+
+const ProfitMetrics: React.FC<ProfitMetricsProps> = ({ data }) => {
+  // 从数组数据中提取2023和2024年的数据
+  const data2023 = data?.find(item => item.year === 2023);
+  const data2024 = data?.find(item => item.year === 2024);
+
+  // 计算最大值用于图表比例(暂时不使用,保留用于未来动态高度)
+  const _maxValue = data ? Math.max(
+    data2023?.profitTotal || 0,
+    data2023?.profitNet || 0,
+    data2024?.profitTotal || 0,
+    data2024?.profitNet || 0
+  ) : 0;
+
+  return (
+    <div className="h-full flex flex-col">
+      {/* 模块标题 */}
+      <div className="text-center mb-4">
+        <h3 className="text-xl font-bold text-white">利润总额与净利润</h3>
+        <p className="text-sm text-white/70">利润数据对比</p>
+      </div>
+
+      {/* 数据图表区域 */}
+      <div className="flex-1 flex items-end justify-center space-x-8">
+        {data ? (
+          <>
+            {/* 2023年数据 */}
+            <div className="flex flex-col items-center">
+              <BarElement
+                value={data2023?.profitTotal || 0}
+                maxValue={_maxValue}
+                type="profit-total"
+                label="2023"
+              />
+              <DataLabel
+                value={data2023?.profitTotal || 0}
+                label="利润总额"
+                unit="元"
+                className="mt-4"
+              />
+            </div>
+
+            {/* 2024年数据 */}
+            <div className="flex flex-col items-center">
+              <BarElement
+                value={data2024?.profitNet || 0}
+                maxValue={_maxValue}
+                type="profit-net"
+                label="2024"
+              />
+              <DataLabel
+                value={data2024?.profitNet || 0}
+                label="净利润"
+                unit="元"
+                className="mt-4"
+              />
+            </div>
+          </>
+        ) : (
+          <div className="text-white/50 text-center">
+            数据加载中...
+          </div>
+        )}
+      </div>
+
+      {/* 图例说明 */}
+      <div className="flex justify-center space-x-6 mt-4 text-xs text-white/60">
+        <div className="flex items-center">
+          <div className="w-3 h-3 bg-yellow-500 rounded mr-1" />
+          <span>利润总额</span>
+        </div>
+        <div className="flex items-center">
+          <div className="w-3 h-3 bg-purple-500 rounded mr-1" />
+          <span>净利润</span>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ProfitMetrics;

+ 39 - 0
src/client/home/pages/FinancialDashboard/components/ReportHeader.tsx

@@ -0,0 +1,39 @@
+import React from 'react';
+
+interface ReportHeaderProps {
+  className?: string;
+  title?: string;
+}
+
+const ReportHeader: React.FC<ReportHeaderProps> = ({
+  className = '',
+  title = "资产负债率"
+}) => {
+  return (
+    <div className={className} data-name="报表头部" data-node-id="1705:119710">
+      {/* 渐变背景 */}
+      <div className="absolute bg-gradient-to-r blur-[14.5px] bottom-0 filter from-[rgba(162,190,255,0)] left-[18.55%] right-[18.15%] to-[95.957%] to-[rgba(162,190,255,0)] top-0 via-[51.477%] via-[rgba(162,190,255,0.302)]" data-node-id="1705:119700" />
+
+      {/* 左侧装饰 */}
+      <div className="absolute flex inset-[38.16%_77.08%_38.16%_0.03%] items-center justify-center">
+        <div className="flex-none h-[9px] rotate-[180deg] scale-y-[-100%] w-[136px]">
+          <div className="relative size-full" data-name="左侧" data-node-id="1705:119702">
+            <img alt="" className="block max-w-none size-full" src="/financial-dashboard/header-left.png" />
+          </div>
+        </div>
+      </div>
+
+      {/* 标题 */}
+      <p className="absolute font-['HarmonyOS_Sans_SC:Regular',sans-serif] inset-[19.74%_29.03%_19.74%_28.81%] leading-[normal] not-italic text-[20px] text-center text-white whitespace-pre-wrap" data-node-id="1705:119705">
+        {title}
+      </p>
+
+      {/* 右侧装饰 */}
+      <div className="absolute inset-[38.16%_0.24%_38.16%_76.86%]" data-name="右侧" data-node-id="1705:119706">
+        <img alt="" className="block max-w-none size-full" src="/financial-dashboard/header-right.png" />
+      </div>
+    </div>
+  );
+};
+
+export default ReportHeader;

+ 29 - 0
src/client/home/pages/FinancialDashboard/components/TimeIcon.tsx

@@ -0,0 +1,29 @@
+import React from 'react';
+
+interface TimeIconProps {
+  className?: string;
+}
+
+const TimeIcon: React.FC<TimeIconProps> = ({ className = '' }) => {
+  return (
+    <svg
+      width="20"
+      height="20"
+      viewBox="0 0 20 20"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      className={`text-white/70 ${className}`}
+    >
+      <circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="2" />
+      <path
+        d="M10 5V10L13 13"
+        stroke="currentColor"
+        strokeWidth="2"
+        strokeLinecap="round"
+        strokeLinejoin="round"
+      />
+    </svg>
+  );
+};
+
+export default TimeIcon;

+ 52 - 0
src/client/home/pages/FinancialDashboard/components/VariationModal.tsx

@@ -0,0 +1,52 @@
+import React, { useEffect } from 'react';
+import ModalOverlay from './ModalOverlay';
+import ModalDialog from './ModalDialog';
+import ModalContent from './ModalContent';
+
+interface VariationModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  data?: any;
+}
+
+const VariationModal: React.FC<VariationModalProps> = ({
+  isOpen,
+  onClose,
+  data
+}) => {
+  // 点击外部关闭弹窗
+  useEffect(() => {
+    const handleEscape = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        onClose();
+      }
+    };
+
+    if (isOpen) {
+      document.addEventListener('keydown', handleEscape);
+      document.body.style.overflow = 'hidden';
+    }
+
+    return () => {
+      document.removeEventListener('keydown', handleEscape);
+      document.body.style.overflow = 'unset';
+    };
+  }, [isOpen, onClose]);
+
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center">
+      {/* 遮罩层 */}
+      <ModalOverlay onClick={onClose} />
+
+      {/* 弹窗容器 */}
+      <ModalDialog>
+        {/* 弹窗内容 */}
+        <ModalContent data={data} onClose={onClose} />
+      </ModalDialog>
+    </div>
+  );
+};
+
+export default VariationModal;

+ 1 - 1
src/client/home/routes.tsx

@@ -8,7 +8,7 @@ import LoginPage from './pages/LoginPage';
 import RegisterPage from './pages/RegisterPage';
 import MemberPage from './pages/MemberPage';
 import GrainOilDashboard from './pages/SupplyChainDashboards/GrainOilDashboard';
-import { FinancialDashboard } from './pages/FinancialDashboard/FinancialDashboard';
+import FinancialDashboard from './pages/FinancialDashboard/FinancialDashboard';
 
 export const router = createBrowserRouter([
   {