import React, { useState, useRef, useEffect, useMemo } from "react";
|
import { useTranslate, useRefresh, useSidebarState } from "react-admin";
|
import {
|
TextField,
|
Select,
|
MenuItem,
|
Button,
|
Box,
|
SpeedDial,
|
SpeedDialAction,
|
useTheme,
|
Fab,
|
} from '@mui/material';
|
import {
|
MoreVert as MoreVertIcon,
|
Edit as EditIcon,
|
FileCopy as FileCopyIcon,
|
Save as SaveIcon,
|
Print as PrintIcon,
|
Share as ShareIcon,
|
} from '@mui/icons-material';
|
import { MAP_MODE } from "./constants";
|
import Player from './player';
|
import * as Tool from './tool';
|
import { NotificationProvider, useNotification } from './Notification';
|
import Insight from "./insight";
|
import Device from "./Device";
|
import Settings from "./settings";
|
import Batch from "./batch";
|
import * as Http from './http';
|
import WebSocketClient from './websocket'
|
import ConfirmButton from "../page/components/ConfirmButton";
|
import { FitScreen } from '@mui/icons-material';
|
|
|
let player;
|
let websocket;
|
|
const Map = () => {
|
const notify = useNotification();
|
const [sidebarOpen] = useSidebarState();
|
const translate = useTranslate();
|
const theme = useTheme();
|
const themeMode = theme.palette.mode;
|
|
const mapRef = useRef();
|
const contentRef = useRef();
|
const [app, setApp] = useState(null);
|
const [mapContainer, setMapContainer] = useState(null);
|
|
const [mode, setMode] = useState(MAP_MODE.OBSERVER_MODE);
|
const [insightVisible, setInsightVisible] = useState(false);
|
const [deviceVisible, setDeviceVisible] = useState(false);
|
const [settingsVisible, setSettingsVisible] = useState(false);
|
const [batchSelectionVisible, setBatchSelectionVisible] = useState(false);
|
|
const [spriteSettings, setSpriteSettings] = useState(null);
|
const prevSpriteSettingsRef = useRef();
|
const [batchSprites, setBatchSprites] = useState([]);
|
|
const [curZone, setCurZone] = useState(() => {
|
const storedValue = localStorage.getItem('curZone');
|
return storedValue !== null ? JSON.parse(storedValue) : null;
|
});
|
|
const handleResize = () => {
|
const width = contentRef.current.offsetWidth;
|
const height = contentRef.current.offsetHeight;
|
player.resize(width, height);
|
};
|
|
useEffect(() => {
|
Tool.patchRaLayout('0px');
|
const initialize = async () => {
|
player = new Player(mapRef.current, themeMode);
|
setApp(player.app);
|
setMapContainer(player.mapContainer);
|
Tool.setApp(player.app);
|
Tool.setMapContainer(player.mapContainer);
|
Tool.setThemeMode(themeMode);
|
Http.setNotify(notify);
|
Http.setMapContainer(player.mapContainer);
|
websocket = new WebSocketClient('/ws/map/websocket');
|
|
await Http.fetchMapData();
|
websocket.connect();
|
websocket.onMessage = (wsMsg) => {
|
Tool.generateDynamicGraphic(curZone, JSON.parse(wsMsg));
|
}
|
|
setTimeout(() => {
|
player.adaptScreen();
|
notify.info(translate('page.map.welcome'));
|
}, 200)
|
}
|
initialize();
|
|
// resize
|
handleResize();
|
window.addEventListener('resize', handleResize);
|
|
return () => {
|
if (websocket) {
|
websocket.onMessage = () => { }
|
websocket.close();
|
}
|
player.destroy();
|
window.removeEventListener('resize', handleResize);
|
Tool.patchRaLayout('');
|
};
|
}, [])
|
|
useEffect(() => {
|
setTimeout(handleResize, 300)
|
}, [sidebarOpen]);
|
|
useEffect(() => {
|
player.setTheme(themeMode);
|
Tool.setThemeMode(themeMode);
|
}, [themeMode])
|
|
const switchMode = (mode) => {
|
Tool.removeSelectedEffect();
|
|
setDeviceVisible(false);
|
setSettingsVisible(false);
|
setBatchSelectionVisible(false);
|
|
setSpriteSettings(null);
|
setBatchSprites([]);
|
|
switch (mode) {
|
case MAP_MODE.OBSERVER_MODE:
|
player.hideGridLines();
|
player.hideStarryBackground();
|
|
player.activateMapMultiSelect((selectedSprites, restartFn) => {
|
setBatchSprites(selectedSprites);
|
});
|
|
break
|
case MAP_MODE.MOVABLE_MODE:
|
player.showGridLines();
|
player.hideStarryBackground();
|
|
player.activateMapMultiSelect((selectedSprites, restartFn) => {
|
Tool.spriteListBeMovable(selectedSprites, () => {
|
restartFn();
|
});
|
});
|
|
mapContainer.children.forEach(child => {
|
Tool.beMovable(child);
|
})
|
break
|
case MAP_MODE.SETTINGS_MODE:
|
player.hideGridLines();
|
// player.showStarryBackground(); // 0x2f68ac
|
|
player.activateMapMultiSelect((selectedSprites, restartFn) => {
|
setBatchSprites(selectedSprites);
|
});
|
|
mapContainer.children.forEach(child => {
|
Tool.beSettings(child, setSpriteSettings);
|
})
|
break
|
default:
|
break
|
}
|
}
|
|
useEffect(() => {
|
if (!mapContainer) {
|
return
|
}
|
switchMode(mode);
|
}, [mode, mapContainer]);
|
|
const onDrop = (sprite, type, x, y) => {
|
const { mapX, mapY } = Tool.getRealPosition(x, y);
|
sprite.x = mapX;
|
sprite.y = mapY;
|
|
Tool.initSprite(sprite, type);
|
mapContainer.addChild(sprite);
|
Tool.beMovable(sprite);
|
};
|
|
// watch spriteSettings
|
useEffect(() => {
|
if (!mapContainer) {
|
return;
|
}
|
prevSpriteSettingsRef.current = spriteSettings;
|
if (spriteSettings && prevSpriteSettings !== spriteSettings) {
|
Tool.removeSelectedEffect();
|
}
|
if (spriteSettings) {
|
Tool.showSelectedEffect(spriteSettings)
|
setSettingsVisible(true);
|
} else {
|
Tool.removeSelectedEffect();
|
}
|
}, [spriteSettings, mapContainer])
|
const prevSpriteSettings = prevSpriteSettingsRef.current;
|
|
// watch batchSprites
|
React.useEffect(() => {
|
if (!mapContainer) {
|
return;
|
}
|
if (batchSprites?.length > 0) {
|
setBatchSelectionVisible(true)
|
} else {
|
player.clearSelectedSprites();
|
setBatchSelectionVisible(false)
|
}
|
}, [batchSprites])
|
|
const actions = [
|
{ icon: <FileCopyIcon />, name: '复制' },
|
{ icon: <SaveIcon />, name: '保存' },
|
{ icon: <PrintIcon />, name: '打印' },
|
{ icon: <ShareIcon />, name: '分享' },
|
{ icon: <EditIcon />, name: '编辑' },
|
];
|
|
return (
|
<Box
|
sx={{
|
width: '100%',
|
height: '100%',
|
display: 'flex',
|
flexDirection: 'column',
|
}}
|
>
|
{/* header */}
|
<Box
|
sx={{
|
display: 'flex',
|
alignItems: 'center',
|
backgroundColor: theme.palette.background.default,
|
color: theme.palette.text.primary,
|
padding: '0 16px',
|
height: '64px',
|
flexShrink: 0, // keep height
|
zIndex: 200,
|
boxShadow: theme.shadows[1],
|
}}
|
>
|
<TextField
|
variant="outlined"
|
size="small"
|
placeholder="搜索..."
|
sx={{
|
width: '200px',
|
backgroundColor: theme.palette.background.paper,
|
borderRadius: 1,
|
}}
|
/>
|
<Box sx={{ flexGrow: 1 }} />
|
|
{mode === MAP_MODE.OBSERVER_MODE && (
|
<>
|
<Button
|
variant="contained"
|
color="primary"
|
sx={{ mr: 2 }}
|
>
|
{translate('page.map.action.startup')}
|
</Button>
|
<Button variant="contained" color="primary">
|
{translate('page.map.action.monitor')}
|
</Button>
|
</>
|
)}
|
|
{mode === MAP_MODE.MOVABLE_MODE && (
|
<>
|
<Button
|
variant="outlined"
|
sx={{ mr: 2 }}
|
onClick={() => {
|
Tool.clearMapData();
|
}}
|
>
|
{translate('page.map.action.clear')}
|
</Button>
|
<ConfirmButton
|
label="page.map.action.save"
|
variant="contained"
|
sx={{ mr: 2 }}
|
onConfirm={() => {
|
Http.saveMapData(curZone);
|
}}
|
/>
|
<Button
|
variant="contained"
|
color="primary"
|
onClick={() => setDeviceVisible(!deviceVisible)}
|
>
|
{translate('page.map.devices.title')}
|
</Button>
|
</>
|
)}
|
|
{mode === MAP_MODE.SETTINGS_MODE && (
|
<>
|
<Button
|
variant="outlined"
|
sx={{ mr: 2 }}
|
onClick={() => {
|
Tool.clearMapData();
|
}}
|
>
|
{translate('page.map.action.clear')}
|
</Button>
|
<ConfirmButton
|
label="page.map.action.save"
|
variant="contained"
|
onConfirm={() => {
|
Http.saveMapData(curZone);
|
}}
|
/>
|
</>
|
)}
|
|
<Select
|
value={mode}
|
onChange={(event) => {
|
setMode(event.target.value);
|
}}
|
variant="outlined"
|
size="small"
|
sx={{
|
ml: 2,
|
backgroundColor: theme.palette.background.paper,
|
color: theme.palette.text.primary,
|
borderRadius: 1,
|
}}
|
>
|
<MenuItem value={MAP_MODE.OBSERVER_MODE}>{translate('page.map.mode.observer')}</MenuItem>
|
<MenuItem value={MAP_MODE.MOVABLE_MODE}>{translate('page.map.mode.movable')}</MenuItem>
|
<MenuItem value={MAP_MODE.SETTINGS_MODE}>{translate('page.map.mode.settings')}</MenuItem>
|
</Select>
|
</Box>
|
{/* content */}
|
<Box
|
sx={{
|
flexGrow: 1, // fill remaining of map space
|
position: 'relative',
|
backgroundColor: '#fff',
|
}}
|
>
|
<Box
|
ref={contentRef}
|
sx={{
|
position: 'relative',
|
width: '100%',
|
height: '100%',
|
backgroundColor: '#e0e0e0',
|
overflowY: 'hidden',
|
}}
|
>
|
<div ref={mapRef} style={{
|
position: 'absolute',
|
top: 0,
|
left: 0,
|
width: '100%',
|
height: '100%',
|
}} />
|
</Box>
|
|
{/* <SpeedDial
|
ariaLabel="SpeedDial 示例"
|
sx={{ position: 'absolute', bottom: 16, right: 16 }}
|
icon={<MoreVertIcon />}
|
>
|
{actions.map((action) => (
|
<SpeedDialAction
|
key={action.name}
|
icon={action.icon}
|
tooltipTitle={action.name}
|
/>
|
))}
|
</SpeedDial> */}
|
|
<Box
|
sx={{
|
position: 'absolute',
|
left: 35,
|
bottom: 30,
|
display: 'flex',
|
flexDirection: 'column',
|
gap: 1
|
}}
|
>
|
<Fab
|
variant="extended"
|
color="primary"
|
size="medium"
|
onClick={() => {
|
player.adaptScreen();
|
}}
|
>
|
<FitScreen />
|
{translate('page.map.action.adapt')}
|
</Fab>
|
</Box>
|
</Box>
|
|
<Insight
|
open={insightVisible}
|
onCancel={() => {
|
setInsightVisible(false);
|
}}
|
width={378}
|
/>
|
|
<Device
|
open={deviceVisible}
|
onCancel={() => {
|
setDeviceVisible(false);
|
}}
|
onDrop={onDrop}
|
width={378}
|
/>
|
|
<Settings
|
open={settingsVisible}
|
onCancel={() => {
|
setSpriteSettings(null);
|
setSettingsVisible(false);
|
}}
|
sprite={spriteSettings}
|
setSpriteSettings={setSpriteSettings}
|
width={570}
|
/>
|
|
<Batch
|
open={batchSelectionVisible}
|
onCancel={() => {
|
setBatchSprites([]);
|
setBatchSelectionVisible(false);
|
}}
|
batchSprites={batchSprites}
|
mode={mode}
|
width={570}
|
/>
|
|
</Box>
|
);
|
}
|
|
const MapPage = () => {
|
return (
|
<NotificationProvider>
|
<Map />
|
</NotificationProvider>
|
)
|
}
|
|
export default MapPage;
|