父史诗: 史诗005 - 史诗005 - 供应链可视化大屏实现 docs/prd/epic-005-supply-chain-visualization.md
Completed
As a 前端开发者
I want 组件能够动态加载SupplyChainContext中的种业-果蔬组合数据
so that 用户可以通过路由参数/supply-chain/seed-fruit访问种业-果蔬组合大屏,并看到基于动态数据的完整供应链地图可视化、关键指标和数据卡片
Scope: 修改现有组件以动态加载SupplyChainContext中的种业-果蔬组合数据,包括定位点坐标、关键指标、供应链网络和弹出框数据
/supply-chain/seed-fruit正确加载种业-果蔬数据src/client/home/pages/SupplyChainDashboards/context/SupplyChainContext.tsxsrc/client/home/pages/SupplyChainDashboards/components/src/share/types.ts (共享类型)tests/unit/client/home/pages/SupplyChainDashboards/context/// 组合类型定义
type DashboardType = 'grain-oil' | 'seed-fruit' | 'livestock-aquaculture' | 'fresh-food-salt';
// 产业类型定义(继承自现有ThemeContext)
type IndustryType = "粮食" | "油脂" | "种业" | "果蔬" | "畜牧" | "水产" | "鲜食" | "泛盐";
// 定位点坐标接口
interface LocationPoint {
id: string;
type: 'base' | 'chain';
x: number;
y: number;
industry: IndustryType;
name?: string;
data?: Record<string, any>;
}
// 关键指标数据接口
interface MetricData {
id: string;
label: string;
value: string | number;
unit?: string;
industry: IndustryType;
}
// 供应链网络数据接口
interface SupplyChainNetwork {
connections: Array<{
from: string;
to: string;
type: string;
}>;
nodes: Record<string, LocationPoint>;
}
// 弹出框数据接口
interface PopupData {
id: string;
title: string;
content: string;
position: { x: number; y: number };
industry: IndustryType;
}
// 供应链数据接口
interface SupplyChainData {
// 组合名称
name: string;
// 支持的产业
industries: IndustryType[];
// 定位点数据
mapPoints: Record<IndustryType, LocationPoint[]>;
// 关键指标数据
keyMetrics: Record<IndustryType, MetricData[]>;
// 供应链网络数据
supplyChainNetwork: Record<IndustryType, SupplyChainNetwork>;
// 弹出框数据
popupData: Record<IndustryType, PopupData[]>;
}
src/client/home/pages/SupplyChainDashboards/components/SupplyChainMap.tsxsrc/client/home/pages/SupplyChainDashboards/components/KeyMetrics.tsxsrc/client/home/pages/SupplyChainDashboards/components/PopupInfoBox.tsxseed-popup1、fruit-popup1)// 正确的数据获取方式
const { currentData, currentIndustry } = useSupplyChain();
// 获取当前产业的定位点数据
const mapPoints = currentData?.mapPoints[currentIndustry] || [];
// 获取当前产业的关键指标数据
const keyMetrics = currentData?.keyMetrics[currentIndustry] || [];
款式分配规则:
定位逻辑:
// 根据产业在组合中的位置确定款式和定位方式
function getIndustryVariant(industry: string, industries: string[]): 'first' | 'second' | null {
const index = industries.indexOf(industry);
if (index === 0) return 'first'; // 第一个产业使用第一款
if (index === 1) return 'second'; // 第二个产业使用第二款
return null; // 其他情况返回null,使用默认值
}
实现策略:
展示模式策略:
实现方案:
// 图片展示模式类型
type ImageDisplayMode = 'single' | 'multiple';
// 根据图片数量确定展示模式
function getImageDisplayMode(imageUrls: string[]): ImageDisplayMode {
return imageUrls.length <= 1 ? 'single' : 'multiple';
}
// 单图模式布局
const singleImageLayout = (
<div className="flex items-center justify-center">
<img src={imageUrls[0]} className="max-w-full max-h-full object-contain" />
</div>
);
// 多图模式布局
const multipleImageLayout = (
<div className="flex overflow-x-auto gap-4">
{imageUrls.map((url, index) => (
<img key={index} src={url} className="flex-shrink-0 max-h-full object-contain" />
))}
</div>
);
CSS样式要求:
滚动条交互实现方案:
// 滚动条状态管理
interface ScrollbarState {
isDragging: boolean;
scrollPosition: number;
scrollbarWidth: number;
containerWidth: number;
}
// 滚动条交互逻辑
const useScrollbarInteraction = (containerRef: React.RefObject<HTMLDivElement>) => {
const [scrollState, setScrollState] = useState<ScrollbarState>({
isDragging: false,
scrollPosition: 0,
scrollbarWidth: 0,
containerWidth: 0
});
// 处理滚动条点击
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 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 syncScrollbarPosition = () => {
if (containerRef.current) {
const scrollLeft = containerRef.current.scrollLeft;
const scrollWidth = containerRef.current.scrollWidth - containerRef.current.clientWidth;
const scrollPercentage = scrollWidth > 0 ? scrollLeft / scrollWidth : 0;
setScrollState(prev => ({
...prev,
scrollPosition: scrollPercentage
}));
}
};
return {
scrollState,
handleScrollbarClick,
handleScrollbarDrag,
syncScrollbarPosition
};
};
滚动条样式设计:
/* 滚动条容器 */
.scrollbar-container {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
cursor: pointer;
}
/* 滚动条滑块 */
.scrollbar-thumb {
position: absolute;
height: 100%;
background: ${themeColor};
border-radius: 4px;
transition: background-color 0.2s ease;
cursor: grab;
}
.scrollbar-thumb:hover {
background: ${themeColor};
opacity: 0.8;
}
.scrollbar-thumb:active {
cursor: grabbing;
}
使用@tanstack/react-query进行服务端状态管理,实现数据获取、缓存和同步:
// React Query配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5分钟
gcTime: 10 * 60 * 1000, // 10分钟
},
},
});
// 数据查询Hook
const useSupplyChainData = (dashboardType: DashboardType) => {
return useQuery({
queryKey: ['supply-chain', dashboardType],
queryFn: () => loadStaticData(dashboardType),
enabled: !!dashboardType,
});
};
基于现有SupplyChainContext数据,修改组件使用动态数据:
// SupplyChainMap组件修改示例
const SupplyChainMap: React.FC<SupplyChainMapProps> = ({ onPointClick }) => {
const { currentData, currentIndustry } = useSupplyChain();
// 从context获取当前产业的定位点数据
const mapPoints = currentData?.mapPoints[currentIndustry] || [];
return (
<div className="supply-chain-map">
{mapPoints.map(point => (
<MapPoint
key={point.id}
point={point}
onClick={() => onPointClick(point)}
/>
))}
</div>
);
};
// KeyMetrics组件修改示例
const KeyMetrics: React.FC<KeyMetricsProps> = ({ title, subtitle }) => {
const { currentData, currentIndustry } = useSupplyChain();
// 从context获取当前产业的关键指标数据
const keyMetrics = currentData?.keyMetrics[currentIndustry] || [];
return (
<div className="key-metrics">
{keyMetrics.map(metric => (
<DataCard
key={metric.id}
title={metric.label}
value={metric.value}
unit={metric.unit}
/>
))}
</div>
);
};
SupplyChainMap组件修改步骤:
KeyMetrics组件修改步骤:
tests/unit/client/home/pages/SupplyChainDashboards/context//supply-chain/seed-fruit正确解析| Date | Version | Description | Author |
|---|---|---|---|
| 2025-11-16 | 1.0 | 初始故事创建,基于Epic 005需求 | Bob (SM) |
| 2025-11-16 | 1.1 | 添加点击popup时弹出完整SupplyChainModal的任务和验收标准 | Claude |
| 2025-11-16 | 1.2 | 添加SupplyChainModal图片展示布局优化任务,支持单图居中显示和多图横向滚动 | Claude |
| 2025-11-16 | 1.3 | 添加滚动条交互功能需求,支持点击拖动控制图片滚动 | Claude |
/supply-chain/:dashboardType已支持seed-fruit组合src/client/home/pages/SupplyChainDashboards/components/SupplyChainMap.tsx - ✅ 已修改为使用动态定位点数据src/client/home/pages/SupplyChainDashboards/components/KeyMetrics.tsx - ✅ 已修改为使用动态关键指标数据src/client/home/pages/SupplyChainDashboards/components/PopupInfoBox.tsx - ✅ 已修改为使用动态弹出框数据,并建立定位点ID与弹出框数据的映射关系,添加双款式支持和定位逻辑src/client/home/pages/SupplyChainDashboards/components/icons/PopupInfoBox2.tsx - ✅ 已修复SVG属性语法src/client/home/pages/SupplyChainDashboards/context/SupplyChainContext.tsx - ✅ 已为所有4个组合的49个定位点添加对应的弹出框数据,并移除重复的坐标定义,优化初始化逻辑src/client/home/pages/SupplyChainDashboards/SupplyChainDashboard.tsx - ✅ 已优化组件逻辑,移除pointIndex相关代码src/client/home/pages/SupplyChainDashboards/components/icons/IndustryIcon.tsx - ✅ 已更新为使用真实的产业图标src/client/home/pages/SupplyChainDashboards/components/icons/SeedIcon.tsx - ✅ 种业图标src/client/home/pages/SupplyChainDashboards/components/icons/FruitIcon.tsx - ✅ 果蔬图标src/client/home/pages/SupplyChainDashboards/components/icons/LivestockIcon.tsx - ✅ 畜牧图标src/client/home/pages/SupplyChainDashboards/components/icons/AquacultureIcon.tsx - ✅ 水产图标src/client/home/pages/SupplyChainDashboards/components/icons/FreshFoodIcon.tsx - ✅ 鲜食图标src/client/home/pages/SupplyChainDashboards/components/icons/SaltIcon.tsx - ✅ 泛盐图标src/client/home/pages/SupplyChainDashboards/context/SupplyChainContext.tsx - 包含完整的种业-果蔬组合数据src/client/home/routes.tsx - 动态路由配置src/client/home/pages/SupplyChainDashboards/SupplyChainDashboard.tsx - 统一组件已支持所有组合