Переглянути джерело

✨ feat(supply-chain): 实现供应链组件动态数据集成

- 删除GrainOilDashboard组件及相关测试文件,统一使用SupplyChainDashboard
- 在SupplyChainDashboard中集成动态数据获取逻辑
- 修改KeyMetrics组件使用SupplyChainContext中的动态关键指标数据
- 重构PopupInfoBox组件移除硬编码数据,从SupplyChainContext获取弹出框数据
- 更新SupplyChainMap组件使用动态定位点数据
- 添加数据缺失时的toast提示功能
- 更新相关测试用例适配新的数据结构

📝 docs(story): 更新开发进度文档

- 添加PopupInfoBox组件动态数据集成任务
- 更新测试任务描述,将"数据卡片"改为"弹出框数据"
yourname 2 місяців тому
батько
коміт
7450fd5cb6

+ 7 - 1
docs/stories/005.004.story.md

@@ -32,6 +32,12 @@ In Progress
   - [ ] 实现关键指标数据的动态渲染
   - [ ] 实现关键指标数据的动态渲染
   - [ ] 验证种业关键指标正确显示(良种繁育能力、自建育种基地、辐射带动面积)
   - [ ] 验证种业关键指标正确显示(良种繁育能力、自建育种基地、辐射带动面积)
   - [ ] 验证果蔬关键指标正确显示(加工能力、自建果蔬基地、辐射带动面积)
   - [ ] 验证果蔬关键指标正确显示(加工能力、自建果蔬基地、辐射带动面积)
+- [ ] 修改PopupInfoBox组件使用动态数据 (AC: #3, #4)
+  - [ ] 移除组件中的硬编码defaultData
+  - [ ] 从SupplyChainContext中获取当前产业的弹出框数据
+  - [ ] 实现弹出框数据的动态渲染
+  - [ ] 验证种业弹出框数据正确显示
+  - [ ] 验证果蔬弹出框数据正确显示
 - [ ] 验证种业-果蔬组合路由功能 (AC: #3, #4)
 - [ ] 验证种业-果蔬组合路由功能 (AC: #3, #4)
   - [ ] 测试路由`/supply-chain/seed-fruit`正确加载种业-果蔬数据
   - [ ] 测试路由`/supply-chain/seed-fruit`正确加载种业-果蔬数据
   - [ ] 验证组合内产业切换功能(种业↔果蔬)
   - [ ] 验证组合内产业切换功能(种业↔果蔬)
@@ -39,7 +45,7 @@ In Progress
 - [ ] 测试数据集成完整性 (AC: #4)
 - [ ] 测试数据集成完整性 (AC: #4)
   - [ ] 验证地图定位点正确显示
   - [ ] 验证地图定位点正确显示
   - [ ] 验证关键指标数据正确展示
   - [ ] 验证关键指标数据正确展示
-  - [ ] 验证数据卡片内容准确
+  - [ ] 验证弹出框数据内容准确
   - [ ] 验证供应链网络连接线正确绘制
   - [ ] 验证供应链网络连接线正确绘制
 
 
 ## Dev Notes
 ## Dev Notes

+ 0 - 141
src/client/home/pages/SupplyChainDashboards/GrainOilDashboard.tsx

@@ -1,141 +0,0 @@
-import React, { useState } from 'react';
-import SupplyChainMap from './components/SupplyChainMap';
-import SupplyChainModel from './components/SupplyChainModel';
-import KeyMetrics from './components/KeyMetrics';
-import PopupInfoBox from './components/PopupInfoBox';
-
-// 导入新的组件库
-import Navigation from './components/layout/Navigation';
-import SupplyChainBackground from './components/layout/SupplyChainBackground';
-import BackgroundGrid from './components/layout/BackgroundGrid';
-import HeaderBar from './components/layout/HeaderBar';
-import { IndustryType } from './components/icons/IndustryIcon';
-
-// 导入供应链上下文
-import { SupplyChainProvider, useSupplyChain } from './context/SupplyChainContext';
-
-// 定义组件接口
-interface GrainOilDashboardProps {
-  // 可以添加props如果需要
-}
-
-// 定义弹出框状态
-interface PopupState {
-  isVisible: boolean;
-  position: { x: number; y: number };
-  data?: {
-    title: string;
-    subtitle: string;
-    metrics: {
-      label: string;
-      value: string;
-      unit: string;
-    }[];
-  };
-}
-
-// 内部组件,使用ThemeProvider
-const DashboardContent: React.FC = () => {
-  const [activeTab, setActiveTab] = useState<IndustryType>('粮食');
-  const [popupState, setPopupState] = useState<PopupState>({
-    isVisible: false,
-    position: { x: 650, y: 250 }
-  });
-  const { setIndustry } = useSupplyChain();
-
-  // 处理tab切换
-  const handleTabChange = (tab: IndustryType) => {
-    setActiveTab(tab);
-    setIndustry(tab); // 更新主题色
-  };
-
-  // 处理定位点点击
-  const handlePointClick = (point: any) => {
-    console.log('点击定位点:', point);
-
-    // 根据定位点类型设置不同的数据
-    let popupData;
-    if (point.type === 'base') {
-      popupData = {
-        title: "源头",
-        subtitle: "江汉大米核心示范基地荆门/荆州/黄冈/孝感",
-        metrics: [
-          { label: "辐射带动:", value: ">20", unit: "万亩" },
-          { label: "自建基地规模:", value: "6~15", unit: "万亩" }
-        ]
-      };
-    } else {
-      popupData = {
-        title: "源头",
-        subtitle: "江汉大米核心示范基地荆门/荆州/黄冈/孝感",
-        metrics: [
-          { label: "辐射带动:", value: ">20", unit: "万亩" },
-          { label: "自建基地规模:", value: "6~15", unit: "万亩" }
-        ]
-      };
-    }
-
-    setPopupState({
-      isVisible: true,
-      position: { x: 650, y: 250 },
-      data: popupData
-    });
-  };
-
-  // 关闭弹出框
-  const handleClosePopup = () => {
-    setPopupState(prev => ({ ...prev, isVisible: false }));
-  };
-
-  return (
-    <div className="h-[1080px] w-[1920px] bg-[#0a1a3a] relative overflow-hidden">
-      {/* 背景 */}
-      <div className="absolute contents left-0 top-0">
-        <SupplyChainBackground />
-        <BackgroundGrid />
-        <HeaderBar title="粮食•油脂" />
-      </div>
-
-      {/* 左侧导航 */}
-      <Navigation
-        activeTab={activeTab}
-        onTabChange={handleTabChange}
-        availableIndustries={["粮食", "油脂"]}
-      />
-
-      {/* 中间地图区域 */}
-      <SupplyChainMap
-        onPointClick={handlePointClick}
-      />
-
-      {/* 右侧供应链合作模式 这个不用主题色*/}
-      <SupplyChainModel />
-
-      {/* 左侧数据展示 */}
-      <KeyMetrics
-        title="优质稻米"
-        subtitle="产业链联合体"
-      />
-
-      {/* 弹出框 */}
-      {popupState.isVisible && (
-        <PopupInfoBox
-          data={popupState.data}
-          position={popupState.position}
-          onClose={handleClosePopup}
-        />
-      )}
-    </div>
-  );
-};
-
-// 主组件
-const GrainOilDashboard: React.FC<GrainOilDashboardProps> = () => {
-  return (
-    <SupplyChainProvider defaultIndustry="粮食">
-      <DashboardContent />
-    </SupplyChainProvider>
-  );
-};
-
-export default GrainOilDashboard;

+ 30 - 28
src/client/home/pages/SupplyChainDashboards/SupplyChainDashboard.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import { useParams } from 'react-router';
 import { useParams } from 'react-router';
 import { SupplyChainProvider, DashboardType, useSupplyChain } from './context/SupplyChainContext';
 import { SupplyChainProvider, DashboardType, useSupplyChain } from './context/SupplyChainContext';
+import { toast } from 'sonner';
 import SupplyChainMap from './components/SupplyChainMap';
 import SupplyChainMap from './components/SupplyChainMap';
 import SupplyChainModel from './components/SupplyChainModel';
 import SupplyChainModel from './components/SupplyChainModel';
 import KeyMetrics from './components/KeyMetrics';
 import KeyMetrics from './components/KeyMetrics';
@@ -16,13 +17,16 @@ interface PopupState {
   isVisible: boolean;
   isVisible: boolean;
   position: { x: number; y: number };
   position: { x: number; y: number };
   data?: {
   data?: {
+    id: string;
     title: string;
     title: string;
-    subtitle: string;
-    metrics: {
+    content: string;
+    position: { x: number; y: number };
+    industry: string;
+    metrics?: Array<{
       label: string;
       label: string;
       value: string;
       value: string;
       unit: string;
       unit: string;
-    }[];
+    }>;
   };
   };
 }
 }
 
 
@@ -32,39 +36,37 @@ const DashboardContent: React.FC = () => {
     isVisible: false,
     isVisible: false,
     position: { x: 650, y: 250 }
     position: { x: 650, y: 250 }
   });
   });
-  const { currentIndustry, setIndustry, currentDashboard } = useSupplyChain();
+  const { currentIndustry, setIndustry, currentDashboard, currentData } = useSupplyChain();
 
 
   // 处理定位点点击
   // 处理定位点点击
   const handlePointClick = (point: any) => {
   const handlePointClick = (point: any) => {
     console.log('点击定位点:', point);
     console.log('点击定位点:', point);
 
 
-    // 根据定位点类型设置不同的数据
+    // 从SupplyChainContext中获取当前产业的弹出框数据
+    const popupDataList = currentData?.popupData[currentIndustry] || [];
+
+    // 根据定位点类型选择对应的弹出框数据
     let popupData;
     let popupData;
-    if (point.type === 'base') {
-      popupData = {
-        title: "源头",
-        subtitle: "江汉大米核心示范基地荆门/荆州/黄冈/孝感",
-        metrics: [
-          { label: "辐射带动:", value: ">20", unit: "万亩" },
-          { label: "自建基地规模:", value: "6~15", unit: "万亩" }
-        ]
-      };
+    if (popupDataList.length > 0) {
+      // 如果有多个弹出框数据,可以根据point.id或point.type来选择
+      // 这里简单使用第一个弹出框数据
+      popupData = popupDataList[0];
+
+      setPopupState({
+        isVisible: true,
+        position: { x: 650, y: 250 },
+        data: popupData
+      });
     } else {
     } else {
-      popupData = {
-        title: "源头",
-        subtitle: "江汉大米核心示范基地荆门/荆州/黄冈/孝感",
-        metrics: [
-          { label: "辐射带动:", value: ">20", unit: "万亩" },
-          { label: "自建基地规模:", value: "6~15", unit: "万亩" }
-        ]
-      };
+      // 如果没有动态数据,显示toast提示
+      toast.warning(`当前${currentIndustry}产业暂无详细数据`, {
+        description: '请选择其他产业或联系管理员添加数据',
+        duration: 3000
+      });
+
+      // 不显示弹出框
+      setPopupState(prev => ({ ...prev, isVisible: false }));
     }
     }
-
-    setPopupState({
-      isVisible: true,
-      position: { x: 650, y: 250 },
-      data: popupData
-    });
   };
   };
 
 
   // 关闭弹出框
   // 关闭弹出框

+ 24 - 38
src/client/home/pages/SupplyChainDashboards/components/KeyMetrics.tsx

@@ -1,12 +1,13 @@
 import React from 'react';
 import React from 'react';
 import { useSupplyChain } from './../context/SupplyChainContext';
 import { useSupplyChain } from './../context/SupplyChainContext';
 
 
-// 定义指标数据类型
+// 定义指标数据类型 - 与SupplyChainContext保持一致
 interface MetricData {
 interface MetricData {
-  title: string;
-  value: string;
-  unit: string;
-  digits: string[];
+  id: string;
+  label: string;
+  value: string | number;
+  unit?: string;
+  industry: string;
 }
 }
 
 
 interface KeyMetricsProps {
 interface KeyMetricsProps {
@@ -17,17 +18,19 @@ interface KeyMetricsProps {
 
 
 // 数据卡片组件
 // 数据卡片组件
 const DataCard: React.FC<{
 const DataCard: React.FC<{
-  title: string;
-  unit: string;
-  digits: string[];
-}> = ({ title, unit, digits }) => {
+  metric: MetricData;
+}> = ({ metric }) => {
   const { themeColor } = useSupplyChain();
   const { themeColor } = useSupplyChain();
 
 
+  // 将数值转换为数字数组用于显示
+  const valueString = metric.value.toString();
+  const digits = valueString.split('');
+
   return (
   return (
     <div className="box-border content-stretch flex flex-col gap-[20px] items-start px-0 py-[20px] relative shrink-0 w-[278px]">
     <div className="box-border content-stretch flex flex-col gap-[20px] items-start px-0 py-[20px] relative shrink-0 w-[278px]">
       <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0 w-full">
       <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0 w-full">
         <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[40px] text-white whitespace-nowrap">
         <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[40px] text-white whitespace-nowrap">
-          <p className="leading-[50px]">{title}</p>
+          <p className="leading-[50px]">{metric.label}</p>
         </div>
         </div>
       </div>
       </div>
       <div className="content-stretch flex gap-[10px] h-[80px] items-end relative shrink-0 w-full">
       <div className="content-stretch flex gap-[10px] h-[80px] items-end relative shrink-0 w-full">
@@ -43,7 +46,7 @@ const DataCard: React.FC<{
         </div>
         </div>
         <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
         <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
           <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white whitespace-nowrap">
           <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white whitespace-nowrap">
-            <p className="leading-[32px]">{unit}</p>
+            <p className="leading-[32px]">{metric.unit || ''}</p>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
@@ -56,29 +59,14 @@ const KeyMetrics: React.FC<KeyMetricsProps> = ({
   title = "优质稻米",
   title = "优质稻米",
   subtitle = "产业链联合体"
   subtitle = "产业链联合体"
 }) => {
 }) => {
-  // 默认指标数据
-  const defaultMetrics: MetricData[] = [
-    {
-      title: "加工能力达",
-      value: "200",
-      unit: "万吨/年",
-      digits: ['2', '0', '0']
-    },
-    {
-      title: "自建优质水稻基地",
-      value: "15",
-      unit: "万亩",
-      digits: ['1', '5']
-    },
-    {
-      title: "辐射带动面积",
-      value: "20",
-      unit: "万亩",
-      digits: ['>', '2', '0']
-    }
-  ];
+  // 使用SupplyChainContext获取动态数据
+  const { currentData, currentIndustry } = useSupplyChain();
+
+  // 从context获取当前产业的关键指标数据
+  const dynamicMetrics = currentData?.keyMetrics[currentIndustry] || [];
 
 
-  const displayMetrics = metrics.length > 0 ? metrics : defaultMetrics;
+  // 优先使用传入的metrics,否则使用动态数据
+  const displayMetrics = metrics.length > 0 ? metrics : dynamicMetrics;
 
 
   return (
   return (
     <div className="absolute content-stretch flex flex-col gap-[11px] items-start left-[199.88px] top-[calc(50%+4.02px)] translate-y-[-50%] w-[330px]">
     <div className="absolute content-stretch flex flex-col gap-[11px] items-start left-[199.88px] top-[calc(50%+4.02px)] translate-y-[-50%] w-[330px]">
@@ -95,12 +83,10 @@ const KeyMetrics: React.FC<KeyMetricsProps> = ({
       {/* 指标数据区域 */}
       {/* 指标数据区域 */}
       <div className="grid-cols-[max-content] grid-rows-[max-content] inline-grid justify-items-start leading-[0] relative shrink-0">
       <div className="grid-cols-[max-content] grid-rows-[max-content] inline-grid justify-items-start leading-[0] relative shrink-0">
         <div className="box-border col-[1] content-stretch flex flex-col h-[700px] items-center justify-between ml-0 mt-0 px-0 py-[20px] relative row-[1] w-[330px]">
         <div className="box-border col-[1] content-stretch flex flex-col h-[700px] items-center justify-between ml-0 mt-0 px-0 py-[20px] relative row-[1] w-[330px]">
-          {displayMetrics.map((metric, index) => (
+          {displayMetrics.map((metric) => (
             <DataCard
             <DataCard
-              key={index}
-              title={metric.title}
-              unit={metric.unit}
-              digits={metric.digits}
+              key={metric.id}
+              metric={metric}
             />
             />
           ))}
           ))}
         </div>
         </div>

+ 41 - 53
src/client/home/pages/SupplyChainDashboards/components/PopupInfoBox.tsx

@@ -3,15 +3,18 @@ import { IndustryType } from './icons/IndustryIcon';
 import PopupInfoBoxIcon from './icons/PopupInfoBox';
 import PopupInfoBoxIcon from './icons/PopupInfoBox';
 import { useSupplyChain } from './../context/SupplyChainContext';
 import { useSupplyChain } from './../context/SupplyChainContext';
 
 
-// 定义弹出框数据类型
+// 定义弹出框数据类型 - 与SupplyChainContext保持一致
 interface PopupData {
 interface PopupData {
+  id: string;
   title: string;
   title: string;
-  subtitle: string;
-  metrics: {
+  content: string;
+  position: { x: number; y: number };
+  industry: string;
+  metrics?: Array<{
     label: string;
     label: string;
     value: string;
     value: string;
     unit: string;
     unit: string;
-  }[];
+  }>;
 }
 }
 
 
 interface PopupInfoBoxProps {
 interface PopupInfoBoxProps {
@@ -26,18 +29,18 @@ const PopupInfoBox: React.FC<PopupInfoBoxProps> = ({
   position = { x: 717.28, y: 273.13 },
   position = { x: 717.28, y: 273.13 },
   onClose
   onClose
 }) => {
 }) => {
-  const { themeColor } = useSupplyChain();
-  // 默认数据
-  const defaultData: PopupData = {
-    title: "源头",
-    subtitle: "江汉大米核心示范基地荆门/荆州/黄冈/孝感",
-    metrics: [
-      { label: "辐射带动:", value: ">20", unit: "万亩" },
-      { label: "自建基地规模:", value: "6~15", unit: "万亩" }
-    ]
-  };
+  const { themeColor, currentData, currentIndustry } = useSupplyChain();
 
 
-  const displayData = data || defaultData;
+  // 从context获取当前产业的弹出框数据
+  const dynamicPopupData = currentData?.popupData[currentIndustry]?.[0];
+
+  // 优先使用传入的data,否则使用动态数据
+  const displayData = data || dynamicPopupData;
+
+  // 如果没有数据,不渲染弹出框
+  if (!displayData) {
+    return null;
+  }
 
 
   return (
   return (
     <div
     <div
@@ -65,55 +68,40 @@ const PopupInfoBox: React.FC<PopupInfoBoxProps> = ({
           <div className="content-stretch flex flex-col gap-[12px] items-start relative shrink-0 w-full">
           <div className="content-stretch flex flex-col gap-[12px] items-start relative shrink-0 w-full">
             <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
             <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
               <div className={`flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[36px] whitespace-nowrap text-[${themeColor}]`}>
               <div className={`flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[36px] whitespace-nowrap text-[${themeColor}]`}>
-                <p className="leading-[32px]">{displayData.title}</p>
+                <p className="leading-[32px]">{displayData?.title || ''}</p>
               </div>
               </div>
             </div>
             </div>
             <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0 w-full">
             <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0 w-full">
               <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white w-full">
               <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white w-full">
-                <p className="leading-[32px] bg-gradient-to-b from-white to-sky-400 bg-clip-text text-transparent break-words w-full">{displayData.subtitle}</p>
+                <p className="leading-[32px] bg-gradient-to-b from-white to-sky-400 bg-clip-text text-transparent break-words w-full">{displayData?.content || ''}</p>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
 
 
           {/* 数据指标区域 */}
           {/* 数据指标区域 */}
-          <div className="content-stretch flex flex-col gap-[12px] items-start relative shrink-0 w-full">
-            {/* 第一行数据 */}
-            <div className="content-stretch flex gap-[20px] items-start relative shrink-0">
-              <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
-                <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white whitespace-nowrap">
-                  <p className="leading-[32px]">{displayData.metrics[0]?.label}</p>
-                </div>
-              </div>
-              <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
-                <div className={`flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[36px] whitespace-nowrap text-[${themeColor}]`}>
-                  <p className="leading-[32px]">{displayData.metrics[0]?.value}</p>
-                </div>
-              </div>
-              <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
-                <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white whitespace-nowrap">
-                  <p className="leading-[32px]">{displayData.metrics[0]?.unit}</p>
-                </div>
-              </div>
-            </div>
-            {/* 第二行数据 */}
-            <div className="content-stretch flex gap-[20px] items-start relative shrink-0">
-              <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
-                <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white whitespace-nowrap">
-                  <p className="leading-[32px]">{displayData.metrics[1]?.label}</p>
-                </div>
-              </div>
-              <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
-                <div className={`flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[36px] whitespace-nowrap text-[${themeColor}]`}>
-                  <p className="leading-[32px]">{displayData.metrics[1]?.value}</p>
+          {displayData?.metrics && displayData.metrics.length > 0 && (
+            <div className="content-stretch flex flex-col gap-[12px] items-start relative shrink-0 w-full">
+              {displayData.metrics.map((metric, index) => (
+                <div key={index} className="content-stretch flex gap-[20px] items-start relative shrink-0">
+                  <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
+                    <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white whitespace-nowrap">
+                      <p className="leading-[32px]">{metric.label}</p>
+                    </div>
+                  </div>
+                  <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
+                    <div className={`flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[36px] whitespace-nowrap text-[${themeColor}]`}>
+                      <p className="leading-[32px]">{metric.value}</p>
+                    </div>
+                  </div>
+                  <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
+                    <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white whitespace-nowrap">
+                      <p className="leading-[32px]">{metric.unit}</p>
+                    </div>
+                  </div>
                 </div>
                 </div>
-              </div>
-              <div className="box-border content-stretch flex gap-[10px] items-center px-0 py-[4px] relative shrink-0">
-                <div className="flex flex-col font-bold justify-center leading-[0] not-italic relative shrink-0 text-[24px] text-white whitespace-nowrap">
-                  <p className="leading-[32px]">{displayData.metrics[1]?.unit}</p>
-                </div>
-              </div>
+              ))}
             </div>
             </div>
-          </div>
+          )}
         </div>
         </div>
       </div>
       </div>
 
 

+ 23 - 25
src/client/home/pages/SupplyChainDashboards/components/SupplyChainMap.tsx

@@ -1,13 +1,17 @@
 import React from 'react';
 import React from 'react';
 import BasePointIcon from './icons/BasePointIcon';
 import BasePointIcon from './icons/BasePointIcon';
 import SupplyChainIcons from './icons/SupplyChainIcons';
 import SupplyChainIcons from './icons/SupplyChainIcons';
+import { useSupplyChain } from '../context/SupplyChainContext';
 
 
-// 定义定位点类型
+// 定义定位点类型 - 与SupplyChainContext保持一致
 interface LocationPoint {
 interface LocationPoint {
   id: string;
   id: string;
-  type: 'base' | 'industryChain';
-  position: { x: number; y: number };
+  type: 'base' | 'chain';
+  x: number;
+  y: number;
+  industry: string;
   name?: string;
   name?: string;
+  data?: Record<string, any>;
 }
 }
 
 
 // 定义连接线类型
 // 定义连接线类型
@@ -32,8 +36,8 @@ const BasePoint: React.FC<{
     <div
     <div
       className="absolute cursor-pointer transition-transform hover:scale-110"
       className="absolute cursor-pointer transition-transform hover:scale-110"
       style={{
       style={{
-        left: `${point.position.x}px`,
-        top: `${point.position.y}px`,
+        left: `${point.x}px`,
+        top: `${point.y}px`,
         width: '40px',
         width: '40px',
         height: '48px'
         height: '48px'
       }}
       }}
@@ -60,8 +64,8 @@ const IndustryChainPoint: React.FC<{
     <div
     <div
       className="absolute cursor-pointer transition-transform hover:scale-110"
       className="absolute cursor-pointer transition-transform hover:scale-110"
       style={{
       style={{
-        left: `${point.position.x}px`,
-        top: `${point.position.y}px`,
+        left: `${point.x}px`,
+        top: `${point.y}px`,
         width: '40px',
         width: '40px',
         height: '48px'
         height: '48px'
       }}
       }}
@@ -85,8 +89,8 @@ const ConnectionLine: React.FC<{
   toPoint: LocationPoint;
   toPoint: LocationPoint;
 }> = ({ fromPoint, toPoint }) => {
 }> = ({ fromPoint, toPoint }) => {
   // 计算连接线的位置和角度
   // 计算连接线的位置和角度
-  const dx = toPoint.position.x - fromPoint.position.x;
-  const dy = toPoint.position.y - fromPoint.position.y;
+  const dx = toPoint.x - fromPoint.x;
+  const dy = toPoint.y - fromPoint.y;
   const length = Math.sqrt(dx * dx + dy * dy);
   const length = Math.sqrt(dx * dx + dy * dy);
   const angle = Math.atan2(dy, dx) * 180 / Math.PI;
   const angle = Math.atan2(dy, dx) * 180 / Math.PI;
 
 
@@ -94,8 +98,8 @@ const ConnectionLine: React.FC<{
     <div
     <div
       className="absolute pointer-events-none"
       className="absolute pointer-events-none"
       style={{
       style={{
-        left: `${fromPoint.position.x + 20}px`,
-        top: `${fromPoint.position.y + 24}px`,
+        left: `${fromPoint.x + 20}px`,
+        top: `${fromPoint.y + 24}px`,
         width: `${length}px`,
         width: `${length}px`,
         height: '2px',
         height: '2px',
         transform: `rotate(${angle}deg)`,
         transform: `rotate(${angle}deg)`,
@@ -146,21 +150,14 @@ const SupplyChainMap: React.FC<SupplyChainMapProps> = ({
   points = [],
   points = [],
   onPointClick
   onPointClick
 }) => {
 }) => {
-  // 默认定位点数据
-  const defaultPoints: LocationPoint[] = [
-    { id: 'base1', type: 'base', position: { x: 1142.89, y: 717.84 }, name: '基地1' },
-    { id: 'base2', type: 'base', position: { x: 1664.25, y: 530.82 }, name: '基地2' },
-    { id: 'base3', type: 'base', position: { x: 1435, y: 527.14 }, name: '基地3' },
-    { id: 'base4', type: 'base', position: { x: 1203.07, y: 514.31 }, name: '基地4' },
-    { id: 'chain1', type: 'industryChain', position: { x: 1273.12, y: 551.14 }, name: '产业链1' },
-    { id: 'chain2', type: 'industryChain', position: { x: 1403, y: 597.5 }, name: '产业链2' },
-    { id: 'chain3', type: 'industryChain', position: { x: 1694.25, y: 645.5 }, name: '产业链3' },
-    { id: 'chain4', type: 'industryChain', position: { x: 1237.87, y: 761.84 }, name: '产业链4' }
-  ];
+  // 使用SupplyChainContext获取动态数据
+  const { currentData, currentIndustry } = useSupplyChain();
 
 
+  // 从context获取当前产业的定位点数据
+  const dynamicPoints = currentData?.mapPoints[currentIndustry] || [];
 
 
-
-  const displayPoints = points.length > 0 ? points : defaultPoints;
+  // 优先使用传入的points,否则使用动态数据
+  const displayPoints = points.length > 0 ? points : dynamicPoints;
 
 
   return (
   return (
     <div className="absolute contents left-[529.88px] top-[calc(50%+64.03px)] translate-y-[-50%]">
     <div className="absolute contents left-[529.88px] top-[calc(50%+64.03px)] translate-y-[-50%]">
@@ -197,7 +194,7 @@ const SupplyChainMap: React.FC<SupplyChainMapProps> = ({
                 onClick={onPointClick}
                 onClick={onPointClick}
               />
               />
             );
             );
-          } else {
+          } else if (point.type === 'chain') {
             return (
             return (
               <IndustryChainPoint
               <IndustryChainPoint
                 key={point.id}
                 key={point.id}
@@ -206,6 +203,7 @@ const SupplyChainMap: React.FC<SupplyChainMapProps> = ({
               />
               />
             );
             );
           }
           }
+          return null;
         })}
         })}
 
 
         {/* 图例 */}
         {/* 图例 */}

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

@@ -7,7 +7,6 @@ import { MainLayout } from './layouts/MainLayout';
 import LoginPage from './pages/LoginPage';
 import LoginPage from './pages/LoginPage';
 import RegisterPage from './pages/RegisterPage';
 import RegisterPage from './pages/RegisterPage';
 import MemberPage from './pages/MemberPage';
 import MemberPage from './pages/MemberPage';
-import GrainOilDashboard from './pages/SupplyChainDashboards/GrainOilDashboard';
 import SupplyChainDashboard from './pages/SupplyChainDashboards/SupplyChainDashboard';
 import SupplyChainDashboard from './pages/SupplyChainDashboards/SupplyChainDashboard';
 import FinancialDashboard from './pages/FinancialDashboard/FinancialDashboard';
 import FinancialDashboard from './pages/FinancialDashboard/FinancialDashboard';
 
 

+ 0 - 121
tests/unit/client/home/pages/SupplyChainDashboards/GrainOilDashboard.test.tsx

@@ -1,121 +0,0 @@
-import React from 'react';
-import { render, screen, fireEvent } from '@testing-library/react';
-import { describe, it, expect, vi } from 'vitest';
-import GrainOilDashboard from '@/client/home/pages/SupplyChainDashboards/GrainOilDashboard';
-
-// Mock child components
-vi.mock('@/client/home/pages/SupplyChainDashboards/components/SupplyChainMap', () => ({
-  default: ({ onPointClick }: { onPointClick: (point: any) => void }) => (
-    <div data-testid="supply-chain-map">
-      <button onClick={() => onPointClick({ id: 'test-point', type: 'base', position: { x: 100, y: 100 } })}>
-        Test Point
-      </button>
-    </div>
-  )
-}));
-
-vi.mock('@/client/home/pages/SupplyChainDashboards/components/SupplyChainModel', () => ({
-  default: () => <div data-testid="supply-chain-model">Supply Chain Model</div>
-}));
-
-vi.mock('@/client/home/pages/SupplyChainDashboards/components/KeyMetrics', () => ({
-  default: () => <div data-testid="key-metrics">Key Metrics</div>
-}));
-
-describe('GrainOilDashboard', () => {
-  it('should render the dashboard with all components', () => {
-    render(<GrainOilDashboard />);
-
-    // Check if main container is rendered
-    const container = screen.getByRole('generic');
-    expect(container).toHaveClass('h-[1080px]');
-    expect(container).toHaveClass('w-[1920px]');
-
-    // Check if all child components are rendered
-    expect(screen.getByTestId('supply-chain-map')).toBeInTheDocument();
-    expect(screen.getByTestId('supply-chain-model')).toBeInTheDocument();
-    expect(screen.getByTestId('key-metrics')).toBeInTheDocument();
-  });
-
-  it('should render navigation with default active tab', () => {
-    render(<GrainOilDashboard />);
-
-    // Check if navigation is rendered
-    const navigation = screen.getByText('粮食');
-    expect(navigation).toBeInTheDocument();
-
-    // Check if grain tab is active by default
-    const grainText = screen.getByText('粮食').closest('div');
-    expect(grainText).toHaveClass('text-white');
-
-    // Check if oil tab is inactive
-    const oilText = screen.getByText('油脂').closest('div');
-    expect(oilText).toHaveClass('text-[rgba(255,255,255,0.5)]');
-  });
-
-  it('should handle navigation tab switching', () => {
-    render(<GrainOilDashboard />);
-
-    // Find and click the oil tab
-    const oilTab = screen.getByText('油脂').closest('div[class*="cursor-pointer"]');
-    expect(oilTab).toBeInTheDocument();
-
-    if (oilTab) {
-      fireEvent.click(oilTab);
-
-      // After clicking, the oil tab should become active
-      const oilText = screen.getByText('油脂').closest('div');
-      expect(oilText).toHaveClass('text-white');
-
-      // And grain tab should become inactive
-      const grainText = screen.getByText('粮食').closest('div');
-      expect(grainText).toHaveClass('text-[rgba(255,255,255,0.5)]');
-    }
-  });
-
-  it('should handle map point clicks', () => {
-    const consoleSpy = vi.spyOn(console, 'log');
-
-    render(<GrainOilDashboard />);
-
-    // Click on a test point in the map
-    const testPoint = screen.getByText('Test Point');
-    fireEvent.click(testPoint);
-
-    // Check if the click handler was called
-    expect(consoleSpy).toHaveBeenCalledWith('点击定位点:', {
-      id: 'test-point',
-      type: 'base',
-      position: { x: 100, y: 100 }
-    });
-
-    consoleSpy.mockRestore();
-  });
-
-  it('should render with correct background and styling', () => {
-    render(<GrainOilDashboard />);
-
-    const container = screen.getByRole('generic');
-
-    // Check background color
-    expect(container).toHaveClass('bg-[#0a1a3a]');
-
-    // Check dimensions
-    expect(container).toHaveClass('h-[1080px]');
-    expect(container).toHaveClass('w-[1920px]');
-
-    // Check positioning
-    expect(container).toHaveClass('relative');
-    expect(container).toHaveClass('overflow-hidden');
-  });
-
-  it('should render title and header elements', () => {
-    render(<GrainOilDashboard />);
-
-    // Check if title is rendered
-    const title = screen.getByText('粮食•油脂');
-    expect(title).toBeInTheDocument();
-    expect(title).toHaveClass('text-white');
-    expect(title).toHaveClass('text-[34px]');
-  });
-});

+ 6 - 5
tests/unit/client/home/pages/SupplyChainDashboards/components/KeyMetrics.test.tsx

@@ -1,4 +1,3 @@
-import React from 'react';
 import { render, screen } from '@testing-library/react';
 import { render, screen } from '@testing-library/react';
 import { describe, it, expect } from 'vitest';
 import { describe, it, expect } from 'vitest';
 import KeyMetrics from '@/client/home/pages/SupplyChainDashboards/components/KeyMetrics';
 import KeyMetrics from '@/client/home/pages/SupplyChainDashboards/components/KeyMetrics';
@@ -40,16 +39,18 @@ describe('KeyMetrics', () => {
   it('should render with custom metrics', () => {
   it('should render with custom metrics', () => {
     const customMetrics = [
     const customMetrics = [
       {
       {
-        title: '自定义指标1',
+        id: 'custom1',
+        label: '自定义指标1',
         value: '123',
         value: '123',
         unit: '个',
         unit: '个',
-        digits: ['1', '2', '3']
+        industry: '粮食'
       },
       },
       {
       {
-        title: '自定义指标2',
+        id: 'custom2',
+        label: '自定义指标2',
         value: '45',
         value: '45',
         unit: '件',
         unit: '件',
-        digits: ['4', '5']
+        industry: '粮食'
       }
       }
     ];
     ];
 
 

+ 3 - 4
tests/unit/client/home/pages/SupplyChainDashboards/components/SupplyChainMap.test.tsx

@@ -1,4 +1,3 @@
-import React from 'react';
 import { render, screen, fireEvent } from '@testing-library/react';
 import { render, screen, fireEvent } from '@testing-library/react';
 import { describe, it, expect, vi } from 'vitest';
 import { describe, it, expect, vi } from 'vitest';
 import SupplyChainMap from '@/client/home/pages/SupplyChainDashboards/components/SupplyChainMap';
 import SupplyChainMap from '@/client/home/pages/SupplyChainDashboards/components/SupplyChainMap';
@@ -18,8 +17,8 @@ describe('SupplyChainMap', () => {
 
 
   it('should render custom points when provided', () => {
   it('should render custom points when provided', () => {
     const customPoints = [
     const customPoints = [
-      { id: 'custom1', type: 'base' as const, position: { x: 100, y: 100 }, name: 'Custom Base' },
-      { id: 'custom2', type: 'industryChain' as const, position: { x: 200, y: 200 }, name: 'Custom Chain' }
+      { id: 'custom1', type: 'base' as const, x: 100, y: 100, industry: '粮食', name: 'Custom Base' },
+      { id: 'custom2', type: 'chain' as const, x: 200, y: 200, industry: '粮食', name: 'Custom Chain' }
     ];
     ];
 
 
     render(<SupplyChainMap points={customPoints} />);
     render(<SupplyChainMap points={customPoints} />);
@@ -32,7 +31,7 @@ describe('SupplyChainMap', () => {
   it('should handle point clicks', () => {
   it('should handle point clicks', () => {
     const mockOnPointClick = vi.fn();
     const mockOnPointClick = vi.fn();
     const customPoints = [
     const customPoints = [
-      { id: 'test-point', type: 'base' as const, position: { x: 100, y: 100 }, name: 'Test Point' }
+      { id: 'test-point', type: 'base' as const, x: 100, y: 100, industry: '粮食', name: 'Test Point' }
     ];
     ];
 
 
     render(<SupplyChainMap points={customPoints} onPointClick={mockOnPointClick} />);
     render(<SupplyChainMap points={customPoints} onPointClick={mockOnPointClick} />);