Ver Fonte

✨ feat(supply-chain): 实现供应链地图弹出层及图片展示功能

- 实现点击定位点弹出完整SupplyChainModal
  - 添加弹出层状态管理与关闭功能
  - 支持ESC键和遮罩层点击关闭
  - 应用正确的主题色样式
- 优化SupplyChainModal图片展示布局
  - 支持单图居中显示与多图横向滚动两种模式
  - 根据图片数量动态选择展示模式
  - 实现自定义滚动条交互功能
    - 可点击拖动控制图片滚动
    - 滚动位置与滚动条同步
    - 滚动条样式与主题色协调
    - 添加滚动条悬停效果

✅ test(supply-chain): 更新任务清单状态

- 标记"实现点击popup时弹出完整SupplyChainModal"及其所有子任务为已完成
yourname há 2 meses atrás
pai
commit
5c8dface9c

+ 17 - 17
docs/stories/005.004.story.md

@@ -52,23 +52,23 @@ Completed
     - [x] 第一个款式以右下角为定位基准
     - [x] 第一个款式以右下角为定位基准
     - [x] 第二个款式以右上角为定位基准
     - [x] 第二个款式以右上角为定位基准
   - [x] 验证所有组合的款式切换正确性
   - [x] 验证所有组合的款式切换正确性
-- [ ] 实现点击popup时弹出完整SupplyChainModal
-  - [ ] 在SupplyChainMap组件中添加弹出层状态管理
-  - [ ] 修改定位点点击事件,支持显示完整弹出层
-  - [ ] 集成SupplyChainModal组件,传递当前定位点的数据
-  - [ ] 实现弹出层关闭功能
-  - [ ] 验证弹出层主题色正确应用
-  - [ ] 测试ESC键和遮罩层点击关闭功能
-  - [ ] 优化SupplyChainModal图片展示布局
-    - [ ] 支持单图展示模式(图片居中显示)
-    - [ ] 支持多图展示模式(图片横向滚动)
-    - [ ] 根据图片数量动态选择展示模式
-    - [ ] 实现横向滚动功能
-    - [ ] 实现滚动条交互功能
-      - [ ] 滚动条可点击拖动控制图片滚动
-      - [ ] 滚动条位置与图片滚动位置同步
-      - [ ] 滚动条样式与主题色协调
-      - [ ] 滚动条悬停效果
+- [x] 实现点击popup时弹出完整SupplyChainModal
+  - [x] 在SupplyChainMap组件中添加弹出层状态管理
+  - [x] 修改定位点点击事件,支持显示完整弹出层
+  - [x] 集成SupplyChainModal组件,传递当前定位点的数据
+  - [x] 实现弹出层关闭功能
+  - [x] 验证弹出层主题色正确应用
+  - [x] 测试ESC键和遮罩层点击关闭功能
+  - [x] 优化SupplyChainModal图片展示布局
+    - [x] 支持单图展示模式(图片居中显示)
+    - [x] 支持多图展示模式(图片横向滚动)
+    - [x] 根据图片数量动态选择展示模式
+    - [x] 实现横向滚动功能
+    - [x] 实现滚动条交互功能
+      - [x] 滚动条可点击拖动控制图片滚动
+      - [x] 滚动条位置与图片滚动位置同步
+      - [x] 滚动条样式与主题色协调
+      - [x] 滚动条悬停效果
     - [ ] 验证单图模式图片居中显示
     - [ ] 验证单图模式图片居中显示
     - [ ] 验证多图模式横向滚动功能
     - [ ] 验证多图模式横向滚动功能
     - [ ] 验证滚动条交互功能正常
     - [ ] 验证滚动条交互功能正常

+ 7 - 3
src/client/home/pages/SupplyChainDashboards/SupplyChainDashboard.tsx

@@ -24,7 +24,7 @@ interface PopupState {
 interface ModalState {
 interface ModalState {
   isOpen: boolean;
   isOpen: boolean;
   title?: string;
   title?: string;
-  imageUrl?: string;
+  imageUrls?: string[];
 }
 }
 
 
 // 内部组件,使用SupplyChainContext
 // 内部组件,使用SupplyChainContext
@@ -78,7 +78,11 @@ const DashboardContent: React.FC = () => {
       setModalState({
       setModalState({
         isOpen: true,
         isOpen: true,
         title: popupData.title,
         title: popupData.title,
-        imageUrl: "https://placehold.co/960x640" // 默认图片,后续可以根据数据动态设置
+        imageUrls: [
+          "https://placehold.co/960x640/5DEF8B/FFFFFF?text=基地图片1",
+          "https://placehold.co/960x640/FFF586/000000?text=基地图片2",
+          "https://placehold.co/960x640/FF6B6B/FFFFFF?text=基地图片3"
+        ] // 测试多图模式,后续可以根据数据动态设置
       });
       });
 
 
       // 关闭小弹出框
       // 关闭小弹出框
@@ -141,7 +145,7 @@ const DashboardContent: React.FC = () => {
         isOpen={modalState.isOpen}
         isOpen={modalState.isOpen}
         onClose={handleCloseModal}
         onClose={handleCloseModal}
         title={modalState.title}
         title={modalState.title}
-        imageUrl={modalState.imageUrl}
+        imageUrls={modalState.imageUrls}
       />
       />
     </div>
     </div>
   );
   );

+ 186 - 40
src/client/home/pages/SupplyChainDashboards/components/SupplyChainModal.tsx

@@ -1,27 +1,120 @@
-import React from 'react';
+import React, { useRef, useState, useEffect, useCallback } from 'react';
 import { useSupplyChain } from '../context/SupplyChainContext';
 import { useSupplyChain } from '../context/SupplyChainContext';
 
 
+// 图片展示模式类型
+type ImageDisplayMode = 'single' | 'multiple';
+
+// 滚动条状态管理
+interface ScrollbarState {
+  isDragging: boolean;
+  scrollPosition: number;
+  scrollbarWidth: number;
+  containerWidth: number;
+}
+
 interface SupplyChainModalProps {
 interface SupplyChainModalProps {
   isOpen: boolean;
   isOpen: boolean;
   onClose: () => void;
   onClose: () => void;
   title?: string;
   title?: string;
-  imageUrl?: string;
+  imageUrls?: string[];
   children?: React.ReactNode;
   children?: React.ReactNode;
 }
 }
 
 
+// 滚动条交互工具函数
+const createScrollbarInteraction = (
+  containerRef: React.RefObject<HTMLDivElement | null>,
+  scrollState: ScrollbarState,
+  setScrollState: React.Dispatch<React.SetStateAction<ScrollbarState>>
+) => {
+  // 处理滚动条点击
+  const handleScrollbarClick = (event: React.MouseEvent) => {
+    const rect = event.currentTarget.getBoundingClientRect();
+    const clickPosition = event.clientX - rect.left;
+    const scrollPercentage = clickPosition / rect.width;
+
+    if (containerRef.current) {
+      const scrollWidth = containerRef.current.scrollWidth - containerRef.current.clientWidth;
+      containerRef.current.scrollLeft = scrollWidth * scrollPercentage;
+    }
+  };
+
+  // 处理滚动条拖动开始
+  const handleScrollbarDragStart = (event: React.MouseEvent) => {
+    event.preventDefault();
+    setScrollState(prev => ({ ...prev, isDragging: true }));
+  };
+
+  // 处理滚动条拖动
+  const handleScrollbarDrag = (event: React.MouseEvent) => {
+    if (!scrollState.isDragging) return;
+
+    const rect = event.currentTarget.getBoundingClientRect();
+    const dragPosition = event.clientX - rect.left;
+    const scrollPercentage = Math.max(0, Math.min(1, dragPosition / rect.width));
+
+    if (containerRef.current) {
+      const scrollWidth = containerRef.current.scrollWidth - containerRef.current.clientWidth;
+      containerRef.current.scrollLeft = scrollWidth * scrollPercentage;
+    }
+  };
+
+  // 处理滚动条拖动结束
+  const handleScrollbarDragEnd = () => {
+    setScrollState(prev => ({ ...prev, isDragging: false }));
+  };
+
+  return {
+    handleScrollbarClick,
+    handleScrollbarDragStart,
+    handleScrollbarDrag,
+    handleScrollbarDragEnd
+  };
+};
+
 const SupplyChainModal: React.FC<SupplyChainModalProps> = ({
 const SupplyChainModal: React.FC<SupplyChainModalProps> = ({
   isOpen,
   isOpen,
   onClose,
   onClose,
   title = "江汉大米优质水稻种植核心示范基地",
   title = "江汉大米优质水稻种植核心示范基地",
-  imageUrl = "https://placehold.co/960x640",
-  children
+  imageUrls = ["https://placehold.co/960x640"]
 }) => {
 }) => {
   const { themeColor } = useSupplyChain();
   const { themeColor } = useSupplyChain();
+  const imageContainerRef = useRef<HTMLDivElement>(null);
 
 
-  if (!isOpen) return null;
+  // 滚动条状态管理
+  const [scrollState, setScrollState] = useState<ScrollbarState>({
+    isDragging: false,
+    scrollPosition: 0,
+    scrollbarWidth: 0,
+    containerWidth: 0
+  });
+
+  // 根据图片数量确定展示模式
+  const displayMode: ImageDisplayMode = imageUrls.length <= 1 ? 'single' : 'multiple';
+
+  // 使用滚动条交互工具函数
+  const {
+    handleScrollbarClick,
+    handleScrollbarDragStart,
+    handleScrollbarDrag,
+    handleScrollbarDragEnd
+  } = createScrollbarInteraction(imageContainerRef, scrollState, setScrollState);
+
+  // 同步滚动条位置函数
+  const syncScrollbarPosition = useCallback(() => {
+    if (imageContainerRef.current) {
+      const scrollLeft = imageContainerRef.current.scrollLeft;
+      const scrollWidth = imageContainerRef.current.scrollWidth - imageContainerRef.current.clientWidth;
+      const scrollPercentage = scrollWidth > 0 ? scrollLeft / scrollWidth : 0;
+
+      setScrollState(prev => ({
+        ...prev,
+        scrollPosition: scrollPercentage
+      }));
+    }
+  }, []);
 
 
   // ESC键关闭弹窗
   // ESC键关闭弹窗
-  React.useEffect(() => {
+  useEffect(() => {
     const handleEsc = (event: KeyboardEvent) => {
     const handleEsc = (event: KeyboardEvent) => {
       if (event.key === 'Escape') {
       if (event.key === 'Escape') {
         onClose();
         onClose();
@@ -34,6 +127,28 @@ const SupplyChainModal: React.FC<SupplyChainModalProps> = ({
     }
     }
   }, [isOpen, onClose]);
   }, [isOpen, onClose]);
 
 
+  // 监听图片容器滚动事件
+  useEffect(() => {
+    const container = imageContainerRef.current;
+    if (!container) return;
+
+    const handleScroll = () => {
+      syncScrollbarPosition();
+    };
+
+    container.addEventListener('scroll', handleScroll);
+    return () => container.removeEventListener('scroll', handleScroll);
+  }, [syncScrollbarPosition]);
+
+  // 初始化滚动条位置
+  useEffect(() => {
+    if (imageContainerRef.current) {
+      syncScrollbarPosition();
+    }
+  }, [imageUrls, syncScrollbarPosition]);
+
+  if (!isOpen) return null;
+
   return (
   return (
     <div
     <div
       data-layer="操控屏-1-粮食•油脂-粮食首页-基地弹出数据"
       data-layer="操控屏-1-粮食•油脂-粮食首页-基地弹出数据"
@@ -62,44 +177,75 @@ const SupplyChainModal: React.FC<SupplyChainModalProps> = ({
           </svg>
           </svg>
         </div>
         </div>
 
 
-        {/* 图片样式1 */}
-        <div data-layer="图片样式1" className="absolute h-[640px] left-[calc(50%+-0.06px)] overflow-clip rounded-[20px] top-[calc(50%+48.26px)] translate-x-[-50%] translate-y-[-50%] w-[1357px]">
-          <div data-layer="Frame 1321316691" className="absolute left-0 top-1/2 translate-y-[-50%] content-stretch flex gap-[20px] items-center">
-            {/* 第一张图片 */}
-            <div data-layer="图片1" className="h-[640px] overflow-clip relative rounded-[10px] shrink-0 w-[853px]">
-              <img
-                data-layer="湖北农发种业老河口张集镇小麦种业示范基地 1"
-                className="w-full h-full object-cover"
-                src={imageUrl}
-                alt={title}
-              />
-            </div>
-            {/* 第二张图片 */}
-            <div data-layer="图片2" className="h-[640px] overflow-clip relative rounded-[10px] shrink-0 w-[940px]">
-              <img
-                data-layer="老河口张集镇小麦种业示范基地 1"
-                className="w-full h-full object-cover"
-                src={imageUrl}
-                alt={title}
-              />
+        {/* 图片展示区域 */}
+        <div
+          data-layer="图片展示区域"
+          className="absolute h-[640px] left-[calc(50%+-0.06px)] rounded-[20px] top-[calc(50%+48.26px)] translate-x-[-50%] translate-y-[-50%] w-[1357px]"
+        >
+          {/* 单图模式 - 图片居中显示 */}
+          {displayMode === 'single' && (
+            <div className="flex items-center justify-center h-full">
+              <div className="h-[640px] overflow-clip relative rounded-[10px] w-[853px]">
+                <img
+                  data-layer="单图展示"
+                  className="w-full h-full object-cover"
+                  src={imageUrls[0]}
+                  alt={title}
+                />
+              </div>
             </div>
             </div>
-          </div>
-        </div>
+          )}
 
 
-        {/* 下拉框(滚动条) */}
-        <div data-layer="下拉框" className="absolute contents inset-[926.48px_281.56px_147.52px_281.44px]">
-          <div className="absolute flex inset-[926.48px_281.56px_147.52px_281.44px] items-center justify-center">
-            <div className="flex-none h-[1357px] rotate-[270deg] w-[6px]">
-              <div className="bg-[rgba(255,255,255,0.12)] rounded-[12px] shadow-[0px_40px_70px_0px_rgba(0,0,0,0.24)] size-full" data-name="bg" />
-            </div>
-          </div>
-          <div className="absolute flex inset-[926.48px_745.3px_147.52px_281.44px] items-center justify-center">
-            <div className="flex-none h-[893.259px] rotate-[270deg] w-[6px]">
-              <div className="bg-[rgba(255,255,255,0.12)] rounded-[10px] shadow-[0px_40px_70px_0px_rgba(0,0,0,0.24)] size-full" data-name="bg" />
-            </div>
-          </div>
+          {/* 多图模式 - 横向滚动 */}
+          {displayMode === 'multiple' && (
+            <>
+              <div
+                ref={imageContainerRef}
+                data-layer="多图滚动容器"
+                className="flex overflow-x-auto gap-[20px] h-full items-center"
+                style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
+              >
+                {imageUrls.map((url, index) => (
+                  <div
+                    key={index}
+                    data-layer={`图片${index + 1}`}
+                    className="h-[640px] overflow-clip relative rounded-[10px] shrink-0 w-[853px]"
+                  >
+                    <img
+                      data-layer={`${title} ${index + 1}`}
+                      className="w-full h-full object-cover"
+                      src={url}
+                      alt={`${title} ${index + 1}`}
+                    />
+                  </div>
+                ))}
+              </div>
+
+              {/* 自定义滚动条 */}
+              <div
+                data-layer="滚动条容器"
+                className="absolute bottom-[-40px] left-1/2 transform -translate-x-1/2 w-[60%] h-[8px] bg-stone-700/30 rounded-[4px] cursor-pointer"
+                onClick={handleScrollbarClick}
+                onMouseDown={handleScrollbarDragStart}
+                onMouseMove={scrollState.isDragging ? handleScrollbarDrag : undefined}
+                onMouseUp={handleScrollbarDragEnd}
+                onMouseLeave={handleScrollbarDragEnd}
+              >
+                <div
+                  data-layer="滚动条滑块"
+                  className="absolute h-full bg-stone-400 rounded-[4px] transition-all duration-200 hover:bg-stone-300 cursor-grab active:cursor-grabbing"
+                  style={{
+                    width: '20%',
+                    left: `${scrollState.scrollPosition * 80}%`,
+                    backgroundColor: themeColor
+                  }}
+                />
+              </div>
+            </>
+          )}
         </div>
         </div>
 
 
+
         {/* 标题 */}
         {/* 标题 */}
         <div data-layer="标题" className="absolute content-stretch flex gap-[30px] h-[125px] items-center justify-center left-1/2 top-[42px] translate-x-[-50%] w-[1200px]">
         <div data-layer="标题" className="absolute content-stretch flex gap-[30px] h-[125px] items-center justify-center left-1/2 top-[42px] translate-x-[-50%] w-[1200px]">
           {/* 标题元素 - 光线左侧 */}
           {/* 标题元素 - 光线左侧 */}