920bb5635c88c2f2f9a21134c81ebbc344539987..c3cff07f14703caa88ae92e06c57395d4ce5d01d
2025-12-17 vincentlu
#
c3cff0 对比 | 目录
2025-12-17 vincentlu
#
e94c4a 对比 | 目录
2025-12-17 vincentlu
#
3c1abe 对比 | 目录
2025-12-17 vincentlu
#
d6dbab 对比 | 目录
2025-12-17 vincentlu
#
5ecfeb 对比 | 目录
2025-12-17 vincentlu
#
25abc0 对比 | 目录
2025-12-17 vincentlu
#
baaf4d 对比 | 目录
2025-12-17 vincentlu
#
b8d973 对比 | 目录
2025-12-17 vincentlu
#
adcd2e 对比 | 目录
2025-12-17 vincentlu
#
3e184e 对比 | 目录
2025-12-17 vincentlu
#
1e99f0 对比 | 目录
2025-12-17 vincentlu
#
81825c 对比 | 目录
2025-12-17 vincentlu
#
bc7a57 对比 | 目录
2025-12-17 vincentlu
#
e10544 对比 | 目录
2025-12-17 vincentlu
#
af519f 对比 | 目录
2025-12-17 vincentlu
#
398fde 对比 | 目录
2025-12-17 vincentlu
#
e40bee 对比 | 目录
2025-12-17 vincentlu
#
b5019d 对比 | 目录
2025-12-17 vincentlu
#
f02bb4 对比 | 目录
2025-12-17 vincentlu
#
ed1359 对比 | 目录
2025-12-17 vincentlu
#
14bd9f 对比 | 目录
2025-12-17 vincentlu
#
8c1daa 对比 | 目录
2025-12-17 vincentlu
#
43c6b1 对比 | 目录
2025-12-17 vincentlu
#
c72117 对比 | 目录
2025-12-17 vincentlu
#
c917d8 对比 | 目录
2025-12-17 vincentlu
#
1ef2fe 对比 | 目录
2025-12-17 vincentlu
#
421d64 对比 | 目录
2025-12-17 vincentlu
#
bd2a6a 对比 | 目录
3个文件已添加
12个文件已修改
859 ■■■■■ 已修改文件
zy-acs-flow/src/i18n/core/enMap.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/i18n/core/zhMap.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/i18n/en.js 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/i18n/zh.js 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/AreaList.jsx 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/MapPage.jsx 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/areaSettings/AreaAdvancedTab.jsx 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/areaSettings/AreaBasicTab.jsx 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/areaSettings/index.jsx 64 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/header/AreaFab.jsx 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/header/RouteFab.jsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/insight/area/index.jsx 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/player.js 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/map/tool.js 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-manager/src/main/java/com/zy/acs/manager/manager/service/impl/AreaServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/i18n/core/enMap.js
@@ -1,15 +1,6 @@
const enMap = {
    page: {
        map: {
            action: {
                addArea: 'Add Area',
            },
            prompt: {
                areaName: 'Please enter area name',
            },
            msg: {
                areaCreated: 'Area "%{name}" created',
            },
        },
    },
}
zy-acs-flow/src/i18n/core/zhMap.js
@@ -1,15 +1,6 @@
const zhMap = {
    page: {
        map: {
            action: {
                addArea: '添加区域',
            },
            prompt: {
                areaName: '请输入区域名称',
            },
            msg: {
                areaCreated: '区域 "%{name}" 已创建',
            },
        },
    },
}
zy-acs-flow/src/i18n/en.js
@@ -664,6 +664,7 @@
                flip: 'FLIP',
                fake: 'FAKE',
                route: 'ROUTE',
                area: 'AREA',
                disable: 'DISABLE',
                enable: 'ENABLE',
                reset: 'RESET',
@@ -675,6 +676,8 @@
                oneClickPatrol: 'One-click Patrol',
                cancelPatrol: 'Cancel Patrol',
                addArea: 'Add Area',
                cancelAddArea: 'Cancel Add',
                areaList: 'Area List',
            },
            mode: {
                observer: 'OBSERVER',
@@ -757,6 +760,42 @@
                    },
                },
            },
            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',
                },
            },
        },
    }
};
zy-acs-flow/src/i18n/zh.js
@@ -659,11 +659,12 @@
                monitor: '日志监控',
                save: '保存地图',
                clear: '清空地图',
                adapt: 'ADAPT',
                rotate: 'ROTATE',
                flip: 'FLIP',
                fake: 'FAKE',
                route: 'ROUTE',
                adapt: '适配',
                rotate: '旋转',
                flip: '翻转',
                fake: '模拟',
                route: '路线',
                area: '区域',
                disable: '禁用',
                enable: '启用',
                reset: '重置',
@@ -675,6 +676,8 @@
                oneClickPatrol: '一键巡逻',
                cancelPatrol: '取消巡逻',
                addArea: '添加区域',
                cancelAddArea: '取消添加',
                areaList: '区域列表',
            },
            mode: {
                observer: '观察模式',
@@ -757,6 +760,42 @@
                    },
                },
            },
            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}" 已创建',
                },
            },
        },
    }
};
zy-acs-flow/src/map/AreaList.jsx
New file
@@ -0,0 +1,164 @@
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;
zy-acs-flow/src/map/MapPage.jsx
@@ -14,6 +14,7 @@
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";
@@ -26,6 +27,7 @@
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;
@@ -51,6 +53,7 @@
    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);
@@ -58,6 +61,7 @@
    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;
@@ -137,7 +141,9 @@
        setBatchSelectionVisible(false);
        setAreaSettingsVisible(false);
        setAreaDrawing(false);
        setAreaListVisible(false);
        Tool.cancelAreaDrawing();
        Tool.hideAreas(curZone, setShowAreas);
        setCurSprite(null);
        setBatchSprites([]);
@@ -212,6 +218,7 @@
        if (!mapContainer) {
            return;
        }
        setAreaListVisible(false);
        Tool.removeSelectedEffect();
        if (curSprite) {
            if (mode === MAP_MODE.OBSERVER_MODE) {
@@ -362,14 +369,24 @@
                {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) => {
@@ -389,7 +406,9 @@
                                }
                            }}
                        >
                            {translate('page.map.action.addArea')}
                            {areaDrawing
                                ? translate('page.map.action.cancelAddArea')
                                : translate('page.map.action.addArea')}
                        </Button>
                    </>
                )}
@@ -465,6 +484,16 @@
                        gap: 2
                    }}
                >
                    {mode === MAP_MODE.OBSERVER_MODE && (
                        <>
                            <AreaFab
                                curZone={curZone}
                                showAreas={showAreas}
                                setShowAreas={setShowAreas}
                                notify={notify}
                            />
                        </>
                    )}
                    {mode !== MAP_MODE.MOVABLE_MODE && (
                        <>
                            <RouteFab
@@ -549,6 +578,14 @@
                width={570}
            />
            <AreaList
                zoneId={curZone}
                open={areaListVisible}
                onClose={() => setAreaListVisible(false)}
                setCurSprite={setCurSprite}
                width={378}
            />
        </Box>
    );
}
zy-acs-flow/src/map/areaSettings/AreaAdvancedTab.jsx
@@ -9,20 +9,40 @@
    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)}
@@ -30,7 +50,7 @@
                </Grid>
                <Grid item xs={6}>
                    <TextField
                        label={translate('page.map.area.maxQty', { _: '最大数量' })}
                        label={translate('page.map.area.form.maxQty')}
                        fullWidth
                        type="number"
                        value={maxQty}
@@ -39,26 +59,48 @@
                </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}
@@ -66,8 +108,18 @@
                    />
                </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>
zy-acs-flow/src/map/areaSettings/AreaBasicTab.jsx
@@ -52,7 +52,7 @@
        if (disableSave) {
            return;
        }
        const confirmMsg = translate('page.map.area.saveConfirm', { _: '确认保存当前修改?' });
        const confirmMsg = translate('page.map.area.confirm.save');
        if (window.confirm(confirmMsg)) {
            onSave?.();
        }
@@ -62,17 +62,17 @@
        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"
@@ -83,7 +83,7 @@
            <Box >
                <Typography variant="subtitle2" gutterBottom>
                    {translate('page.map.area.agv', { _: '选择AGV小车' })}
                    {translate('page.map.area.form.agv')}
                </Typography>
                <Autocomplete
                    multiple
@@ -143,7 +143,7 @@
                            {...params}
                            size="small"
                            variant="outlined"
                            placeholder={translate('page.map.area.agv.placeholder', { _: '' })}
                            placeholder={translate('page.map.area.form.agvPlaceholder')}
                        />
                    )}
                />
@@ -151,7 +151,7 @@
            <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"
@@ -176,7 +176,7 @@
                        ))
                    ) : (
                        <Typography variant="body2" color="text.secondary">
                            {translate('page.map.area.barcodes.empty', { _: '暂无条码' })}
                            {translate('page.map.area.form.codesEmpty')}
                        </Typography>
                    )}
                </Paper>
@@ -184,10 +184,10 @@
            <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>
zy-acs-flow/src/map/areaSettings/index.jsx
@@ -63,8 +63,10 @@
    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);
@@ -84,8 +86,10 @@
            setCode('');
            setMaxCount('');
            setSpeedLimit('');
            setMemo('');
            setStartPoint({ x: '', y: '' });
            setEndPoint({ x: '', y: '' });
            setPriority('');
            setMemo('');
            setAgvList([]);
            setCodeList([]);
            setInitialBasic({ name: '', agvIds: [] });
@@ -107,8 +111,16 @@
            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);
@@ -127,23 +139,39 @@
        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 () => {
@@ -158,10 +186,6 @@
            }
            onCancel?.();
        }
    };
    const handleSaveAdvanced = () => {
        // placeholder for save logic
    };
    const basicDirty = name !== initialBasic.name
@@ -185,7 +209,7 @@
                            <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 />
@@ -214,8 +238,8 @@
                                        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 />
@@ -243,10 +267,12 @@
                                                setMaxQty={setMaxCount}
                                                speedLimit={speedLimit}
                                                setSpeedLimit={setSpeedLimit}
                                                shapeData={memo}
                                                setShapeData={setMemo}
                                                startPoint={startPoint}
                                                endPoint={endPoint}
                                                priority={priority}
                                                setPriority={setPriority}
                                                memo={memo}
                                                setMemo={setMemo}
                                                onSave={handleSaveAdvanced}
                                            />
                                        )}
zy-acs-flow/src/map/header/AreaFab.jsx
New file
@@ -0,0 +1,55 @@
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 />
                &nbsp;{translate('page.map.action.area')}&nbsp;
                {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;
zy-acs-flow/src/map/header/RouteFab.jsx
@@ -33,6 +33,9 @@
                size="small"
                disabled={loading}
                onClick={handleClick}
                sx={{
                    minWidth: 100
                }}
            >
                <AltRouteIcon />
                &nbsp;{translate('page.map.action.route')}&nbsp;
zy-acs-flow/src/map/insight/area/index.jsx
New file
@@ -0,0 +1,182 @@
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;
zy-acs-flow/src/map/player.js
@@ -2,7 +2,7 @@
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 {
@@ -90,6 +90,9 @@
                    // 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);
zy-acs-flow/src/map/tool.js
@@ -399,6 +399,20 @@
        .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;
@@ -911,11 +925,22 @@
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);
@@ -945,7 +970,7 @@
    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) => {
@@ -961,6 +986,9 @@
                mapContainer.addChild(graphics);
            }
        });
        if (callback) {
            callback(areas);
        }
    });
};
@@ -983,8 +1011,11 @@
    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),
@@ -995,8 +1026,10 @@
    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));
@@ -1114,3 +1147,60 @@
        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();
};
zy-acs-manager/src/main/java/com/zy/acs/manager/manager/service/impl/AreaServiceImpl.java
@@ -20,6 +20,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.Objects;
@Slf4j
@Service("areaService")
@@ -91,10 +92,30 @@
        // 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());