| | |
| | | const enMap = { |
| | | page: { |
| | | map: { |
| | | action: { |
| | | addArea: 'Add Area', |
| | | }, |
| | | prompt: { |
| | | areaName: 'Please enter area name', |
| | | }, |
| | | msg: { |
| | | areaCreated: 'Area "%{name}" created', |
| | | }, |
| | | }, |
| | | }, |
| | | } |
| | |
| | | const zhMap = { |
| | | page: { |
| | | map: { |
| | | action: { |
| | | addArea: '添加区域', |
| | | }, |
| | | prompt: { |
| | | areaName: '请输入区域名称', |
| | | }, |
| | | msg: { |
| | | areaCreated: '区域 "%{name}" 已创建', |
| | | }, |
| | | }, |
| | | }, |
| | | } |
| | |
| | | flip: 'FLIP', |
| | | fake: 'FAKE', |
| | | route: 'ROUTE', |
| | | area: 'AREA', |
| | | disable: 'DISABLE', |
| | | enable: 'ENABLE', |
| | | reset: 'RESET', |
| | |
| | | oneClickPatrol: 'One-click Patrol', |
| | | cancelPatrol: 'Cancel Patrol', |
| | | addArea: 'Add Area', |
| | | cancelAddArea: 'Cancel Add', |
| | | areaList: 'Area List', |
| | | }, |
| | | mode: { |
| | | observer: 'OBSERVER', |
| | |
| | | }, |
| | | }, |
| | | }, |
| | | area: { |
| | | title: 'Area Settings', |
| | | tabs: { |
| | | basic: 'Basic', |
| | | advanced: 'Advanced', |
| | | }, |
| | | form: { |
| | | name: 'Name', |
| | | agv: 'Assign AGVs', |
| | | agvPlaceholder: 'Select AGVs', |
| | | codes: 'Codes in area (%{count})', |
| | | codesEmpty: 'No codes', |
| | | code: 'Area Code', |
| | | maxQty: 'Maximum Quantity', |
| | | speedLimit: 'Speed Limit', |
| | | startX: 'Start X', |
| | | startY: 'Start Y', |
| | | endX: 'End X', |
| | | endY: 'End Y', |
| | | memo: 'Memo', |
| | | priority: 'Priority', |
| | | agvCount: 'AGV · %{count}', |
| | | areaSize: 'Area %{size} ㎡', |
| | | }, |
| | | confirm: { |
| | | save: 'Save current changes?', |
| | | delete: 'This action cannot be undone. Delete this area?', |
| | | }, |
| | | prompt: { |
| | | nameInput: 'Please enter area name', |
| | | areaName: 'Please enter area name', |
| | | }, |
| | | msg: { |
| | | areaCreated: 'Area "%{name}" created', |
| | | }, |
| | | }, |
| | | }, |
| | | } |
| | | }; |
| | |
| | | monitor: '日志监控', |
| | | save: '保存地图', |
| | | clear: '清空地图', |
| | | adapt: 'ADAPT', |
| | | rotate: 'ROTATE', |
| | | flip: 'FLIP', |
| | | fake: 'FAKE', |
| | | route: 'ROUTE', |
| | | adapt: '适配', |
| | | rotate: '旋转', |
| | | flip: '翻转', |
| | | fake: '模拟', |
| | | route: '路线', |
| | | area: '区域', |
| | | disable: '禁用', |
| | | enable: '启用', |
| | | reset: '重置', |
| | |
| | | oneClickPatrol: '一键巡逻', |
| | | cancelPatrol: '取消巡逻', |
| | | addArea: '添加区域', |
| | | cancelAddArea: '取消添加', |
| | | areaList: '区域列表', |
| | | }, |
| | | mode: { |
| | | observer: '观察模式', |
| | |
| | | }, |
| | | }, |
| | | }, |
| | | area: { |
| | | title: '区域设置', |
| | | tabs: { |
| | | basic: '基础', |
| | | advanced: '高级', |
| | | }, |
| | | form: { |
| | | name: '名称', |
| | | agv: '选择AGV小车', |
| | | agvPlaceholder: '请选择AGV', |
| | | codes: '区域内条码集合 (%{count})', |
| | | codesEmpty: '暂无条码', |
| | | code: '区域编码', |
| | | maxQty: '最大数量', |
| | | speedLimit: '速度限制', |
| | | startX: '起点 X', |
| | | startY: '起点 Y', |
| | | endX: '终点 X', |
| | | endY: '终点 Y', |
| | | memo: '备注', |
| | | priority: '优先级', |
| | | agvCount: 'AGV · %{count}', |
| | | areaSize: '占地 %{size} ㎡', |
| | | }, |
| | | confirm: { |
| | | save: '确认保存当前修改?', |
| | | delete: '删除后将无法恢复,确认删除?', |
| | | }, |
| | | prompt: { |
| | | nameInput: '请输入区域名称', |
| | | areaName: '请输入区域名称', |
| | | }, |
| | | msg: { |
| | | areaCreated: '区域 "%{name}" 已创建', |
| | | }, |
| | | }, |
| | | }, |
| | | } |
| | | }; |
| New file |
| | |
| | | import React, { useEffect, useState } from "react"; |
| | | import { useTranslate } from "react-admin"; |
| | | import { |
| | | Drawer, |
| | | Box, |
| | | Typography, |
| | | IconButton, |
| | | Stack, |
| | | useTheme, |
| | | List, |
| | | ListItemButton, |
| | | ListItemText, |
| | | CircularProgress, |
| | | } from '@mui/material'; |
| | | import CloseIcon from '@mui/icons-material/Close'; |
| | | import { PAGE_DRAWER_WIDTH } from '@/config/setting'; |
| | | import { fetchAreaList } from './http'; |
| | | import * as Tool from './tool'; |
| | | |
| | | const AreaList = ({ |
| | | open, |
| | | onClose, |
| | | zoneId, |
| | | width = PAGE_DRAWER_WIDTH, |
| | | setCurSprite, |
| | | }) => { |
| | | const translate = useTranslate(); |
| | | const theme = useTheme(); |
| | | const themeMode = theme.palette.mode; |
| | | const [areas, setAreas] = useState([]); |
| | | const [loading, setLoading] = useState(false); |
| | | |
| | | useEffect(() => { |
| | | if (!open) { |
| | | setAreas([]); |
| | | return; |
| | | } |
| | | setLoading(true); |
| | | fetchAreaList(zoneId) |
| | | .then((list) => { |
| | | setAreas(Array.isArray(list) ? list : []); |
| | | }) |
| | | .finally(() => setLoading(false)); |
| | | }, [open, zoneId]); |
| | | |
| | | const handleItemClick = (area) => { |
| | | if (!area?.id) { |
| | | return; |
| | | } |
| | | const sprite = Tool.findAreaSpriteById(area.id); |
| | | if (sprite) { |
| | | Tool.focusAreaSprite(sprite, 570); |
| | | onClose(); |
| | | setCurSprite(sprite); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <Drawer |
| | | variant="persistent" |
| | | open={open} |
| | | anchor="right" |
| | | onClose={onClose} |
| | | sx={{ zIndex: 100, opacity: 0.95 }} |
| | | > |
| | | {open && ( |
| | | <Box pt={12} width={{ xs: '100vw', sm: width }} height={'calc(100vh - 200px)'} mt={{ xs: 2, sm: 1 }}> |
| | | <Stack direction="row" alignItems="center" px={3} py={2}> |
| | | <Typography variant="h6" flex={1}> |
| | | {translate('page.map.action.areaList')} |
| | | </Typography> |
| | | <IconButton onClick={onClose} size="small"> |
| | | <CloseIcon /> |
| | | </IconButton> |
| | | </Stack> |
| | | |
| | | <Box px={3} pb={3}> |
| | | {loading ? ( |
| | | <Box display="flex" justifyContent="center" mt={6}> |
| | | <CircularProgress /> |
| | | </Box> |
| | | ) : ( |
| | | <List dense sx={{ maxHeight: '70vh', overflowY: 'auto' }}> |
| | | {areas.length === 0 && ( |
| | | <Typography variant="body2" color="text.secondary" textAlign="center" py={4}> |
| | | {translate('page.map.area.form.codesEmpty')} |
| | | </Typography> |
| | | )} |
| | | {areas.map((area) => { |
| | | const agvCount = Array.isArray(area.agvList) ? area.agvList.length : 0; |
| | | const width = Math.abs((area?.end?.x ?? 0) - (area?.start?.x ?? 0)); |
| | | const height = Math.abs((area?.end?.y ?? 0) - (area?.start?.y ?? 0)); |
| | | const areaSize = (width * height) / 1_000_000; |
| | | const formattedSize = areaSize > 0 ? areaSize.toFixed(2) : '0.00'; |
| | | return ( |
| | | <ListItemButton |
| | | key={area.id} |
| | | onClick={() => handleItemClick(area)} |
| | | sx={{ |
| | | borderRadius: 2, |
| | | mb: 1, |
| | | px: 2, |
| | | py: 1.5, |
| | | background: |
| | | themeMode === 'light' |
| | | ? 'linear-gradient(145deg, #f8f9fa, #ffffff)' |
| | | : 'linear-gradient(145deg, #2d3436, #353b48)', |
| | | border: `1px solid ${theme.palette.divider}`, |
| | | display: 'flex', |
| | | gap: 1.5, |
| | | alignItems: 'center', |
| | | justifyContent: 'space-between', |
| | | transition: 'transform 0.2s ease, box-shadow 0.2s ease', |
| | | boxShadow: |
| | | themeMode === 'light' |
| | | ? '0 6px 12px rgba(0,0,0,0.06)' |
| | | : '0 6px 12px rgba(0,0,0,0.25)', |
| | | '&:hover': { |
| | | transform: 'translateY(-2px)', |
| | | boxShadow: |
| | | themeMode === 'light' |
| | | ? '0 12px 24px rgba(0,0,0,0.08)' |
| | | : '0 12px 24px rgba(0,0,0,0.35)', |
| | | }, |
| | | }} |
| | | > |
| | | <Box |
| | | sx={{ |
| | | width: 12, |
| | | height: 12, |
| | | borderRadius: '50%', |
| | | bgcolor: area.color ? Number(area.color) : theme.palette.info.light, |
| | | border: `1px solid ${theme.palette.common.white}`, |
| | | boxShadow: '0 0 4px rgba(0,0,0,0.2)', |
| | | }} |
| | | /> |
| | | <ListItemText |
| | | primary={area.name || translate('page.map.area.form.name')} |
| | | primaryTypographyProps={{ |
| | | fontWeight: 600, |
| | | fontSize: 16, |
| | | }} |
| | | /> |
| | | <Box textAlign="right"> |
| | | <Typography variant="body2" fontWeight={500}> |
| | | {translate('page.map.area.form.agvCount', { count: agvCount })} |
| | | </Typography> |
| | | <Typography variant="caption" color="text.secondary"> |
| | | {translate('page.map.area.form.areaSize', { size: formattedSize })} |
| | | </Typography> |
| | | </Box> |
| | | </ListItemButton> |
| | | ); |
| | | })} |
| | | </List> |
| | | )} |
| | | </Box> |
| | | </Box> |
| | | )} |
| | | </Drawer> |
| | | ); |
| | | }; |
| | | |
| | | export default AreaList; |
| | |
| | | import { NotificationProvider, useNotification } from './Notification'; |
| | | import Insight from "./insight"; |
| | | import Device from "./Device"; |
| | | import AreaList from "./AreaList"; |
| | | import Settings from "./settings"; |
| | | import Batch from "./batch"; |
| | | import AreaSettings from "./areaSettings"; |
| | |
| | | import PulseSignal from "../page/components/PulseSignal"; |
| | | import FakeFab from "./header/FakeFab"; |
| | | import RouteFab from "./header/RouteFab"; |
| | | import AreaFab from "./header/AreaFab"; |
| | | import MoreOperate from "./header/MoreOperate"; |
| | | |
| | | let player; |
| | |
| | | const [settingsVisible, setSettingsVisible] = useState(false); |
| | | const [batchSelectionVisible, setBatchSelectionVisible] = useState(false); |
| | | const [areaSettingsVisible, setAreaSettingsVisible] = useState(false); |
| | | const [areaListVisible, setAreaListVisible] = useState(false); |
| | | const [areaDrawing, setAreaDrawing] = useState(false); |
| | | |
| | | const [curSprite, setCurSprite] = useState(null); |
| | |
| | | |
| | | const [rcsStatus, setRcsStatus] = useState(null); |
| | | const [showRoutes, setShowRoutes] = useState(false); |
| | | const [showAreas, setShowAreas] = useState(false); |
| | | const [curZone, setCurZone] = useState(() => { |
| | | const storedValue = localStorage.getItem('curZone'); |
| | | return storedValue !== null ? JSON.parse(storedValue) : null; |
| | |
| | | setBatchSelectionVisible(false); |
| | | setAreaSettingsVisible(false); |
| | | setAreaDrawing(false); |
| | | setAreaListVisible(false); |
| | | Tool.cancelAreaDrawing(); |
| | | Tool.hideAreas(curZone, setShowAreas); |
| | | |
| | | setCurSprite(null); |
| | | setBatchSprites([]); |
| | |
| | | if (!mapContainer) { |
| | | return; |
| | | } |
| | | setAreaListVisible(false); |
| | | Tool.removeSelectedEffect(); |
| | | if (curSprite) { |
| | | if (mode === MAP_MODE.OBSERVER_MODE) { |
| | |
| | | {mode === MAP_MODE.AREA_MODE && ( |
| | | <> |
| | | <Button |
| | | variant={areaDrawing ? "outlined" : "contained"} |
| | | color="primary" |
| | | sx={{}} |
| | | disabled={areaDrawing} |
| | | variant="outlined" |
| | | onClick={() => setAreaListVisible(!areaListVisible)} |
| | | sx={{ mr: 2 }} |
| | | > |
| | | {translate('page.map.action.areaList')} |
| | | </Button> |
| | | <Button |
| | | variant="contained" |
| | | color={areaDrawing ? "error" : "primary"} |
| | | onClick={() => { |
| | | if (areaDrawing) { |
| | | Tool.cancelAreaDrawing(); |
| | | setAreaDrawing(false); |
| | | return; |
| | | } |
| | | setCurSprite(null); |
| | | const started = Tool.startAreaDrawing({ |
| | | promptText: translate('page.map.prompt.areaName'), |
| | | promptText: translate('page.map.area.prompt.nameInput'), |
| | | onComplete: ({ name, start, end, color, graphics }) => { |
| | | if (name) { |
| | | Http.saveAreaData(curZone, { name, start, end, color }).then((savedArea) => { |
| | |
| | | } |
| | | }} |
| | | > |
| | | {translate('page.map.action.addArea')} |
| | | {areaDrawing |
| | | ? translate('page.map.action.cancelAddArea') |
| | | : translate('page.map.action.addArea')} |
| | | </Button> |
| | | </> |
| | | )} |
| | |
| | | gap: 2 |
| | | }} |
| | | > |
| | | {mode === MAP_MODE.OBSERVER_MODE && ( |
| | | <> |
| | | <AreaFab |
| | | curZone={curZone} |
| | | showAreas={showAreas} |
| | | setShowAreas={setShowAreas} |
| | | notify={notify} |
| | | /> |
| | | </> |
| | | )} |
| | | {mode !== MAP_MODE.MOVABLE_MODE && ( |
| | | <> |
| | | <RouteFab |
| | |
| | | width={570} |
| | | /> |
| | | |
| | | <AreaList |
| | | zoneId={curZone} |
| | | open={areaListVisible} |
| | | onClose={() => setAreaListVisible(false)} |
| | | setCurSprite={setCurSprite} |
| | | width={378} |
| | | /> |
| | | |
| | | </Box> |
| | | ); |
| | | } |
| | |
| | | setMaxQty, |
| | | speedLimit, |
| | | setSpeedLimit, |
| | | shapeData, |
| | | setShapeData, |
| | | startPoint, |
| | | endPoint, |
| | | memo, |
| | | setMemo, |
| | | priority, |
| | | setPriority, |
| | | onSave, |
| | | }) => { |
| | | const translate = useTranslate(); |
| | | const formatCoord = (value) => { |
| | | if (value === null || value === undefined || value === '') { |
| | | return ''; |
| | | } |
| | | const num = Number(value); |
| | | if (Number.isNaN(num)) { |
| | | return String(value); |
| | | } |
| | | return num.toFixed(2); |
| | | }; |
| | | |
| | | const handleSubmit = (e) => { |
| | | e.preventDefault(); |
| | | const confirmMsg = translate('page.map.area.confirm.save'); |
| | | if (window.confirm(confirmMsg)) { |
| | | onSave?.(); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <Box component="form" onSubmit={(e) => { e.preventDefault(); onSave(); }}> |
| | | <Box component="form" onSubmit={handleSubmit}> |
| | | <Grid container spacing={2}> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label={translate('page.map.area.code', { _: '区域编码' })} |
| | | label={translate('page.map.area.form.code')} |
| | | fullWidth |
| | | value={areaCode} |
| | | onChange={(e) => setAreaCode(e.target.value)} |
| | |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <TextField |
| | | label={translate('page.map.area.maxQty', { _: '最大数量' })} |
| | | label={translate('page.map.area.form.maxQty')} |
| | | fullWidth |
| | | type="number" |
| | | value={maxQty} |
| | |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <TextField |
| | | label={translate('page.map.area.speedLimit', { _: '速度限制' })} |
| | | label={translate('page.map.area.form.speedLimit')} |
| | | fullWidth |
| | | type="number" |
| | | value={speedLimit} |
| | | onChange={(e) => setSpeedLimit(e.target.value)} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <Grid item xs={12} sm={6}> |
| | | <TextField |
| | | label={translate('page.map.area.shape', { _: '形状数据' })} |
| | | label={translate('page.map.area.form.startX')} |
| | | fullWidth |
| | | multiline |
| | | minRows={3} |
| | | value={shapeData} |
| | | onChange={(e) => setShapeData(e.target.value)} |
| | | value={formatCoord(startPoint?.x)} |
| | | InputProps={{ readOnly: true }} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12} sm={6}> |
| | | <TextField |
| | | label={translate('page.map.area.form.startY')} |
| | | fullWidth |
| | | value={formatCoord(startPoint?.y)} |
| | | InputProps={{ readOnly: true }} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12} sm={6}> |
| | | <TextField |
| | | label={translate('page.map.area.form.endX')} |
| | | fullWidth |
| | | value={formatCoord(endPoint?.x)} |
| | | InputProps={{ readOnly: true }} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12} sm={6}> |
| | | <TextField |
| | | label={translate('page.map.area.form.endY')} |
| | | fullWidth |
| | | value={formatCoord(endPoint?.y)} |
| | | InputProps={{ readOnly: true }} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label={translate('page.map.area.priority', { _: '优先级' })} |
| | | label={translate('page.map.area.form.priority')} |
| | | fullWidth |
| | | type="number" |
| | | value={priority} |
| | |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label={translate('page.map.area.form.memo')} |
| | | fullWidth |
| | | multiline |
| | | minRows={3} |
| | | value={memo} |
| | | onChange={(e) => setMemo(e.target.value)} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <Button variant="contained" type="submit"> |
| | | {translate('common.action.save', { _: '保存' })} |
| | | {translate('ra.action.save')} |
| | | </Button> |
| | | </Grid> |
| | | </Grid> |
| | |
| | | if (disableSave) { |
| | | return; |
| | | } |
| | | const confirmMsg = translate('page.map.area.saveConfirm', { _: '确认保存当前修改?' }); |
| | | const confirmMsg = translate('page.map.area.confirm.save'); |
| | | if (window.confirm(confirmMsg)) { |
| | | onSave?.(); |
| | | } |
| | |
| | | if (!canDelete) { |
| | | return; |
| | | } |
| | | const confirmMsg = translate('page.map.area.deleteConfirm', { _: '删除后将无法恢复,确认删除?' }); |
| | | const confirmMsg = translate('page.map.area.confirm.delete'); |
| | | if (window.confirm(confirmMsg)) { |
| | | onDelete?.(); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <Stack spacing={3}> |
| | | <Stack spacing={3} sx={{ mt: 1 }}> |
| | | <Stack direction="row" spacing={1} alignItems="center"> |
| | | <TextField |
| | | label={translate('page.map.area.name', { _: '名称' })} |
| | | label={translate('page.map.area.form.name')} |
| | | size="small" |
| | | fullWidth |
| | | variant="outlined" |
| | |
| | | |
| | | <Box > |
| | | <Typography variant="subtitle2" gutterBottom> |
| | | {translate('page.map.area.agv', { _: '选择AGV小车' })} |
| | | {translate('page.map.area.form.agv')} |
| | | </Typography> |
| | | <Autocomplete |
| | | multiple |
| | |
| | | {...params} |
| | | size="small" |
| | | variant="outlined" |
| | | placeholder={translate('page.map.area.agv.placeholder', { _: '' })} |
| | | placeholder={translate('page.map.area.form.agvPlaceholder')} |
| | | /> |
| | | )} |
| | | /> |
| | |
| | | |
| | | <Box> |
| | | <Typography variant="subtitle2" gutterBottom> |
| | | {translate('page.map.area.barcodes', { _: '区域内条码集合' }) + " (" + codeList.length + ")"} |
| | | {translate('page.map.area.form.codes', { count: codeList.length })} |
| | | </Typography> |
| | | <Paper |
| | | variant="outlined" |
| | |
| | | )) |
| | | ) : ( |
| | | <Typography variant="body2" color="text.secondary"> |
| | | {translate('page.map.area.barcodes.empty', { _: '暂无条码' })} |
| | | {translate('page.map.area.form.codesEmpty')} |
| | | </Typography> |
| | | )} |
| | | </Paper> |
| | |
| | | |
| | | <Box display="flex" justifyContent="space-between" alignItems="center"> |
| | | <Button variant="contained" onClick={handleSaveClick} disabled={disableSave}> |
| | | {translate('common.action.save', { _: '保存' })} |
| | | {translate('ra.action.save')} |
| | | </Button> |
| | | <Button variant="text" color="error" onClick={handleDeleteClick} disabled={!canDelete}> |
| | | {translate('common.action.delete', { _: '删除' })} |
| | | {translate('ra.action.delete')} |
| | | </Button> |
| | | </Box> |
| | | </Stack> |
| | |
| | | const [code, setCode] = useState(''); |
| | | const [maxCount, setMaxCount] = useState(''); |
| | | const [speedLimit, setSpeedLimit] = useState(''); |
| | | const [memo, setMemo] = useState(''); |
| | | const [startPoint, setStartPoint] = useState({ x: '', y: '' }); |
| | | const [endPoint, setEndPoint] = useState({ x: '', y: '' }); |
| | | const [priority, setPriority] = useState(''); |
| | | const [memo, setMemo] = useState(''); |
| | | const [agvOptions, setAgvOptions] = useState([]); |
| | | const [initialBasic, setInitialBasic] = useState({ name: '', agvIds: [] }); |
| | | const [curAreaInfo, setCurAreaInfo] = useState(null); |
| | |
| | | setCode(''); |
| | | setMaxCount(''); |
| | | setSpeedLimit(''); |
| | | setMemo(''); |
| | | setStartPoint({ x: '', y: '' }); |
| | | setEndPoint({ x: '', y: '' }); |
| | | setPriority(''); |
| | | setMemo(''); |
| | | setAgvList([]); |
| | | setCodeList([]); |
| | | setInitialBasic({ name: '', agvIds: [] }); |
| | |
| | | setCode(curAreaInfo.code || ''); |
| | | setMaxCount(curAreaInfo.maxCount ?? ''); |
| | | setSpeedLimit(curAreaInfo.speedLimit ?? ''); |
| | | setMemo(curAreaInfo.memo || ''); |
| | | setStartPoint({ |
| | | x: curAreaInfo.start?.x ?? '', |
| | | y: curAreaInfo.start?.y ?? '', |
| | | }); |
| | | setEndPoint({ |
| | | x: curAreaInfo.end?.x ?? '', |
| | | y: curAreaInfo.end?.y ?? '', |
| | | }); |
| | | setPriority(curAreaInfo.priority ?? ''); |
| | | setMemo(curAreaInfo.memo || ''); |
| | | |
| | | const selected = curAreaInfo.agvList || []; |
| | | const normalizedSelection = mapSelectionToOptions(selected, agvOptions); |
| | |
| | | setActiveTab(newValue); |
| | | }; |
| | | |
| | | const handleSaveBasic = async () => { |
| | | const submitAreaUpdate = async (payload = {}) => { |
| | | const id = sprite?.data?.id; |
| | | if (!id) { |
| | | return; |
| | | } |
| | | const payload = { |
| | | id, |
| | | name, |
| | | agvIds: agvList.map(getAgvOptionId), |
| | | }; |
| | | const data = await updateAreaData(payload); |
| | | const data = await updateAreaData({ id, ...payload }); |
| | | if (data) { |
| | | setCurAreaInfo(data); |
| | | if (sprite) { |
| | | Tool.updateAreaSpriteName(sprite, data.name || name); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const handleSaveBasic = async () => { |
| | | await submitAreaUpdate({ |
| | | name, |
| | | agvIds: agvList.map(getAgvOptionId), |
| | | }); |
| | | }; |
| | | |
| | | const handleSaveAdvanced = async () => { |
| | | await submitAreaUpdate({ |
| | | name, |
| | | agvIds: agvList.map(getAgvOptionId), |
| | | code, |
| | | maxCount, |
| | | speedLimit, |
| | | priority, |
| | | memo, |
| | | start: startPoint, |
| | | end: endPoint, |
| | | }); |
| | | }; |
| | | |
| | | const handleDeleteArea = async () => { |
| | |
| | | } |
| | | onCancel?.(); |
| | | } |
| | | }; |
| | | |
| | | const handleSaveAdvanced = () => { |
| | | // placeholder for save logic |
| | | }; |
| | | |
| | | const basicDirty = name !== initialBasic.name |
| | |
| | | <Typography variant="h6" flex="1"> |
| | | {sprite |
| | | ? translate(`page.map.devices.${sprite?.data?.type?.toLowerCase()}`) + ' - ' + sprite?.data?.name |
| | | : translate('page.map.settings.title')} |
| | | : translate('page.map.area.title')} |
| | | </Typography> |
| | | <IconButton onClick={handleClose} size="small"> |
| | | <CloseIcon /> |
| | |
| | | variant="fullWidth" |
| | | sx={{ mb: 0 }} |
| | | > |
| | | <Tab label={translate('page.map.area.basic', { _: '基础' })} /> |
| | | <Tab label={translate('page.map.area.advanced', { _: '高级' })} /> |
| | | <Tab label={translate('page.map.area.tabs.basic')} /> |
| | | <Tab label={translate('page.map.area.tabs.advanced')} /> |
| | | </Tabs> |
| | | |
| | | <Divider /> |
| | |
| | | setMaxQty={setMaxCount} |
| | | speedLimit={speedLimit} |
| | | setSpeedLimit={setSpeedLimit} |
| | | shapeData={memo} |
| | | setShapeData={setMemo} |
| | | startPoint={startPoint} |
| | | endPoint={endPoint} |
| | | priority={priority} |
| | | setPriority={setPriority} |
| | | memo={memo} |
| | | setMemo={setMemo} |
| | | onSave={handleSaveAdvanced} |
| | | /> |
| | | )} |
| New file |
| | |
| | | import React from "react"; |
| | | import { useTranslate } from "react-admin"; |
| | | import { Fab } from '@mui/material'; |
| | | import CropFreeIcon from '@mui/icons-material/CropFree'; |
| | | import CropIcon from '@mui/icons-material/Crop'; |
| | | import CircularProgress from '@mui/material/CircularProgress'; |
| | | import * as Tool from '../tool'; |
| | | |
| | | const AreaFab = (props) => { |
| | | const { curZone, showAreas, setShowAreas } = props; |
| | | const translate = useTranslate(); |
| | | |
| | | const [loading, setLoading] = React.useState(false); |
| | | |
| | | const handleClick = () => { |
| | | if (showAreas) { |
| | | Tool.hideAreas(curZone, setShowAreas); |
| | | } else { |
| | | Tool.showAreas(curZone, setShowAreas, setLoading); |
| | | } |
| | | } |
| | | |
| | | return ( |
| | | <> |
| | | <Fab |
| | | variant="extended" |
| | | color={showAreas ? 'primary' : 'default'} |
| | | size="small" |
| | | disabled={loading} |
| | | onClick={handleClick} |
| | | sx={{ |
| | | minWidth: 100 |
| | | }} |
| | | > |
| | | <CropIcon /> |
| | | {translate('page.map.action.area')} |
| | | {loading && ( |
| | | <> |
| | | <svg width={0} height={0}> |
| | | <defs> |
| | | <linearGradient id="my_gradient" x1="0%" y1="0%" x2="0%" y2="100%"> |
| | | <stop offset="0%" stopColor="#e01cd5" /> |
| | | <stop offset="100%" stopColor="#1CB5E0" /> |
| | | </linearGradient> |
| | | </defs> |
| | | </svg> |
| | | <CircularProgress size={18} thickness={8} sx={{ 'svg circle': { stroke: 'url(#my_gradient)' } }} /> |
| | | </> |
| | | )} |
| | | </Fab> |
| | | </> |
| | | ) |
| | | } |
| | | |
| | | export default AreaFab; |
| | |
| | | size="small" |
| | | disabled={loading} |
| | | onClick={handleClick} |
| | | sx={{ |
| | | minWidth: 100 |
| | | }} |
| | | > |
| | | <AltRouteIcon /> |
| | | {translate('page.map.action.route')} |
| New file |
| | |
| | | import React, { useState, useEffect, useMemo } from 'react'; |
| | | import { useTranslate } from "react-admin"; |
| | | import { |
| | | Box, |
| | | Tabs, |
| | | Tab, |
| | | Divider, |
| | | List, |
| | | ListItemButton, |
| | | ListItemText, |
| | | Typography, |
| | | Stack, |
| | | } from '@mui/material'; |
| | | import JsonShow from '../../JsonShow'; |
| | | import { fetchAreaList } from '../../http'; |
| | | |
| | | const AreaInsight = ({ curZone, setTitle }) => { |
| | | const translate = useTranslate(); |
| | | const [areas, setAreas] = useState([]); |
| | | const [selectedId, setSelectedId] = useState(null); |
| | | const [activeTab, setActiveTab] = useState(0); |
| | | const formatCoord = (value) => { |
| | | if (value === null || value === undefined) { |
| | | return '-'; |
| | | } |
| | | if (typeof value === 'number') { |
| | | return value.toFixed(2); |
| | | } |
| | | const num = Number(value); |
| | | if (!Number.isNaN(num)) { |
| | | return num.toFixed(2); |
| | | } |
| | | return String(value); |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | let mounted = true; |
| | | if (!curZone) { |
| | | setAreas([]); |
| | | setSelectedId(null); |
| | | return; |
| | | } |
| | | fetchAreaList(curZone).then((list) => { |
| | | if (!mounted) { |
| | | return; |
| | | } |
| | | const nextList = Array.isArray(list) ? list : []; |
| | | setAreas(nextList); |
| | | setSelectedId((prev) => { |
| | | if (prev) { |
| | | const exists = nextList.some((item) => item?.id === prev); |
| | | if (exists) { |
| | | return prev; |
| | | } |
| | | } |
| | | return nextList[0]?.id ?? null; |
| | | }); |
| | | }); |
| | | return () => { |
| | | mounted = false; |
| | | }; |
| | | }, [curZone]); |
| | | |
| | | const selectedArea = useMemo( |
| | | () => areas.find((area) => area?.id === selectedId) || null, |
| | | [areas, selectedId] |
| | | ); |
| | | |
| | | useEffect(() => { |
| | | if (setTitle) { |
| | | const label = selectedArea |
| | | ? `${translate('page.map.devices.area')} - ${selectedArea.name || selectedArea.id}` |
| | | : translate('page.map.devices.area'); |
| | | setTitle(label); |
| | | } |
| | | }, [selectedArea, setTitle, translate]); |
| | | |
| | | useEffect(() => { |
| | | return () => { |
| | | if (setTitle) { |
| | | setTitle(null); |
| | | } |
| | | }; |
| | | }, [setTitle]); |
| | | |
| | | const handleTabChange = (_event, newValue) => { |
| | | setActiveTab(newValue); |
| | | }; |
| | | |
| | | const detailPairs = [ |
| | | { label: translate('page.map.area.form.name'), value: selectedArea?.name }, |
| | | { label: translate('page.map.area.form.code'), value: selectedArea?.code }, |
| | | { label: translate('page.map.area.form.maxQty'), value: selectedArea?.maxCount }, |
| | | { label: translate('page.map.area.form.speedLimit'), value: selectedArea?.speedLimit }, |
| | | { label: translate('page.map.area.form.priority'), value: selectedArea?.priority }, |
| | | ]; |
| | | |
| | | return ( |
| | | <Box sx={{ height: '100%', display: 'flex', gap: 2 }}> |
| | | <Box width={220} sx={{ borderRight: 1, borderColor: 'divider', overflowY: 'auto' }}> |
| | | <List dense disablePadding> |
| | | {areas.map((area) => ( |
| | | <ListItemButton |
| | | key={area.id} |
| | | selected={area.id === selectedId} |
| | | onClick={() => setSelectedId(area.id)} |
| | | > |
| | | <ListItemText |
| | | primary={area.name || `#${area.id}`} |
| | | secondary={area.code} |
| | | /> |
| | | </ListItemButton> |
| | | ))} |
| | | {areas.length === 0 && ( |
| | | <Typography variant="body2" color="text.secondary" p={2}> |
| | | {translate('page.map.area.form.codesEmpty')} |
| | | </Typography> |
| | | )} |
| | | </List> |
| | | </Box> |
| | | |
| | | <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}> |
| | | <Tabs |
| | | value={activeTab} |
| | | onChange={handleTabChange} |
| | | sx={{ mb: 0 }} |
| | | > |
| | | <Tab label={translate('page.map.insight.title')} /> |
| | | <Tab label="JSON" /> |
| | | </Tabs> |
| | | |
| | | <Divider /> |
| | | |
| | | <Box flex={1} pt={2}> |
| | | {activeTab === 0 && ( |
| | | <Stack spacing={1}> |
| | | {detailPairs.map(({ label, value }) => ( |
| | | <Stack key={label} direction="row" spacing={1}> |
| | | <Typography variant="body2" color="text.secondary" minWidth={120}> |
| | | {label}: |
| | | </Typography> |
| | | <Typography variant="body2"> |
| | | {value != null && value !== '' ? value : '-'} |
| | | </Typography> |
| | | </Stack> |
| | | ))} |
| | | {selectedArea?.start && selectedArea?.end && ( |
| | | <> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | {translate('page.map.area.form.startX')}/{translate('page.map.area.form.startY')}: |
| | | </Typography> |
| | | <Typography variant="body2"> |
| | | {`${formatCoord(selectedArea.start.x)} , ${formatCoord(selectedArea.start.y)}`} |
| | | </Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | {translate('page.map.area.form.endX')}/{translate('page.map.area.form.endY')}: |
| | | </Typography> |
| | | <Typography variant="body2"> |
| | | {`${formatCoord(selectedArea.end.x)} , ${formatCoord(selectedArea.end.y)}`} |
| | | </Typography> |
| | | </> |
| | | )} |
| | | {(selectedArea?.codeList?.length ?? 0) > 0 && ( |
| | | <Typography variant="body2"> |
| | | {translate('page.map.area.form.codes', { count: selectedArea.codeList.length })} |
| | | </Typography> |
| | | )} |
| | | </Stack> |
| | | )} |
| | | {activeTab === 1 && ( |
| | | <JsonShow |
| | | data={selectedArea || {}} |
| | | height={550} |
| | | /> |
| | | )} |
| | | </Box> |
| | | </Box> |
| | | </Box> |
| | | ); |
| | | }; |
| | | |
| | | export default AreaInsight; |
| | |
| | | import * as TWEEDLE from 'tweedle.js'; |
| | | import * as Tool from './tool'; |
| | | import star from '/img/map/star.png'; |
| | | import { MAP_MIRROR } from './constants'; |
| | | import { DEVICE_TYPE, MAP_MIRROR } from './constants'; |
| | | |
| | | export default class Player { |
| | | |
| | |
| | | |
| | | // sprite show style which be selected |
| | | this.mapContainer.children.forEach(child => { |
| | | if (child?.data?.type === DEVICE_TYPE.AREA) { |
| | | return; |
| | | } |
| | | if (Tool.isSpriteInSelectionBox(child, this.selectionBox)) { |
| | | this.selectedSprites.push(child); |
| | | Tool.markSprite(child); |
| | |
| | | .start(); |
| | | } |
| | | |
| | | export const findAreaSpriteById = (areaId) => { |
| | | if (!mapContainer || areaId == null) { |
| | | return null; |
| | | } |
| | | const targetId = String(areaId); |
| | | for (let i = 0; i < mapContainer.children.length; i += 1) { |
| | | const child = mapContainer.children[i]; |
| | | if (child?.data?.type === DEVICE_TYPE.AREA && String(child.data?.id) === targetId) { |
| | | return child; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | export const clearMapData = () => { |
| | | if (!mapContainer) { |
| | | return; |
| | |
| | | const addAreaLabel = (draft, text, from, to) => { |
| | | const centerX = (from.x + to.x) / 2; |
| | | const centerY = (from.y + to.y) / 2; |
| | | const label = new PIXI.Text(text, { |
| | | fill: themeMode === 'dark' ? '#f1f2f6' : '#535353ff', |
| | | fontSize: 20 / Math.abs(mapContainer.scale.x || 1), |
| | | fontWeight: 'bold', |
| | | const currentScale = Math.abs(mapContainer.scale.x || 1); |
| | | const labelStyle = new PIXI.TextStyle({ |
| | | fontFamily: 'Inter, "Segoe UI", sans-serif', |
| | | fill: themeMode === 'dark' ? '#f1f2f6' : '#606060ff', |
| | | fontSize: Math.max(16, 20 / currentScale), |
| | | fontWeight: 600, |
| | | letterSpacing: 10, |
| | | // stroke: themeMode === 'dark' ? '#1e272e' : '#ffffff', |
| | | // strokeThickness: Math.max(1, 2 / currentScale), |
| | | // dropShadow: true, |
| | | // dropShadowColor: themeMode === 'dark' ? '#00000066' : '#95a5a6', |
| | | // dropShadowBlur: 1.5, |
| | | // dropShadowAngle: Math.PI / 4, |
| | | // dropShadowDistance: 2, |
| | | }); |
| | | const label = new PIXI.Text(text ?? '', labelStyle); |
| | | label.anchor.set(0.5); |
| | | label.position.set(centerX, centerY); |
| | | label.rotation = rotationParseNum(MAP_DEFAULT_ROTATION); |
| | |
| | | sprite.destroy({ children: true, texture: false, baseTexture: false }); |
| | | }; |
| | | |
| | | export const loadAreas = (curZone, setCurSprite) => { |
| | | export const loadAreas = (curZone, setCurSprite, callback) => { |
| | | if (!mapContainer) return; |
| | | clearAreas(); |
| | | fetchAreaList(curZone).then((areas) => { |
| | |
| | | mapContainer.addChild(graphics); |
| | | } |
| | | }); |
| | | if (callback) { |
| | | callback(areas); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | |
| | | |
| | | const draft = new PIXI.Graphics(); |
| | | draft.name = id ? `area_${id}` : 'area_' + generateID(); |
| | | draft.zIndex = DEVICE_Z_INDEX.AREA; |
| | | draft.lineStyle(2 / Math.abs(mapContainer.scale.x || 1), AREA_BORDER_COLOR, 0.9); |
| | | draft.zIndex = 0; |
| | | if (setCurSprite) { |
| | | draft.zIndex = DEVICE_Z_INDEX.AREA; |
| | | } |
| | | draft.lineStyle(1 / Math.abs(mapContainer.scale.x || 1), AREA_BORDER_COLOR, 0.9); |
| | | draft.beginFill(areaColor, 0.18); |
| | | draft.drawRect( |
| | | Math.min(from.x, to.x), |
| | |
| | | draft.endFill(); |
| | | addAreaLabel(draft, name, from, to); |
| | | draft.data = { ...(draft.data || {}), type: DEVICE_TYPE.AREA, name, color: areaColor, id, start: from, end: to }; |
| | | draft.eventMode = 'static'; |
| | | draft.cursor = 'pointer'; |
| | | if (setCurSprite) { |
| | | draft.cursor = 'pointer'; |
| | | draft.eventMode = 'static'; |
| | | } |
| | | if (setCurSprite) { |
| | | draft.off('click'); |
| | | draft.on('click', () => setCurSprite(draft)); |
| | |
| | | areaDrawingCleanup(); |
| | | } |
| | | }; |
| | | |
| | | export const showAreas = (curZone, setShowAreas, setLoading) => { |
| | | setLoading(true); |
| | | loadAreas(curZone, null, (areas) => { |
| | | setLoading(false); |
| | | setShowAreas(true); |
| | | }) |
| | | }; |
| | | |
| | | export const hideAreas = (curZone, setShowAreas) => { |
| | | clearAreas(); |
| | | setShowAreas(false); |
| | | }; |
| | | |
| | | export const focusAreaSprite = (sprite, rightPanelWidth) => { |
| | | if (!sprite || !app || !mapContainer) { |
| | | return; |
| | | } |
| | | |
| | | const data = sprite.data || {}; |
| | | const start = data.start; |
| | | const end = data.end; |
| | | const currentScale = Math.abs(mapContainer.scale.x || 1); |
| | | const boundsBefore = sprite.getBounds(); |
| | | const width = start && end ? Math.abs(end.x - start.x) : boundsBefore.width / currentScale; |
| | | const height = start && end ? Math.abs(end.y - start.y) : boundsBefore.height / currentScale; |
| | | |
| | | const paddedWidth = (width || 1000) * 1.25; |
| | | const paddedHeight = (height || 1000) * 1.25; |
| | | const viewportWidth = app.renderer.width || 1920; |
| | | const viewportHeight = app.renderer.height || 1080; |
| | | const effectiveViewportWidth = Math.max(1, viewportWidth - rightPanelWidth); |
| | | |
| | | let focusScale = Math.min( |
| | | (effectiveViewportWidth * 0.65) / paddedWidth, |
| | | (viewportHeight * 0.65) / paddedHeight |
| | | ); |
| | | focusScale = Math.min(Math.max(focusScale, 0.03), 0.25); |
| | | |
| | | mapContainer.scale.set(MAP_MIRROR ? -focusScale : focusScale, focusScale); |
| | | mapContainer.position.set(0, 0); |
| | | |
| | | const bounds = sprite.getBounds(); |
| | | const centerX = bounds.x + bounds.width / 2; |
| | | const centerY = bounds.y + bounds.height / 2; |
| | | const visibleCenterX = (viewportWidth - rightPanelWidth) / 2; |
| | | |
| | | const targetPos = { |
| | | x: visibleCenterX - centerX, |
| | | y: viewportHeight / 2 - centerY, |
| | | }; |
| | | |
| | | new TWEEDLE.Tween(mapContainer.position) |
| | | .easing(TWEEDLE.Easing.Quadratic.Out) |
| | | .to(targetPos, 500) |
| | | .start(); |
| | | }; |
| | |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.util.Date; |
| | | import java.util.Objects; |
| | | |
| | | @Slf4j |
| | | @Service("areaService") |
| | |
| | | |
| | | // area |
| | | boolean needModify = false; |
| | | if (!Cools.isEmpty(param.getName()) && !area.getName().equals(param.getName())) { |
| | | if (param.getName() != null && !Objects.equals(area.getName(), param.getName())) { |
| | | area.setName(param.getName()); |
| | | needModify = true; |
| | | } |
| | | if (param.getCode() != null && !Objects.equals(area.getCode(), param.getCode())) { |
| | | area.setCode(param.getCode()); |
| | | needModify = true; |
| | | } |
| | | if (param.getMaxCount() != null && !Objects.equals(area.getMaxCount(), param.getMaxCount())) { |
| | | area.setMaxCount(param.getMaxCount()); |
| | | needModify = true; |
| | | } |
| | | if (param.getSpeedLimit() != null && !Objects.equals(area.getSpeedLimit(), param.getSpeedLimit())) { |
| | | area.setSpeedLimit(param.getSpeedLimit()); |
| | | needModify = true; |
| | | } |
| | | if (param.getPriority() != null && !Objects.equals(area.getPriority(), param.getPriority())) { |
| | | area.setPriority(param.getPriority()); |
| | | needModify = true; |
| | | } |
| | | if (param.getMemo() != null && !Objects.equals(area.getMemo(), param.getMemo())) { |
| | | area.setMemo(param.getMemo()); |
| | | needModify = true; |
| | | } |
| | | |
| | | if (needModify) { |
| | | area.setUpdateTime(new Date()); |