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

✨ feat(supply-chain): 实现弹出框交互功能

- 点击地图定位点显示详细信息弹出框,包含基地和产业链数据展示
- 创建PopupInfoBox弹出框组件,集成到GrainOilDashboard组件
- 下载并使用弹出框相关图片资源(PopupBorder.svg、ArrowDecoration.svg、CircleDecoration.svg)
- 实现定位点点击事件处理和弹出框状态管理

📝 docs(story): 更新供应链可视化功能文档

- 添加弹出框交互功能到已完成功能列表
- 更新任务状态,标记弹出框相关子任务为已完成
- 补充弹出框测试验收标准
- 更新故事版本记录至1.3版
yourname 2 месяцев назад
Родитель
Сommit
df78489c2f

+ 13 - 1
docs/stories/005.001.story.md

@@ -19,6 +19,7 @@ docs/prd/epic-005-supply-chain-visualization.md
 3. ✅ 添加供应链网络展示 - 显示"1+1+N"合作模式,包含省粮油集团、区域公司、新型农业经营主体
 3. ✅ 添加供应链网络展示 - 显示"1+1+N"合作模式,包含省粮油集团、区域公司、新型农业经营主体
 4. ✅ 集成关键指标数据展示 - 显示加工能力(200万吨/年)、自建优质水稻基地(15万亩)、辐射带动面积(>20万亩)
 4. ✅ 集成关键指标数据展示 - 显示加工能力(200万吨/年)、自建优质水稻基地(15万亩)、辐射带动面积(>20万亩)
 5. ✅ 实现导航和交互功能 - 左侧导航栏支持产业切换,交互响应流畅,粮食产业图标和文字显示正确
 5. ✅ 实现导航和交互功能 - 左侧导航栏支持产业切换,交互响应流畅,粮食产业图标和文字显示正确
+6. ✅ 实现弹出框交互功能 - 点击地图定位点显示详细信息弹出框,包含基地和产业链数据展示
 
 
 ## Tasks / Subtasks
 ## Tasks / Subtasks
 - [x] 创建GrainOilDashboard组件 (AC: #1)
 - [x] 创建GrainOilDashboard组件 (AC: #1)
@@ -49,7 +50,14 @@ docs/prd/epic-005-supply-chain-visualization.md
   - [x] 添加导航交互状态管理
   - [x] 添加导航交互状态管理
   - [x] 优化大屏切换响应性能
   - [x] 优化大屏切换响应性能
   - [x] 实现粮食产业激活状态样式
   - [x] 实现粮食产业激活状态样式
-- [ ] 添加单元测试 (AC: #1-#5)
+- [x] 实现弹出框交互功能 (AC: #6)
+  - [x] 分析弹出框组件设计规范
+  - [x] 创建PopupInfoBox弹出框组件
+  - [x] 下载弹出框相关图片资源(PopupBorder.svg、ArrowDecoration.svg、CircleDecoration.svg)
+  - [x] 集成弹出框到GrainOilDashboard组件
+  - [x] 实现定位点点击事件处理
+  - [x] 添加弹出框状态管理
+- [ ] 添加单元测试 (AC: #1-#6)
   - [ ] 编写组件渲染测试
   - [ ] 编写组件渲染测试
   - [ ] 编写交互功能测试
   - [ ] 编写交互功能测试
   - [ ] 验证数据展示正确性
   - [ ] 验证数据展示正确性
@@ -138,6 +146,7 @@ docs/prd/epic-005-supply-chain-visualization.md
 - 供应链网络展示完整
 - 供应链网络展示完整
 - 关键数据指标准确显示(粮食和油脂数据)
 - 关键数据指标准确显示(粮食和油脂数据)
 - 导航交互功能正常(粮食/油脂切换)
 - 导航交互功能正常(粮食/油脂切换)
+- 弹出框交互功能正常(点击定位点显示详细信息)
 - 性能指标满足要求
 - 性能指标满足要求
 - 油脂产业特定元素正确显示(菜籽油产业链联合体、油脂图标等)
 - 油脂产业特定元素正确显示(菜籽油产业链联合体、油脂图标等)
 
 
@@ -146,6 +155,8 @@ docs/prd/epic-005-supply-chain-visualization.md
 - 测试地图定位点交互(粮食和油脂)
 - 测试地图定位点交互(粮食和油脂)
 - 验证数据卡片显示正确性(粮食和油脂数据)
 - 验证数据卡片显示正确性(粮食和油脂数据)
 - 测试导航切换功能(粮食/油脂切换)
 - 测试导航切换功能(粮食/油脂切换)
+- 测试弹出框显示和关闭功能
+- 验证弹出框数据展示正确性(基地和产业链信息)
 - 验证响应式布局适配
 - 验证响应式布局适配
 - 测试油脂产业特定功能(菜籽油产业链联合体显示)
 - 测试油脂产业特定功能(菜籽油产业链联合体显示)
 
 
@@ -155,6 +166,7 @@ docs/prd/epic-005-supply-chain-visualization.md
 | 2025-11-06 | 1.0 | 初始故事创建 | Bob (SM) |
 | 2025-11-06 | 1.0 | 初始故事创建 | Bob (SM) |
 | 2025-11-06 | 1.1 | 基于油脂设计图更新故事,补充油脂大屏详细设计规范 | Bob (SM) |
 | 2025-11-06 | 1.1 | 基于油脂设计图更新故事,补充油脂大屏详细设计规范 | Bob (SM) |
 | 2025-11-15 | 1.2 | 粮食大屏实现完成,更新验收标准和任务状态 | Claude |
 | 2025-11-15 | 1.2 | 粮食大屏实现完成,更新验收标准和任务状态 | Claude |
+| 2025-11-15 | 1.3 | 添加弹出框交互功能实现,补充相关任务和验收标准 | Claude |
 
 
 ## Dev Agent Record
 ## Dev Agent Record
 
 

Разница между файлами не показана из-за своего большого размера
+ 2 - 0
public/supply-chain/ArrowDecoration.svg


+ 6 - 0
public/supply-chain/CircleDecoration.svg

@@ -0,0 +1,6 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#229;&#156;&#134;&#229;&#156;&#136;&#229;&#156;&#136;">
+<path id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;" fill-rule="evenodd" clip-rule="evenodd" d="M8.50006 2.19336C11.983 2.19336 14.8065 5.01686 14.8065 8.49981C14.8065 11.9828 11.983 14.8063 8.50006 14.8063C5.0171 14.8063 2.1936 11.9828 2.1936 8.49981C2.1936 5.01686 5.0171 2.19336 8.50006 2.19336V2.19336Z" fill="var(--fill-0, #4AE9DB)"/>
+<path id="&#230;&#164;&#173;&#229;&#156;&#134;&#229;&#189;&#162;&#229;&#164;&#135;&#228;&#187;&#189; 67" d="M8.5 0.5C12.9183 0.5 16.5 4.08173 16.5 8.5C16.5 12.9183 12.9183 16.5 8.5 16.5C4.08173 16.5 0.5 12.9183 0.5 8.5C0.5 4.08173 4.08173 0.5 8.5 0.5Z" stroke="var(--stroke-0, #4AE9DB)"/>
+</g>
+</svg>

+ 17 - 0
public/supply-chain/PopupBorder.svg

@@ -0,0 +1,17 @@
+<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 478 322" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="&#230;&#161;&#134;">
+<g id="&#231;&#188;&#150;&#231;&#187;&#132; 19">
+<path id="&#231;&#159;&#169;&#229;&#189;&#162;" d="M294.937 1.59985L318.377 21.4583L318.544 21.5999H476.4V320.4H343.394L322.125 308.811L321.991 308.738H139.165L139.032 308.81L117.59 320.4H1.69568V31.0637L27.5551 1.59985H294.937Z" fill="var(--fill-0, #231F20)" fill-opacity="0.1" stroke="var(--stroke-0, #00FFFF)" stroke-width="1.2"/>
+<path id="&#231;&#159;&#169;&#229;&#189;&#162;_2" fill-rule="evenodd" clip-rule="evenodd" d="M21.276 1L1.08786 23.8571L1 1H21.276V1Z" fill="var(--fill-0, #00FFFF)"/>
+<path id="&#231;&#155;&#180;&#231;&#186;&#191; 20&#229;&#164;&#135;&#228;&#187;&#189;" d="M1.72388 313.171V320.286H7.8038" stroke="var(--stroke-0, #00FFFF)" stroke-width="1.8" stroke-linecap="square"/>
+<path id="&#231;&#155;&#180;&#231;&#186;&#191; 20&#229;&#164;&#135;&#228;&#187;&#189; 2" d="M476.515 313.171V320.286H470.436" stroke="var(--stroke-0, #00FFFF)" stroke-width="1.8" stroke-linecap="square"/>
+</g>
+<path id="&#231;&#155;&#180;&#231;&#186;&#191; 9" d="M268.021 1.71443L294.92 2.07157L318.165 21.7144H338.429" stroke="url(#paint0_linear_0_2498)" stroke-width="1.8" stroke-linecap="square"/>
+</g>
+<defs>
+<linearGradient id="paint0_linear_0_2498" x1="268.021" y1="1.71443" x2="268.021" y2="21.7144" gradientUnits="userSpaceOnUse">
+<stop stop-color="white"/>
+<stop offset="1" stop-color="#40DEFF"/>
+</linearGradient>
+</defs>
+</svg>

+ 66 - 4
src/client/home/pages/SupplyChainDashboards/GrainOilDashboard.tsx

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
 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';
+import PopupInfoBox from './components/PopupInfoBox';
 
 
 // 导入新的组件库
 // 导入新的组件库
 import Navigation from './components/layout/Navigation';
 import Navigation from './components/layout/Navigation';
@@ -15,10 +16,65 @@ interface GrainOilDashboardProps {
   // 可以添加props如果需要
   // 可以添加props如果需要
 }
 }
 
 
+// 定义弹出框状态
+interface PopupState {
+  isVisible: boolean;
+  position: { x: number; y: number };
+  data?: {
+    title: string;
+    metrics: {
+      label: string;
+      value: string;
+      unit?: string;
+    }[];
+  };
+}
 
 
 // 主组件
 // 主组件
 const GrainOilDashboard: React.FC<GrainOilDashboardProps> = () => {
 const GrainOilDashboard: React.FC<GrainOilDashboardProps> = () => {
   const [activeTab, setActiveTab] = useState<IndustryType>('粮食');
   const [activeTab, setActiveTab] = useState<IndustryType>('粮食');
+  const [popupState, setPopupState] = useState<PopupState>({
+    isVisible: false,
+    position: { x: 717.28, y: 273.13 }
+  });
+
+  // 处理定位点点击
+  const handlePointClick = (point: any) => {
+    console.log('点击定位点:', point);
+
+    // 根据定位点类型设置不同的数据
+    let popupData;
+    if (point.type === 'base') {
+      popupData = {
+        title: `${point.name}基地信息`,
+        metrics: [
+          { label: "种植面积", value: "15", unit: "万亩" },
+          { label: "年产量", value: "200", unit: "万吨" },
+          { label: "带动农户", value: "5000", unit: "户" }
+        ]
+      };
+    } else {
+      popupData = {
+        title: `${point.name}产业链信息`,
+        metrics: [
+          { label: "加工能力", value: "30", unit: "万吨/年" },
+          { label: "储存仓容", value: "300", unit: "万吨" },
+          { label: "年产值", value: "50", unit: "亿元" }
+        ]
+      };
+    }
+
+    setPopupState({
+      isVisible: true,
+      position: { x: 717.28, y: 273.13 },
+      data: popupData
+    });
+  };
+
+  // 关闭弹出框
+  const handleClosePopup = () => {
+    setPopupState(prev => ({ ...prev, isVisible: false }));
+  };
 
 
 
 
   return (
   return (
@@ -39,10 +95,7 @@ const GrainOilDashboard: React.FC<GrainOilDashboardProps> = () => {
 
 
       {/* 中间地图区域 */}
       {/* 中间地图区域 */}
       <SupplyChainMap
       <SupplyChainMap
-        onPointClick={(point) => {
-          console.log('点击定位点:', point);
-          // 这里可以添加点击定位点的处理逻辑
-        }}
+        onPointClick={handlePointClick}
       />
       />
       
       
       {/* 右侧供应链合作模式 */}
       {/* 右侧供应链合作模式 */}
@@ -54,6 +107,15 @@ const GrainOilDashboard: React.FC<GrainOilDashboardProps> = () => {
         title="优质稻米"
         title="优质稻米"
         subtitle="产业链联合体"
         subtitle="产业链联合体"
       />
       />
+
+      {/* 弹出框 */}
+      {popupState.isVisible && (
+        <PopupInfoBox
+          data={popupState.data}
+          position={popupState.position}
+          onClose={handleClosePopup}
+        />
+      )}
     </div>
     </div>
   );
   );
 };
 };

+ 139 - 0
src/client/home/pages/SupplyChainDashboards/components/PopupInfoBox.tsx

@@ -0,0 +1,139 @@
+import React from 'react';
+
+// 定义弹出框数据类型
+interface PopupData {
+  title: string;
+  metrics: {
+    label: string;
+    value: string;
+    unit?: string;
+  }[];
+}
+
+interface PopupInfoBoxProps {
+  data?: PopupData;
+  position?: { x: number; y: number };
+  onClose?: () => void;
+}
+
+const PopupInfoBox: React.FC<PopupInfoBoxProps> = ({
+  data,
+  position = { x: 717.28, y: 273.13 },
+  onClose
+}) => {
+  // 默认数据
+  const defaultData: PopupData = {
+    title: "粮食源头",
+    metrics: [
+      { label: "种植面积", value: "15", unit: "万亩" },
+      { label: "年产量", value: "200", unit: "万吨" },
+      { label: "带动农户", value: "5000", unit: "户" }
+    ]
+  };
+
+  const displayData = data || defaultData;
+
+  return (
+    <div
+      className="absolute overflow-clip"
+      style={{
+        left: `${position.x}px`,
+        top: `${position.y}px`,
+        width: '580px',
+        height: '351px'
+      }}
+    >
+      {/* 箭头指示器 */}
+      <div className="absolute flex items-center justify-center left-0 top-0">
+        <div className="flex-none rotate-[180deg]">
+          <div className="content-stretch flex items-start overflow-clip relative">
+            {/* 箭头装饰 */}
+            <div className="flex h-[calc(1px*((var(--transform-inner-width)*1)+(var(--transform-inner-height)*0)))] items-center justify-center leading-[0] relative shrink-0 w-[calc(1px*((var(--transform-inner-height)*1)+(var(--transform-inner-width)*0)))]">
+              <div className="flex-none rotate-[90deg] scale-y-[-100%]">
+                <div className="grid-cols-[max-content] grid-rows-[max-content] inline-grid justify-items-start relative">
+                  <div className="col-[1] h-[80px] ml-[7.5px] mt-[17px] relative row-[1] w-[2px]">
+                    <div className="absolute bottom-[-1.25%] left-0 right-[-50%] top-[-1.25%]">
+                      <img
+                        alt="箭头装饰"
+                        className="block max-w-none size-full"
+                        src="/supply-chain/ArrowDecoration.svg"
+                      />
+                    </div>
+                  </div>
+                  <div className="col-[1] ml-0 mt-0 relative row-[1] size-[17px]">
+                    <img
+                      alt="圆圈装饰"
+                      className="block max-w-none size-full"
+                      src="/supply-chain/CircleDecoration.svg"
+                    />
+                  </div>
+                </div>
+              </div>
+            </div>
+
+            {/* 弹出框主体 */}
+            <div className="h-[320px] relative shrink-0 w-[476px]">
+              {/* 边框背景 */}
+              <div className="absolute inset-[-0.06%_-0.09%_-0.06%_-0.04%]">
+                <img
+                  alt="弹出框边框"
+                  className="block max-w-none size-full"
+                  src="/supply-chain/PopupBorder.svg"
+                />
+              </div>
+
+              {/* 内容区域 */}
+              <div className="absolute content-stretch flex flex-col gap-[12px] items-center left-[29.81px] top-[31.73px] w-[420px]">
+                {/* 标题区域 */}
+                <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] h-[62px] 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.title}</p>
+                    </div>
+                  </div>
+                  <div className="box-border content-stretch flex gap-[10px] h-[62px] 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 whitespace-nowrap">
+                      <p className="leading-[32px]">详细信息</p>
+                    </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">
+                    {displayData.metrics.slice(0, 3).map((metric, index) => (
+                      <div key={index} 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>
+                  <div className="content-stretch flex gap-[20px] items-start relative shrink-0">
+                    {displayData.metrics.slice(0, 3).map((metric, index) => (
+                      <div key={index} 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-[#C5FF92] whitespace-nowrap">
+                          <p className="leading-[32px]">{metric.value} {metric.unit}</p>
+                        </div>
+                      </div>
+                    ))}
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      {/* 关闭按钮 */}
+      <button
+        className="absolute right-4 top-4 text-white text-xl hover:text-[#C5FF92] transition-colors"
+        onClick={onClose}
+      >
+        ×
+      </button>
+    </div>
+  );
+};
+
+export default PopupInfoBox;

Некоторые файлы не были показаны из-за большого количества измененных файлов