Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/PROJECT_STRUCTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 项目组件目录结构设计

```text
Light-Up/
├─ src/
│ ├─ components/
│ │ ├─ HeaderBar.tsx # 顶部导航:系统名称、搜索框、时间选择
│ │ ├─ LayerManagementPanel.tsx # 左侧图层树:开关、透明度、图例
│ │ ├─ GlobalMap.tsx # 地图主组件:MapLibre + Deck.gl 图层 + ROI绘制 + Tooltip
│ │ ├─ AnalyticsPanel.tsx # 右侧分析面板:ECharts 时序统计图
│ │ └─ TimelineSlider.tsx # 底部时间轴:年份/月度联动
│ ├─ data/
│ │ └─ mockData.ts # Mock GeoJSON网格、时序数据、时间动态计算
│ ├─ services/
│ │ └─ geeService.ts # GEE API 预留服务层(当前为Mock)
│ ├─ types/
│ │ └─ layer.ts # 图层配置、时间状态、像元统计类型
│ ├─ App.tsx # Dashboard布局与全局状态编排
│ ├─ main.tsx # 应用入口
│ └─ index.css # Tailwind样式入口与全局样式
├─ index.html
├─ tailwind.config.js
├─ postcss.config.js
├─ vite.config.ts
├─ tsconfig.json
├─ tsconfig.app.json
└─ package.json
```
12 changes: 12 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>全球草地智能监测管理系统</title>
</head>
<body class="bg-slate-950">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "global-grassland-monitoring-spa",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@deck.gl/core": "^9.1.14",
"@deck.gl/geo-layers": "^9.1.14",
"@deck.gl/layers": "^9.1.14",
"@deck.gl/react": "^9.1.14",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"maplibre-gl": "^4.7.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-map-gl": "^7.1.7"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"typescript": "^5.6.3",
"vite": "^5.4.10",
"@types/geojson": "^7946.0.16"
}
}
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
105 changes: 105 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useMemo, useState } from 'react';
import { AnalyticsPanel } from './components/AnalyticsPanel';
import { GlobalMap } from './components/GlobalMap';
import { HeaderBar } from './components/HeaderBar';
import { LayerManagementPanel } from './components/LayerManagementPanel';
import { TimelineSlider } from './components/TimelineSlider';
import { buildMockTimeSeries, generateGrasslandGrid } from './data/mockData';
import { fetchGeeLayerTileUrl } from './services/geeService';
import type { LayerConfig, TimeState } from './types/layer';

const initialLayers: LayerConfig[] = [
{
id: 'satellite',
name: '卫星影像底图',
category: 'base',
visible: false,
opacity: 1,
legend: [{ color: '#64748b', label: '影像纹理' }],
},
{
id: 'darkMatter',
name: 'Dark Matter 底图',
category: 'base',
visible: true,
opacity: 1,
legend: [{ color: '#0f172a', label: '深色底图' }],
},
{
id: 'grasslandType',
name: '全球草地类型分类',
category: 'grassland',
visible: true,
opacity: 0.65,
legend: [
{ color: '#38bdf8', label: '高寒草甸' },
{ color: '#84cc16', label: '温带草原' },
{ color: '#facc15', label: '热带稀树草原' },
],
},
{
id: 'ndvi',
name: '植被指数 NDVI',
category: 'eco',
visible: true,
opacity: 0.45,
dynamicByTime: true,
legend: [{ color: '#16a34a', label: '低→高植被活性' }],
},
{
id: 'precipitation',
name: '降水热力图',
category: 'meteo',
visible: true,
opacity: 0.35,
dynamicByTime: true,
legend: [{ color: '#0284c7', label: '低→高降水' }],
},
];

function App() {
const [layers, setLayers] = useState(initialLayers);
const [time, setTime] = useState<TimeState>({ year: 2021, month: 7 });
const [panelCollapsed, setPanelCollapsed] = useState(false);
const gridData = useMemo(() => generateGrasslandGrid(), []);
const chartSeries = useMemo(() => buildMockTimeSeries(), []);

const handleTimeChange = async (next: TimeState) => {
setTime(next);
const activeDynamic = layers.filter((layer) => layer.visible && layer.dynamicByTime);
await Promise.all(activeDynamic.map((layer) => fetchGeeLayerTileUrl(layer.id, next)));
};

return (
<div className="relative h-screen w-screen overflow-hidden bg-slate-950 text-slate-100">
<HeaderBar
time={time}
onTimeChange={handleTimeChange}
onSearch={(keyword) => {
// 预留:未来可接入地名服务(Nominatim/GEE Geometry)定位。
// 当前仅演示输入联动与可扩展结构。
// eslint-disable-next-line no-console
console.log('搜索关键词:', keyword);
}}
/>

<LayerManagementPanel
layers={layers}
onVisibilityChange={(layerId, visible) => {
setLayers((prev) => prev.map((layer) => (layer.id === layerId ? { ...layer, visible } : layer)));
}}
onOpacityChange={(layerId, opacity) => {
setLayers((prev) => prev.map((layer) => (layer.id === layerId ? { ...layer, opacity } : layer)));
}}
/>

<AnalyticsPanel collapsed={panelCollapsed} onToggle={() => setPanelCollapsed((prev) => !prev)} series={chartSeries} />

<GlobalMap layers={layers} time={time} gridData={gridData} onRoiFinished={() => setPanelCollapsed(false)} />

<TimelineSlider time={time} onChange={handleTimeChange} />
</div>
);
}

export default App;
42 changes: 42 additions & 0 deletions src/components/AnalyticsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ReactECharts from 'echarts-for-react';

interface AnalyticsPanelProps {
collapsed: boolean;
onToggle: () => void;
series: Array<{ time: string; ndvi: number; precipitation: number }>;
}

export function AnalyticsPanel({ collapsed, onToggle, series }: AnalyticsPanelProps) {
const option = {
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
legend: { textStyle: { color: '#cbd5e1' } },
xAxis: {
type: 'category',
data: series.map((item) => item.time),
axisLabel: { color: '#94a3b8', showMinLabel: true, showMaxLabel: true },
},
yAxis: [
{ type: 'value', name: 'NDVI', axisLabel: { color: '#94a3b8' } },
{ type: 'value', name: '降水(mm)', axisLabel: { color: '#94a3b8' } },
],
series: [
{ name: 'NDVI', type: 'line', smooth: true, data: series.map((item) => item.ndvi) },
{ name: '降水', type: 'line', smooth: true, yAxisIndex: 1, data: series.map((item) => item.precipitation) },
],
};

return (
<aside className={`absolute right-3 top-20 z-20 h-[calc(100%-150px)] rounded-lg border border-slate-700/70 bg-slate-900/85 backdrop-blur transition-all ${collapsed ? 'w-12' : 'w-[420px]'}`}>
<button className="h-10 w-full border-b border-slate-700 text-sm text-cyan-300" onClick={onToggle}>
{collapsed ? '展开分析' : '收起分析'}
</button>
{!collapsed && (
<div className="h-[calc(100%-40px)] p-2">
<h3 className="mb-1 text-sm text-slate-200">ROI 时序统计(Mock)</h3>
<ReactECharts style={{ height: '95%', width: '100%' }} option={option} notMerge lazyUpdate />
</div>
)}
</aside>
);
}
Loading