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

✨ feat(animation): 集成Framer Motion动画库并优化财务大屏组件

- 集成Framer Motion 12.23.24动画库
- 为BarElement组件添加柱形图高度动画、淡入效果
- 为四个数据模块组件添加容器缩放、标题上浮、数据项交错入场动画
- 优化弹窗组件动画:遮罩层淡入淡出、弹窗缩放上浮效果
- 使用AnimatePresence管理组件卸载动画
- 实现交错延迟动画序列,提升用户体验

🤖 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
bc1a8d0e99

+ 3 - 2
package.json

@@ -83,6 +83,7 @@
     "dotenv": "^17.2.1",
     "embla-carousel-react": "^8.6.0",
     "formdata-node": "^6.0.3",
+    "framer-motion": "^12.23.24",
     "hono": "^4.8.5",
     "input-otp": "^1.4.2",
     "ioredis": "^5.6.1",
@@ -101,15 +102,15 @@
     "react-resizable-panels": "^3.0.4",
     "react-router": "^7.7.0",
     "react-router-dom": "^7.7.0",
-    "recharts": "2.15.4",
     "react-toastify": "^11.0.5",
+    "recharts": "2.15.4",
     "reflect-metadata": "^0.2.2",
     "sirv": "^3.0.1",
-    "uuid": "^11.1.0",
     "sonner": "^2.0.7",
     "tailwind-merge": "^3.3.1",
     "tw-animate-css": "^1.3.6",
     "typeorm": "^0.3.25",
+    "uuid": "^11.1.0",
     "vaul": "^1.1.2",
     "zod": "^4.0.15"
   },

+ 38 - 0
pnpm-lock.yaml

@@ -149,6 +149,9 @@ importers:
       formdata-node:
         specifier: ^6.0.3
         version: 6.0.3
+      framer-motion:
+        specifier: ^12.23.24
+        version: 12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
       hono:
         specifier: ^4.8.5
         version: 4.8.9
@@ -2476,6 +2479,20 @@ packages:
     resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==}
     engines: {node: '>= 18'}
 
+  framer-motion@12.23.24:
+    resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
+    peerDependencies:
+      '@emotion/is-prop-valid': '*'
+      react: ^18.0.0 || ^19.0.0
+      react-dom: ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@emotion/is-prop-valid':
+        optional: true
+      react:
+        optional: true
+      react-dom:
+        optional: true
+
   fsevents@2.3.2:
     resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -3049,6 +3066,12 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
+  motion-dom@12.23.23:
+    resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
+
+  motion-utils@12.23.6:
+    resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
+
   mrmime@2.0.1:
     resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
     engines: {node: '>=10'}
@@ -6245,6 +6268,15 @@ snapshots:
 
   formdata-node@6.0.3: {}
 
+  framer-motion@12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+    dependencies:
+      motion-dom: 12.23.23
+      motion-utils: 12.23.6
+      tslib: 2.8.1
+    optionalDependencies:
+      react: 19.1.0
+      react-dom: 19.1.0(react@19.1.0)
+
   fsevents@2.3.2:
     optional: true
 
@@ -6801,6 +6833,12 @@ snapshots:
 
   mkdirp@3.0.1: {}
 
+  motion-dom@12.23.23:
+    dependencies:
+      motion-utils: 12.23.6
+
+  motion-utils@12.23.6: {}
+
   mrmime@2.0.1: {}
 
   ms@2.0.0: {}

+ 43 - 12
src/client/home/pages/FinancialDashboard/components/AssetMetrics.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { motion } from 'framer-motion';
 import BarElement from './BarElement';
 import DataLabel from './DataLabel';
 
@@ -30,19 +31,34 @@ const AssetMetrics: React.FC<AssetMetricsProps> = ({ data }) => {
   ) : 0;
 
   return (
-    <div className="h-full flex flex-col">
+    <motion.div
+      className="h-full flex flex-col"
+      initial={{ opacity: 0, scale: 0.95 }}
+      animate={{ opacity: 1, scale: 1 }}
+      transition={{ duration: 0.6 }}
+    >
       {/* 模块标题 */}
-      <div className="text-center mb-4">
+      <motion.div
+        className="text-center mb-4"
+        initial={{ opacity: 0, y: -20 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.5 }}
+      >
         <h3 className="text-xl font-bold text-white">资产负债率</h3>
         <p className="text-sm text-white/70">资产总额与资产净额</p>
-      </div>
+      </motion.div>
 
       {/* 数据图表区域 */}
       <div className="flex-1 flex items-end justify-center space-x-8">
         {data ? (
           <>
             {/* 2023年数据 */}
-            <div className="flex flex-col items-center">
+            <motion.div
+              className="flex flex-col items-center"
+              initial={{ opacity: 0, x: -30 }}
+              animate={{ opacity: 1, x: 0 }}
+              transition={{ duration: 0.5, delay: 0.2 }}
+            >
               <BarElement
                 value={data2023?.assetTotal || 0}
                 maxValue={_maxValue}
@@ -55,10 +71,15 @@ const AssetMetrics: React.FC<AssetMetricsProps> = ({ data }) => {
                 unit="元"
                 className="mt-4"
               />
-            </div>
+            </motion.div>
 
             {/* 2024年数据 */}
-            <div className="flex flex-col items-center">
+            <motion.div
+              className="flex flex-col items-center"
+              initial={{ opacity: 0, x: 30 }}
+              animate={{ opacity: 1, x: 0 }}
+              transition={{ duration: 0.5, delay: 0.4 }}
+            >
               <BarElement
                 value={data2024?.assetNet || 0}
                 maxValue={_maxValue}
@@ -71,17 +92,27 @@ const AssetMetrics: React.FC<AssetMetricsProps> = ({ data }) => {
                 unit="元"
                 className="mt-4"
               />
-            </div>
+            </motion.div>
           </>
         ) : (
-          <div className="text-white/50 text-center">
+          <motion.div
+            className="text-white/50 text-center"
+            initial={{ opacity: 0 }}
+            animate={{ opacity: 1 }}
+            transition={{ duration: 0.5 }}
+          >
             数据加载中...
-          </div>
+          </motion.div>
         )}
       </div>
 
       {/* 图例说明 */}
-      <div className="flex justify-center space-x-6 mt-4 text-xs text-white/60">
+      <motion.div
+        className="flex justify-center space-x-6 mt-4 text-xs text-white/60"
+        initial={{ opacity: 0, y: 20 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.5, delay: 0.6 }}
+      >
         <div className="flex items-center">
           <div className="w-3 h-3 bg-blue-500 rounded mr-1" />
           <span>资产总额</span>
@@ -90,8 +121,8 @@ const AssetMetrics: React.FC<AssetMetricsProps> = ({ data }) => {
           <div className="w-3 h-3 bg-yellow-500 rounded mr-1" />
           <span>资产净额</span>
         </div>
-      </div>
-    </div>
+      </motion.div>
+    </motion.div>
   );
 };
 

+ 31 - 8
src/client/home/pages/FinancialDashboard/components/BarElement.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { motion } from 'framer-motion';
 
 interface BarElementProps {
   className?: string;
@@ -39,29 +40,51 @@ const BarElement: React.FC<BarElementProps> = ({
   };
 
   return (
-    <div className={className}>
+    <motion.div
+      className={className}
+      initial={{ opacity: 0, y: 20 }}
+      animate={{ opacity: 1, y: 0 }}
+      transition={{ duration: 0.5, delay: 0.1 }}
+    >
       {/* 柱形图主体 */}
       <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`}
+        <motion.div
+          className={`w-12 bg-gradient-to-b ${getBarColor()} rounded-t-lg`}
           style={{ height: `${Math.max(heightPercentage, 10)}%` }}
+          initial={{ height: '10%' }}
+          animate={{ height: `${Math.max(heightPercentage, 10)}%` }}
+          transition={{
+            duration: 1.2,
+            ease: "easeOut",
+            delay: 0.3
+          }}
         />
 
         {/* 标签 */}
-        <div className="mt-2 text-white text-sm font-medium">
+        <motion.div
+          className="mt-2 text-white text-sm font-medium"
+          initial={{ opacity: 0 }}
+          animate={{ opacity: 1 }}
+          transition={{ duration: 0.5, delay: 0.8 }}
+        >
           {label}
-        </div>
+        </motion.div>
 
         {/* 数值显示 */}
-        <div className="mt-1 text-white/70 text-xs">
+        <motion.div
+          className="mt-1 text-white/70 text-xs"
+          initial={{ opacity: 0, scale: 0.8 }}
+          animate={{ opacity: 1, scale: 1 }}
+          transition={{ duration: 0.4, delay: 1.0 }}
+        >
           {type === 'debt-ratio' ? `${value.toFixed(1)}%` :
            value >= 100000000 ? `${(value / 100000000).toFixed(1)}亿` :
            value >= 10000 ? `${(value / 10000).toFixed(1)}万` :
            value.toLocaleString()}
-        </div>
+        </motion.div>
       </div>
-    </div>
+    </motion.div>
   );
 };
 

+ 43 - 12
src/client/home/pages/FinancialDashboard/components/DebtRatioMetrics.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { motion } from 'framer-motion';
 import BarElement from './BarElement';
 import DataLabel from './DataLabel';
 
@@ -27,19 +28,34 @@ const DebtRatioMetrics: React.FC<DebtRatioMetricsProps> = ({ data }) => {
   ) : 0;
 
   return (
-    <div className="h-full flex flex-col">
+    <motion.div
+      className="h-full flex flex-col"
+      initial={{ opacity: 0, scale: 0.95 }}
+      animate={{ opacity: 1, scale: 1 }}
+      transition={{ duration: 0.6, delay: 0.3 }}
+    >
       {/* 模块标题 */}
-      <div className="text-center mb-4">
+      <motion.div
+        className="text-center mb-4"
+        initial={{ opacity: 0, y: -20 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.5, delay: 0.4 }}
+      >
         <h3 className="text-xl font-bold text-white">资产负债率(百分比)</h3>
         <p className="text-sm text-white/70">资产负债率对比</p>
-      </div>
+      </motion.div>
 
       {/* 数据图表区域 */}
       <div className="flex-1 flex items-end justify-center space-x-8">
         {data ? (
           <>
             {/* 2023年数据 */}
-            <div className="flex flex-col items-center">
+            <motion.div
+              className="flex flex-col items-center"
+              initial={{ opacity: 0, x: -30 }}
+              animate={{ opacity: 1, x: 0 }}
+              transition={{ duration: 0.5, delay: 0.5 }}
+            >
               <BarElement
                 value={data2023?.assetLiabilityRatio || 0}
                 maxValue={maxValue}
@@ -52,10 +68,15 @@ const DebtRatioMetrics: React.FC<DebtRatioMetricsProps> = ({ data }) => {
                 unit="%"
                 className="mt-4"
               />
-            </div>
+            </motion.div>
 
             {/* 2024年数据 */}
-            <div className="flex flex-col items-center">
+            <motion.div
+              className="flex flex-col items-center"
+              initial={{ opacity: 0, x: 30 }}
+              animate={{ opacity: 1, x: 0 }}
+              transition={{ duration: 0.5, delay: 0.7 }}
+            >
               <BarElement
                 value={data2024?.assetLiabilityRatio || 0}
                 maxValue={maxValue}
@@ -68,17 +89,27 @@ const DebtRatioMetrics: React.FC<DebtRatioMetricsProps> = ({ data }) => {
                 unit="%"
                 className="mt-4"
               />
-            </div>
+            </motion.div>
           </>
         ) : (
-          <div className="text-white/50 text-center">
+          <motion.div
+            className="text-white/50 text-center"
+            initial={{ opacity: 0 }}
+            animate={{ opacity: 1 }}
+            transition={{ duration: 0.5 }}
+          >
             数据加载中...
-          </div>
+          </motion.div>
         )}
       </div>
 
       {/* 图例说明 */}
-      <div className="flex justify-center space-x-6 mt-4 text-xs text-white/60">
+      <motion.div
+        className="flex justify-center space-x-6 mt-4 text-xs text-white/60"
+        initial={{ opacity: 0, y: 20 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.5, delay: 0.9 }}
+      >
         <div className="flex items-center">
           <div className="w-3 h-3 bg-blue-500 rounded mr-1" />
           <span>2023年负债率</span>
@@ -87,8 +118,8 @@ const DebtRatioMetrics: React.FC<DebtRatioMetricsProps> = ({ data }) => {
           <div className="w-3 h-3 bg-blue-700 rounded mr-1" />
           <span>2024年负债率</span>
         </div>
-      </div>
-    </div>
+      </motion.div>
+    </motion.div>
   );
 };
 

+ 43 - 12
src/client/home/pages/FinancialDashboard/components/IncomeMetrics.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { motion } from 'framer-motion';
 import BarElement from './BarElement';
 import DataLabel from './DataLabel';
 
@@ -27,19 +28,34 @@ const IncomeMetrics: React.FC<IncomeMetricsProps> = ({ data }) => {
   ) : 0;
 
   return (
-    <div className="h-full flex flex-col">
+    <motion.div
+      className="h-full flex flex-col"
+      initial={{ opacity: 0, scale: 0.95 }}
+      animate={{ opacity: 1, scale: 1 }}
+      transition={{ duration: 0.6, delay: 0.2 }}
+    >
       {/* 模块标题 */}
-      <div className="text-center mb-4">
+      <motion.div
+        className="text-center mb-4"
+        initial={{ opacity: 0, y: -20 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.5, delay: 0.3 }}
+      >
         <h3 className="text-xl font-bold text-white">收入</h3>
         <p className="text-sm text-white/70">收入数据对比</p>
-      </div>
+      </motion.div>
 
       {/* 数据图表区域 */}
       <div className="flex-1 flex items-end justify-center space-x-8">
         {data ? (
           <>
             {/* 2023年数据 */}
-            <div className="flex flex-col items-center">
+            <motion.div
+              className="flex flex-col items-center"
+              initial={{ opacity: 0, x: -30 }}
+              animate={{ opacity: 1, x: 0 }}
+              transition={{ duration: 0.5, delay: 0.4 }}
+            >
               <BarElement
                 value={data2023?.income || 0}
                 maxValue={maxValue}
@@ -52,10 +68,15 @@ const IncomeMetrics: React.FC<IncomeMetricsProps> = ({ data }) => {
                 unit="元"
                 className="mt-4"
               />
-            </div>
+            </motion.div>
 
             {/* 2024年数据 */}
-            <div className="flex flex-col items-center">
+            <motion.div
+              className="flex flex-col items-center"
+              initial={{ opacity: 0, x: 30 }}
+              animate={{ opacity: 1, x: 0 }}
+              transition={{ duration: 0.5, delay: 0.6 }}
+            >
               <BarElement
                 value={data2024?.income || 0}
                 maxValue={maxValue}
@@ -68,17 +89,27 @@ const IncomeMetrics: React.FC<IncomeMetricsProps> = ({ data }) => {
                 unit="元"
                 className="mt-4"
               />
-            </div>
+            </motion.div>
           </>
         ) : (
-          <div className="text-white/50 text-center">
+          <motion.div
+            className="text-white/50 text-center"
+            initial={{ opacity: 0 }}
+            animate={{ opacity: 1 }}
+            transition={{ duration: 0.5 }}
+          >
             数据加载中...
-          </div>
+          </motion.div>
         )}
       </div>
 
       {/* 图例说明 */}
-      <div className="flex justify-center space-x-6 mt-4 text-xs text-white/60">
+      <motion.div
+        className="flex justify-center space-x-6 mt-4 text-xs text-white/60"
+        initial={{ opacity: 0, y: 20 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.5, delay: 0.8 }}
+      >
         <div className="flex items-center">
           <div className="w-3 h-3 bg-blue-500 rounded mr-1" />
           <span>2023年收入</span>
@@ -87,8 +118,8 @@ const IncomeMetrics: React.FC<IncomeMetricsProps> = ({ data }) => {
           <div className="w-3 h-3 bg-yellow-500 rounded mr-1" />
           <span>2024年收入</span>
         </div>
-      </div>
-    </div>
+      </motion.div>
+    </motion.div>
   );
 };
 

+ 43 - 12
src/client/home/pages/FinancialDashboard/components/ProfitMetrics.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { motion } from 'framer-motion';
 import BarElement from './BarElement';
 import DataLabel from './DataLabel';
 
@@ -30,19 +31,34 @@ const ProfitMetrics: React.FC<ProfitMetricsProps> = ({ data }) => {
   ) : 0;
 
   return (
-    <div className="h-full flex flex-col">
+    <motion.div
+      className="h-full flex flex-col"
+      initial={{ opacity: 0, scale: 0.95 }}
+      animate={{ opacity: 1, scale: 1 }}
+      transition={{ duration: 0.6, delay: 0.1 }}
+    >
       {/* 模块标题 */}
-      <div className="text-center mb-4">
+      <motion.div
+        className="text-center mb-4"
+        initial={{ opacity: 0, y: -20 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.5, delay: 0.2 }}
+      >
         <h3 className="text-xl font-bold text-white">利润总额与净利润</h3>
         <p className="text-sm text-white/70">利润数据对比</p>
-      </div>
+      </motion.div>
 
       {/* 数据图表区域 */}
       <div className="flex-1 flex items-end justify-center space-x-8">
         {data ? (
           <>
             {/* 2023年数据 */}
-            <div className="flex flex-col items-center">
+            <motion.div
+              className="flex flex-col items-center"
+              initial={{ opacity: 0, x: -30 }}
+              animate={{ opacity: 1, x: 0 }}
+              transition={{ duration: 0.5, delay: 0.3 }}
+            >
               <BarElement
                 value={data2023?.profitTotal || 0}
                 maxValue={_maxValue}
@@ -55,10 +71,15 @@ const ProfitMetrics: React.FC<ProfitMetricsProps> = ({ data }) => {
                 unit="元"
                 className="mt-4"
               />
-            </div>
+            </motion.div>
 
             {/* 2024年数据 */}
-            <div className="flex flex-col items-center">
+            <motion.div
+              className="flex flex-col items-center"
+              initial={{ opacity: 0, x: 30 }}
+              animate={{ opacity: 1, x: 0 }}
+              transition={{ duration: 0.5, delay: 0.5 }}
+            >
               <BarElement
                 value={data2024?.profitNet || 0}
                 maxValue={_maxValue}
@@ -71,17 +92,27 @@ const ProfitMetrics: React.FC<ProfitMetricsProps> = ({ data }) => {
                 unit="元"
                 className="mt-4"
               />
-            </div>
+            </motion.div>
           </>
         ) : (
-          <div className="text-white/50 text-center">
+          <motion.div
+            className="text-white/50 text-center"
+            initial={{ opacity: 0 }}
+            animate={{ opacity: 1 }}
+            transition={{ duration: 0.5 }}
+          >
             数据加载中...
-          </div>
+          </motion.div>
         )}
       </div>
 
       {/* 图例说明 */}
-      <div className="flex justify-center space-x-6 mt-4 text-xs text-white/60">
+      <motion.div
+        className="flex justify-center space-x-6 mt-4 text-xs text-white/60"
+        initial={{ opacity: 0, y: 20 }}
+        animate={{ opacity: 1, y: 0 }}
+        transition={{ duration: 0.5, delay: 0.7 }}
+      >
         <div className="flex items-center">
           <div className="w-3 h-3 bg-yellow-500 rounded mr-1" />
           <span>利润总额</span>
@@ -90,8 +121,8 @@ const ProfitMetrics: React.FC<ProfitMetricsProps> = ({ data }) => {
           <div className="w-3 h-3 bg-purple-500 rounded mr-1" />
           <span>净利润</span>
         </div>
-      </div>
-    </div>
+      </motion.div>
+    </motion.div>
   );
 };
 

+ 32 - 11
src/client/home/pages/FinancialDashboard/components/VariationModal.tsx

@@ -1,4 +1,5 @@
 import React, { useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
 import ModalOverlay from './ModalOverlay';
 import ModalDialog from './ModalDialog';
 import ModalContent from './ModalContent';
@@ -33,19 +34,39 @@ const VariationModal: React.FC<VariationModalProps> = ({
     };
   }, [isOpen, onClose]);
 
-  if (!isOpen) return null;
-
   return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center">
-      {/* 遮罩层 */}
-      <ModalOverlay onClick={onClose} />
+    <AnimatePresence>
+      {isOpen && (
+        <div className="fixed inset-0 z-50 flex items-center justify-center">
+          {/* 遮罩层 */}
+          <motion.div
+            initial={{ opacity: 0 }}
+            animate={{ opacity: 1 }}
+            exit={{ opacity: 0 }}
+            transition={{ duration: 0.3 }}
+          >
+            <ModalOverlay onClick={onClose} />
+          </motion.div>
 
-      {/* 弹窗容器 */}
-      <ModalDialog>
-        {/* 弹窗内容 */}
-        <ModalContent data={data} onClose={onClose} />
-      </ModalDialog>
-    </div>
+          {/* 弹窗容器 */}
+          <motion.div
+            initial={{ opacity: 0, scale: 0.8, y: 20 }}
+            animate={{ opacity: 1, scale: 1, y: 0 }}
+            exit={{ opacity: 0, scale: 0.8, y: 20 }}
+            transition={{
+              duration: 0.4,
+              ease: "easeOut"
+            }}
+            className="relative z-10"
+          >
+            <ModalDialog>
+              {/* 弹窗内容 */}
+              <ModalContent data={data} onClose={onClose} />
+            </ModalDialog>
+          </motion.div>
+        </div>
+      )}
+    </AnimatePresence>
   );
 };