Преглед изворни кода

✨ feat(FinancialDashboard): 实现财务仪表盘图表UI优化

- 添加ModuleHeader和ModuleHeaderBackground组件,统一模块标题样式
- 为所有图表添加水平网格线背景,提升数据可读性
- 替换动态计算柱形高度为Figma设计规范中的精确高度值
- 优化图表标题容器宽度,修复布局错位问题
- 统一各图表组件的结构和样式,提高代码一致性

💄 style(chart): 优化图表视觉呈现细节

- 调整图表标题区域布局,增强视觉层次感
- 为柱形图添加精确的高度值,确保与设计稿完全一致
- 优化图表说明文字容器样式,提升整体美观度
yourname пре 2 месеци
родитељ
комит
7ebd5bee0f

+ 26 - 11
src/client/home/pages/FinancialDashboard/components/AssetMetrics.tsx

@@ -1,5 +1,7 @@
 import BarElement from './BarElement';
 import BaseContainer from './BaseContainer';
+import ModuleHeader from './ModuleHeader';
+import ModuleHeaderBackground from './ModuleHeaderBackground';
 
 interface AssetMetricsProps {
   className?: string;
@@ -13,11 +15,13 @@ const assetData = [
   { year: '2025年', assetTotal: 840.12, assetNet: 421.55 }
 ];
 
-const maxValue = Math.max(...assetData.map(d => Math.max(d.assetTotal, d.assetNet)));
-
-const calculateHeight = (value: number) => {
-  const maxHeight = 224; // 最大柱形高度
-  return (value / maxValue) * maxHeight;
+// 按照Figma设计规范中的精确柱形高度
+const barHeights = {
+  '2021年': { assetTotal: 90, assetNet: 41 },
+  '2022年': { assetTotal: 100, assetNet: 48 },
+  '2023年': { assetTotal: 145, assetNet: 92 },
+  '2024年': { assetTotal: 175, assetNet: 122 },
+  '2025年': { assetTotal: 180, assetNet: 125 }
 };
 
 export function AssetMetrics({ className }: AssetMetricsProps) {
@@ -30,7 +34,7 @@ export function AssetMetrics({ className }: AssetMetricsProps) {
       <div className="absolute content-stretch flex flex-col gap-[20px] h-[388px] items-start left-[41.4px] top-[79px] w-[847.191px]">
           {/* 顶部标题和说明 */}
           <div className="content-stretch flex items-start justify-between relative shrink-0 w-[847px]">
-            <div className="content-stretch flex gap-[20px] items-center relative shrink-0">
+            <div className="content-stretch flex gap-[20px] items-center relative shrink-0 w-[93px]">
               <div className="content-stretch flex gap-[10px] h-[26px] items-center relative shrink-0">
                 <div className="flex items-center justify-center relative shrink-0">
                   <div className="flex-none rotate-[180deg]">
@@ -79,6 +83,18 @@ export function AssetMetrics({ className }: AssetMetricsProps) {
                 </p>
               ))}
             </div>
+
+            {/* 网格线背景 */}
+            <div className="col-[1] grid-cols-[max-content] grid-rows-[max-content] inline-grid justify-items-start ml-[52px] mt-0 relative row-[1]">
+              {/* 水平网格线 */}
+              <div className="col-[1] h-[2px] ml-[0.12px] mt-0 relative row-[1] w-[794.879px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[40px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[80px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[120px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[160px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[200px] relative row-[1] w-[795.191px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[240px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+            </div>
           </div>
 
           {/* 柱形图数据 */}
@@ -95,7 +111,7 @@ export function AssetMetrics({ className }: AssetMetricsProps) {
                       <BarElement
                         variant="asset-total"
                         className={`w-[35px]`}
-                        style={{ height: `${calculateHeight(data.assetTotal)}px` }}
+                        style={{ height: `${barHeights[data.year as keyof typeof barHeights].assetTotal}px` }}
                       />
                     </div>
                   </div>
@@ -111,7 +127,7 @@ export function AssetMetrics({ className }: AssetMetricsProps) {
                       <BarElement
                         variant="asset-net"
                         className={`w-[35px] rounded-[2px]`}
-                        style={{ height: `${calculateHeight(data.assetNet)}px` }}
+                        style={{ height: `${barHeights[data.year as keyof typeof barHeights].assetNet}px` }}
                       />
                     </div>
                   </div>
@@ -122,9 +138,8 @@ export function AssetMetrics({ className }: AssetMetricsProps) {
         </div>
 
       {/* 模块标题 */}
-      <div className="absolute top-4 left-4">
-        <p className="text-[20px] font-medium text-white">资产负债率</p>
-      </div>
+      <ModuleHeaderBackground className="absolute h-[38px] left-0 top-0 w-[594.001px]" />
+      <ModuleHeader className="absolute h-[38px] left-0 overflow-clip top-0 w-[594px]" title="资产负债率" />
     </div>
   );
 }

+ 23 - 10
src/client/home/pages/FinancialDashboard/components/DebtRatioMetrics.tsx

@@ -1,5 +1,6 @@
 import BarElement from './BarElement';
 import BaseContainer from './BaseContainer';
+import ModuleHeader from './ModuleHeader';
 
 interface DebtRatioMetricsProps {
   className?: string;
@@ -13,11 +14,13 @@ const debtRatioData = [
   { year: '2025年', ratio: 49.82 }
 ];
 
-const maxValue = Math.max(...debtRatioData.map(d => d.ratio));
-
-const calculateHeight = (value: number) => {
-  const maxHeight = 150; // 最大柱形高度
-  return (value / maxValue) * maxHeight;
+// 按照Figma设计规范中的精确柱形高度
+const barHeights = {
+  '2021年': { ratio: 110 },
+  '2022年': { ratio: 115 },
+  '2023年': { ratio: 77 },
+  '2024年': { ratio: 71 },
+  '2025年': { ratio: 75 }
 };
 
 export function DebtRatioMetrics({ className }: DebtRatioMetricsProps) {
@@ -29,7 +32,7 @@ export function DebtRatioMetrics({ className }: DebtRatioMetricsProps) {
         <div className="absolute content-stretch flex flex-col gap-[20px] h-[388px] items-start left-[41.4px] top-[79px] w-[847.191px]">
           {/* 顶部标题和说明 */}
           <div className="content-stretch flex items-start justify-between relative shrink-0 w-[847px]">
-            <div className="content-stretch flex gap-[20px] items-center relative shrink-0">
+            <div className="content-stretch flex gap-[20px] items-center relative shrink-0 w-[93px]">
               <div className="content-stretch flex gap-[10px] h-[26px] items-center relative shrink-0">
                 <div className="flex items-center justify-center relative shrink-0">
                   <div className="flex-none rotate-[180deg]">
@@ -68,6 +71,18 @@ export function DebtRatioMetrics({ className }: DebtRatioMetricsProps) {
                 </p>
               ))}
             </div>
+
+            {/* 网格线背景 */}
+            <div className="col-[1] grid-cols-[max-content] grid-rows-[max-content] inline-grid justify-items-start ml-[52px] mt-0 relative row-[1]">
+              {/* 水平网格线 */}
+              <div className="col-[1] h-[2px] ml-[0.12px] mt-0 relative row-[1] w-[794.879px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[40px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[80px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[120px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[160px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[200px] relative row-[1] w-[795.191px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[240px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+            </div>
           </div>
 
           {/* 柱形图数据 */}
@@ -84,7 +99,7 @@ export function DebtRatioMetrics({ className }: DebtRatioMetricsProps) {
                       <BarElement
                         variant="debt-ratio"
                         className={`w-[35px]`}
-                        style={{ height: `${calculateHeight(data.ratio)}px` }}
+                        style={{ height: `${barHeights[data.year as keyof typeof barHeights].ratio}px` }}
                       />
                     </div>
                   </div>
@@ -95,9 +110,7 @@ export function DebtRatioMetrics({ className }: DebtRatioMetricsProps) {
         </div>
 
       {/* 模块标题 */}
-      <div className="absolute top-4 left-4">
-        <p className="text-[20px] font-medium text-white">资产负债率</p>
-      </div>
+      <ModuleHeader className="absolute h-[38px] left-[calc(50%+-0.25px)] overflow-clip top-0 translate-x-[-50%] w-[594px]" title="资产负债率" />
     </div>
   );
 }

+ 23 - 10
src/client/home/pages/FinancialDashboard/components/IncomeMetrics.tsx

@@ -1,5 +1,6 @@
 import BarElement from './BarElement';
 import BaseContainer from './BaseContainer';
+import ModuleHeader from './ModuleHeader';
 
 interface IncomeMetricsProps {
   className?: string;
@@ -13,11 +14,13 @@ const incomeData = [
   { year: '2025年', income: 840.12 }
 ];
 
-const maxValue = Math.max(...incomeData.map(d => d.income));
-
-const calculateHeight = (value: number) => {
-  const maxHeight = 224; // 最大柱形高度
-  return (value / maxValue) * maxHeight;
+// 按照Figma设计规范中的精确柱形高度
+const barHeights = {
+  '2021年': { income: 70 },
+  '2022年': { income: 100 },
+  '2023年': { income: 145 },
+  '2024年': { income: 175 },
+  '2025年': { income: 180 }
 };
 
 export function IncomeMetrics({ className }: IncomeMetricsProps) {
@@ -29,7 +32,7 @@ export function IncomeMetrics({ className }: IncomeMetricsProps) {
         <div className="absolute content-stretch flex flex-col gap-[20px] h-[388px] items-start left-[41.4px] top-[79px] w-[847.191px]">
           {/* 顶部标题和说明 */}
           <div className="content-stretch flex items-start justify-between relative shrink-0 w-[847px]">
-            <div className="content-stretch flex gap-[20px] items-center relative shrink-0">
+            <div className="content-stretch flex gap-[20px] items-center relative shrink-0 w-[93px]">
               <div className="content-stretch flex gap-[10px] h-[26px] items-center relative shrink-0">
                 <div className="flex items-center justify-center relative shrink-0">
                   <div className="flex-none rotate-[180deg]">
@@ -68,6 +71,18 @@ export function IncomeMetrics({ className }: IncomeMetricsProps) {
                 </p>
               ))}
             </div>
+
+            {/* 网格线背景 */}
+            <div className="col-[1] grid-cols-[max-content] grid-rows-[max-content] inline-grid justify-items-start ml-[52px] mt-0 relative row-[1]">
+              {/* 水平网格线 */}
+              <div className="col-[1] h-[2px] ml-[0.12px] mt-0 relative row-[1] w-[794.879px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[40px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[80px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[120px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[160px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[200px] relative row-[1] w-[795.191px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[240px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+            </div>
           </div>
 
           {/* 柱形图数据 */}
@@ -84,7 +99,7 @@ export function IncomeMetrics({ className }: IncomeMetricsProps) {
                       <BarElement
                         variant="income"
                         className={`w-[35px]`}
-                        style={{ height: `${calculateHeight(data.income)}px` }}
+                        style={{ height: `${barHeights[data.year as keyof typeof barHeights].income}px` }}
                       />
                     </div>
                   </div>
@@ -95,9 +110,7 @@ export function IncomeMetrics({ className }: IncomeMetricsProps) {
         </div>
 
       {/* 模块标题 */}
-      <div className="absolute top-4 left-4">
-        <p className="text-[20px] font-medium text-white">收入</p>
-      </div>
+      <ModuleHeader className="absolute h-[38px] left-[calc(50%+-0.25px)] overflow-clip top-0 translate-x-[-50%] w-[594px]" title="收入" />
     </div>
   );
 }

+ 38 - 0
src/client/home/pages/FinancialDashboard/components/ModuleHeader.tsx

@@ -0,0 +1,38 @@
+interface ModuleHeaderProps {
+  className?: string;
+  title?: string;
+}
+
+export default function ModuleHeader({ className, title }: ModuleHeaderProps) {
+  return (
+    <div className={className} data-name="报表头部">
+      <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)]" />
+      <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="左侧">
+            <img alt="" className="block max-w-none size-full" src="/financial-dashboard/header-left.svg" />
+          </div>
+        </div>
+      </div>
+      <div className="absolute flex inset-[38.16%_0.03%_38.16%_77.08%] items-center justify-center">
+        <div className="flex-none h-[9px] rotate-[180deg] scale-y-[-100%] w-[136px]">
+          <div className="relative size-full" data-name="右侧">
+            <img alt="" className="block max-w-none size-full" src="/financial-dashboard/header-right.svg" />
+          </div>
+        </div>
+      </div>
+      <div className="absolute flex inset-[38.16%_0.03%_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="中间">
+            <img alt="" className="block max-w-none size-full" src="/financial-dashboard/header-top-nav.svg" />
+          </div> */}
+        </div>
+      </div>
+      {title && (
+        <p className="absolute inset-[19.74%_29.03%_19.74%_28.81%] leading-[normal] text-[20px] text-center text-white whitespace-pre-wrap">
+          {title}
+        </p>
+      )}
+    </div>
+  );
+}

+ 7 - 0
src/client/home/pages/FinancialDashboard/components/ModuleHeaderBackground.tsx

@@ -0,0 +1,7 @@
+interface ModuleHeaderBackgroundProps {
+  className?: string;
+}
+
+export default function ModuleHeaderBackground({ className }: ModuleHeaderBackgroundProps) {
+  return <div className={className} data-name="头部" />;
+}

+ 26 - 16
src/client/home/pages/FinancialDashboard/components/ProfitMetrics.tsx

@@ -1,5 +1,7 @@
 import BarElement from './BarElement';
 import BaseContainer from './BaseContainer';
+import ModuleHeader from './ModuleHeader';
+import ModuleHeaderBackground from './ModuleHeaderBackground';
 
 interface ProfitMetricsProps {
   className?: string;
@@ -13,16 +15,13 @@ const profitData = [
   { year: '2025年', profitTotal: 1.34, profitNet: 1.00 }
 ];
 
-const maxPositiveValue = Math.max(...profitData.map(d => Math.max(d.profitTotal, d.profitNet)));
-const minNegativeValue = Math.min(...profitData.map(d => Math.min(d.profitTotal, d.profitNet)));
-
-const calculateHeight = (value: number) => {
-  const maxHeight = 100; // 最大柱形高度
-  if (value >= 0) {
-    return (value / maxPositiveValue) * maxHeight;
-  } else {
-    return (Math.abs(value) / Math.abs(minNegativeValue)) * maxHeight;
-  }
+// 按照Figma设计规范中的精确柱形高度
+const barHeights = {
+  '2021年': { profitTotal: 60, profitNet: 68 },
+  '2022年': { profitTotal: 32, profitNet: 24 },
+  '2023年': { profitTotal: 51, profitNet: 24 },
+  '2024年': { profitTotal: 66, profitNet: 49 },
+  '2025年': { profitTotal: 54, profitNet: 40 }
 };
 
 export function ProfitMetrics({ className }: ProfitMetricsProps) {
@@ -34,7 +33,7 @@ export function ProfitMetrics({ className }: ProfitMetricsProps) {
         <div className="absolute content-stretch flex flex-col gap-[20px] h-[388px] items-start left-[41.4px] top-[79px] w-[847.191px]">
           {/* 顶部标题和说明 */}
           <div className="content-stretch flex items-start justify-between relative shrink-0 w-[847px]">
-            <div className="content-stretch flex gap-[20px] items-center relative shrink-0">
+            <div className="content-stretch flex gap-[20px] items-center relative shrink-0 w-[93px]">
               <div className="content-stretch flex gap-[10px] h-[26px] items-center relative shrink-0">
                 <div className="flex items-center justify-center relative shrink-0">
                   <div className="flex-none rotate-[180deg]">
@@ -83,6 +82,18 @@ export function ProfitMetrics({ className }: ProfitMetricsProps) {
                 </p>
               ))}
             </div>
+
+            {/* 网格线背景 */}
+            <div className="col-[1] grid-cols-[max-content] grid-rows-[max-content] inline-grid justify-items-start ml-[52px] mt-0 relative row-[1]">
+              {/* 水平网格线 */}
+              <div className="col-[1] h-[2px] ml-[0.12px] mt-0 relative row-[1] w-[794.879px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[40px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[80px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[120px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[160px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[200px] relative row-[1] w-[795.191px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+              <div className="col-[1] h-[2px] ml-0 mt-[240px] relative row-[1] w-[795px] bg-gradient-to-r from-[rgba(255,255,255,0.1)] to-[rgba(255,255,255,0.05)]" />
+            </div>
           </div>
 
           {/* 柱形图数据 */}
@@ -99,7 +110,7 @@ export function ProfitMetrics({ className }: ProfitMetricsProps) {
                       <BarElement
                         variant="profit-total"
                         className={`w-[35px]`}
-                        style={{ height: `${calculateHeight(data.profitTotal)}px` }}
+                        style={{ height: `${barHeights[data.year as keyof typeof barHeights].profitTotal}px` }}
                       />
                     </div>
                   </div>
@@ -115,7 +126,7 @@ export function ProfitMetrics({ className }: ProfitMetricsProps) {
                       <BarElement
                         variant="profit-net"
                         className={`w-[35px] rounded-[2px]`}
-                        style={{ height: `${calculateHeight(data.profitNet)}px` }}
+                        style={{ height: `${barHeights[data.year as keyof typeof barHeights].profitNet}px` }}
                       />
                     </div>
                   </div>
@@ -126,9 +137,8 @@ export function ProfitMetrics({ className }: ProfitMetricsProps) {
         </div>
 
       {/* 模块标题 */}
-      <div className="absolute top-4 left-4">
-        <p className="text-[20px] font-medium text-white">利润总额与净利润</p>
-      </div>
+      <ModuleHeaderBackground className="absolute h-[38px] left-0 top-0 w-[594.001px]" />
+      <ModuleHeader className="absolute h-[38px] overflow-clip right-0 top-0 w-[594px]" title="利润总额与净利润" />
     </div>
   );
 }