From 10492a922d3a8d295ada4ec99cc928031f3abd0e Mon Sep 17 00:00:00 2001
From: vincentlu <t1341870251@gmail.com>
Date: 星期二, 17 三月 2026 15:29:22 +0800
Subject: [PATCH] #

---
 zy-acs-flow/src/map/NewsLogDialog.jsx                                               |  420 +++++++++++++++++++++++++++++++++++
 zy-acs-flow/src/map/http.js                                                         |   15 +
 zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/TrafficService.java    |    3 
 zy-acs-flow/src/i18n/en.js                                                          |   11 
 zy-acs-manager/src/main/java/com/zy/acs/manager/common/CodeBuilder.java             |    3 
 zy-acs-flow/src/i18n/zh.js                                                          |   11 
 zy-acs-flow/src/map/MapPage.jsx                                                     |   12 
 zy-acs-manager/src/main/java/com/zy/acs/manager/core/controller/NewsController.java |    2 
 zy-acs-common/src/main/java/com/zy/acs/common/utils/News.java                       |  197 ++++++++--------
 9 files changed, 570 insertions(+), 104 deletions(-)

diff --git a/zy-acs-common/src/main/java/com/zy/acs/common/utils/News.java b/zy-acs-common/src/main/java/com/zy/acs/common/utils/News.java
index 34f9abf..5729f35 100644
--- a/zy-acs-common/src/main/java/com/zy/acs/common/utils/News.java
+++ b/zy-acs-common/src/main/java/com/zy/acs/common/utils/News.java
@@ -1,10 +1,16 @@
 package com.zy.acs.common.utils;
 
+import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
+import org.slf4j.helpers.FormattingTuple;
+import org.slf4j.helpers.MessageFormatter;
 
-import java.lang.reflect.Array;
-import java.text.SimpleDateFormat;
-import java.util.*;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * news stories for vincent
@@ -13,85 +19,15 @@
 @Slf4j
 public class News {
 
+    private static final int DEFAULT_CAPACITY = 1024;
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private static final NewsQueue<NewsDomain> NEWS_QUEUE = new NewsQueue<>(DEFAULT_CAPACITY);
+
     public static void main(String[] args) {
         News.info("info{}", 1);
         News.warn("warn{}", 2);
         News.error("error{}", 3);
         System.out.println(News.print());
-    }
-
-    interface NewsSupport<T> { boolean execute(T t); }
-
-    private static final NewsQueue<NewsDomain> NEWS_QUEUE = new NewsQueue<>(NewsDomain.class, 1024);
-
-    @SuppressWarnings({"unchecked"})
-    static class NewsQueue<T> {
-
-        private final transient Class<T> cls;
-        private final T[] arr;
-        private final int capacity;
-        private int head;
-        private int tail;
-
-        { this.head = 0; this.tail = 0; }
-
-        public NewsQueue(Class<T> cls, int capacity) {
-            this.cls = cls;
-            this.arr = (T[]) Array.newInstance(cls, capacity);
-            this.capacity = capacity;
-        }
-
-        public synchronized boolean offer(T t) {
-            if (this.tail == this.capacity) {
-                this.peek();
-            }
-            this.reform();
-            this.arr[this.tail] = t;
-            this.tail ++;
-            return true;
-        }
-
-        public synchronized boolean put(T t) {
-            if (this.tail == this.capacity) {
-                return false;
-            } else {
-                this.reform();
-            }
-            this.arr[this.tail] = t;
-            this.tail ++;
-            return true;
-        }
-
-        public synchronized T peek() {
-            if (this.head == this.tail) {
-                return null;
-            }
-            T t = this.arr[this.head];
-            this.head ++;
-            this.reform();
-            return t;
-        }
-
-        private void reform() {
-            for (int i = this.head; i < this.tail; i++) {
-                this.arr[i-this.head] = this.arr[i];
-            }
-            this.tail -= this.head;
-            this.head = 0;
-        }
-
-        public synchronized int size() {
-            return this.tail - this.head;
-        }
-
-        public synchronized List<T> data() {
-            T[] ts = (T[]) Array.newInstance(this.cls, size());
-            if (this.tail - this.head >= 0) {
-                System.arraycopy(this.arr, this.head, ts, 0, this.tail - this.head);
-            }
-            return Arrays.asList(ts);
-        }
-
     }
 
     public static void info(String format, Object... arguments) {
@@ -115,9 +51,9 @@
         for (int i = 0; i < domains.size(); i++) {
             NewsDomain domain = domains.get(i);
             sb.append("{");
-            sb.append("\"l\":").append(domain.level.idx).append(",");
-            sb.append("\"v\":\"").append(domain.content).append("\"").append(",");
-            sb.append("\"t\":\"").append(domain.date).append("\"");
+            sb.append("\"l\":").append(domain.getLevel().idx).append(",");
+            sb.append("\"v\":\"").append(escapeJson(domain.getContent())).append("\"").append(",");
+            sb.append("\"t\":\"").append(escapeJson(domain.getDate())).append("\"");
             sb.append("}");
             if (i < domains.size() - 1) {
                 sb.append(",");
@@ -131,42 +67,107 @@
         List<Map<String, Object>> res = new ArrayList<>();
         for (NewsDomain datum : NEWS_QUEUE.data()) {
             Map<String, Object> map = new HashMap<>();
-            map.put("l", datum.level.idx);
-            map.put("v", datum.content);
-            map.put("t", datum.date);
+            map.put("l", datum.getLevel().idx);
+            map.put("v", datum.getContent());
+            map.put("t", datum.getDate());
             res.add(map);
         }
         return res;
     }
 
     private static boolean offer(NewsLevel level, String msg, Object[] args) {
-        return NEWS_QUEUE.offer(new NewsDomain(level, replace(msg, args), (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).format(new Date())));
+        String template = msg == null ? "" : msg;
+        FormattingTuple tuple = MessageFormatter.arrayFormat(template, args);
+        String formatted = tuple.getMessage();
+        return NEWS_QUEUE.offer(new NewsDomain(level, formatted,
+                LocalDateTime.now().format(DATE_FORMATTER)));
     }
 
-    private static String replace(String str, Object[] objs){
-        if (null == objs || objs.length == 0 || null == str || "".equals(str.trim())) {
-            return str;
-        } else {
-            StringBuilder sb = new StringBuilder(str);
-            for (Object obj : objs) {
-                int idx = sb.indexOf("{}");
-                if (idx == -1) { break; }
-                sb.replace(idx, idx + 2, String.valueOf(obj));
+    private static String escapeJson(String value) {
+        if (value == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder(value.length());
+        for (char c : value.toCharArray()) {
+            switch (c) {
+                case '"':
+                    sb.append("\\\"");
+                    break;
+                case '\\':
+                    sb.append("\\\\");
+                    break;
+                case '\n':
+                    sb.append("\\n");
+                    break;
+                case '\r':
+                    sb.append("\\r");
+                    break;
+                case '\t':
+                    sb.append("\\t");
+                    break;
+                default:
+                    if (c < 0x20) {
+                        sb.append(String.format("\\u%04x", (int) c));
+                    } else {
+                        sb.append(c);
+                    }
             }
-            return sb.toString();
+        }
+        return sb.toString();
+    }
+
+    private static final class NewsQueue<T> {
+
+        private final Object[] arr;
+        private final int capacity;
+        private int head;
+        private int size;
+
+        private NewsQueue(int capacity) {
+            if (capacity <= 0) {
+                throw new IllegalArgumentException("capacity must be > 0");
+            }
+            this.arr = new Object[capacity];
+            this.capacity = capacity;
+            this.head = 0;
+            this.size = 0;
+        }
+
+        public synchronized boolean offer(T t) {
+            int writeIndex = (head + size) % capacity;
+            arr[writeIndex] = t;
+            if (size == capacity) {
+                head = (head + 1) % capacity;
+            } else {
+                size++;
+            }
+            return true;
+        }
+
+        public synchronized List<T> data() {
+            List<T> copy = new ArrayList<>(size);
+            for (int i = 0; i < size; i++) {
+                int idx = (head + i) % capacity;
+                @SuppressWarnings("unchecked")
+                T element = (T) arr[idx];
+                copy.add(element);
+            }
+            return copy;
         }
     }
 
+    @Data
     static class NewsDomain {
-        public NewsLevel level;
-        public String content;
-        public String date;
+        private final NewsLevel level;
+        private final String content;
+        private final String date;
 
         public NewsDomain(NewsLevel level, String content, String date) {
             this.level = level;
             this.content = content;
             this.date = date;
         }
+
     }
 
     enum NewsLevel {
@@ -174,7 +175,7 @@
         WARN(2),
         ERROR(3),
         ;
-        public int idx;
+        public final int idx;
         NewsLevel(int idx) {
             this.idx = idx;
         }
diff --git a/zy-acs-flow/src/i18n/en.js b/zy-acs-flow/src/i18n/en.js
index 89a69d0..12465ff 100644
--- a/zy-acs-flow/src/i18n/en.js
+++ b/zy-acs-flow/src/i18n/en.js
@@ -875,6 +875,17 @@
         },
         map: {
             welcome: 'Welcome to the RCS System. Tip: Left-click to select objects, right-click to pan the view, and use the scroll wheel to zoom the view.',
+            monitor: {
+                log: {
+                    title: 'Real-time Logs',
+                    autoScroll: 'Auto Scroll',
+                    empty: 'No Logs',
+                    jumpLatest: 'Jump to latest',
+                    lastUpdate: {
+                        empty: 'No Updates',
+                    },
+                },
+            },
             devices: {
                 title: 'Icons',
                 shelf: 'SHELF',
diff --git a/zy-acs-flow/src/i18n/zh.js b/zy-acs-flow/src/i18n/zh.js
index f44c842..b912507 100644
--- a/zy-acs-flow/src/i18n/zh.js
+++ b/zy-acs-flow/src/i18n/zh.js
@@ -875,6 +875,17 @@
         },
         map: {
             welcome: '娆㈣繋浣跨敤 RCS 绯荤粺銆傛彁绀猴細榧犳爣宸﹂敭閫変腑瀵硅薄锛屽彸閿钩绉昏鍥撅紝婊氳疆缂╂斁瑙嗗浘銆�',
+            monitor: {
+                log: {
+                    title: '鏃ュ織鐩戞帶',
+                    autoScroll: '鑷姩婊氬姩',
+                    empty: '鏆傛棤鏃ュ織',
+                    jumpLatest: '鍥炲埌鏈�鏂�',
+                    lastUpdate: {
+                        empty: '鏆傛棤鏇存柊',
+                    },
+                },
+            },
             devices: {
                 title: '鍥炬爣搴�',
                 shelf: '璐ф灦',
diff --git a/zy-acs-flow/src/map/MapPage.jsx b/zy-acs-flow/src/map/MapPage.jsx
index 8b229e2..44f8813 100644
--- a/zy-acs-flow/src/map/MapPage.jsx
+++ b/zy-acs-flow/src/map/MapPage.jsx
@@ -29,6 +29,7 @@
 import RouteFab from "./header/RouteFab";
 import AreaFab from "./header/AreaFab";
 import MoreOperate from "./header/MoreOperate";
+import NewsLogDialog from "./NewsLogDialog";
 
 let player;
 let websocket;
@@ -66,6 +67,7 @@
         const storedValue = localStorage.getItem('curZone');
         return storedValue !== null ? JSON.parse(storedValue) : null;
     });
+    const [logDialogOpen, setLogDialogOpen] = useState(false);
 
     const handleResize = () => {
         if (!contentRef.current || !player) {
@@ -315,7 +317,11 @@
                         >
                             {rcsStatus ? translate('page.map.action.shutdown') : translate('page.map.action.startup')}
                         </Button>
-                        <Button variant="contained" color="primary">
+                        <Button
+                            variant="contained"
+                            color="primary"
+                            onClick={() => setLogDialogOpen(true)}
+                        >
                             {translate('page.map.action.monitor')}
                         </Button>
                         <MoreOperate />
@@ -592,6 +598,10 @@
                 width={378}
             />
 
+            <NewsLogDialog
+                open={logDialogOpen}
+                onClose={() => setLogDialogOpen(false)}
+            />
         </Box>
     );
 }
diff --git a/zy-acs-flow/src/map/NewsLogDialog.jsx b/zy-acs-flow/src/map/NewsLogDialog.jsx
new file mode 100644
index 0000000..695c9bf
--- /dev/null
+++ b/zy-acs-flow/src/map/NewsLogDialog.jsx
@@ -0,0 +1,420 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+    Box,
+    Button,
+    Chip,
+    Divider,
+    FormControlLabel,
+    IconButton,
+    Paper,
+    Stack,
+    Switch,
+    Typography,
+    useTheme,
+} from '@mui/material';
+import CloseIcon from '@mui/icons-material/Close';
+import PushPinIcon from '@mui/icons-material/PushPin';
+import { useTranslate } from 'react-admin';
+import * as Http from './http';
+
+const LOG_LEVEL_META = {
+    1: { label: 'INFO', color: 'info' },
+    2: { label: 'WARN', color: 'warning' },
+    3: { label: 'ERROR', color: 'error' },
+};
+
+const POLLING_INTERVAL = 2000;
+const PANEL_WIDTH = 480;
+const PANEL_HEIGHT = 360;
+const MIN_WIDTH = 320;
+const MIN_HEIGHT = 240;
+const EDGE_MARGIN = 16;
+
+const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
+
+const NewsLogDialog = ({ open, onClose }) => {
+    const translate = useTranslate();
+    const theme = useTheme();
+    const [logs, setLogs] = useState([]);
+    const [autoScroll, setAutoScroll] = useState(true);
+    const [lastUpdated, setLastUpdated] = useState(null);
+    const [position, setPosition] = useState(null);
+    const [size, setSize] = useState(null);
+    const scrollRef = useRef(null);
+    const panelRef = useRef(null);
+    const pollingRef = useRef(null);
+    const dragRef = useRef({ active: false, offsetX: 0, offsetY: 0 });
+    const resizeRef = useRef({
+        active: false,
+        startX: 0,
+        startY: 0,
+        initialWidth: PANEL_WIDTH,
+        initialHeight: PANEL_HEIGHT,
+        initialX: EDGE_MARGIN,
+        initialY: EDGE_MARGIN,
+    });
+    const getSizeLimits = useCallback(() => {
+        const viewportWidth = typeof window === 'undefined' ? PANEL_WIDTH + 64 : window.innerWidth;
+        const viewportHeight = typeof window === 'undefined' ? PANEL_HEIGHT + 160 : window.innerHeight;
+        return {
+            maxWidth: Math.max(MIN_WIDTH, viewportWidth - 64),
+            maxHeight: Math.max(MIN_HEIGHT, viewportHeight - 160),
+        };
+    }, []);
+
+    const buildInitialSize = useCallback(() => {
+        const { maxWidth, maxHeight } = getSizeLimits();
+        return {
+            width: clamp(PANEL_WIDTH, MIN_WIDTH, maxWidth),
+            height: clamp(PANEL_HEIGHT, MIN_HEIGHT, maxHeight),
+        };
+    }, [getSizeLimits]);
+
+    const fetchLogs = useCallback(async () => {
+        const data = await Http.fetchNewsLogs();
+        if (Array.isArray(data)) {
+            setLogs(data);
+            setLastUpdated(new Date());
+        }
+    }, []);
+
+    useEffect(() => {
+        if (!open) {
+            clearInterval(pollingRef.current);
+            pollingRef.current = null;
+            setLogs([]);
+            setLastUpdated(null);
+            setPosition(null);
+            setSize(null);
+            return;
+        }
+        setSize(buildInitialSize());
+        setAutoScroll(true);
+        fetchLogs();
+        pollingRef.current = setInterval(fetchLogs, POLLING_INTERVAL);
+        return () => {
+            clearInterval(pollingRef.current);
+            pollingRef.current = null;
+        };
+    }, [open, fetchLogs, buildInitialSize]);
+
+    useEffect(() => {
+        if (!autoScroll || !scrollRef.current) {
+            return;
+        }
+        scrollRef.current.scrollTo({
+            top: scrollRef.current.scrollHeight,
+            behavior: 'smooth',
+        });
+    }, [logs, autoScroll]);
+
+    const handleScroll = useCallback(() => {
+        if (!scrollRef.current || !autoScroll) {
+            return;
+        }
+        const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
+        if (scrollHeight - (scrollTop + clientHeight) > 32) {
+            setAutoScroll(false);
+        }
+    }, [autoScroll]);
+
+    const handleJumpLatest = () => {
+        setAutoScroll(true);
+        if (scrollRef.current) {
+            scrollRef.current.scrollTo({
+                top: scrollRef.current.scrollHeight,
+                behavior: 'smooth',
+            });
+        }
+    };
+
+    const handleClose = () => {
+        onClose?.();
+    };
+
+    const handleDragging = useCallback((event) => {
+        if (!dragRef.current.active) {
+            return;
+        }
+        event.preventDefault();
+        const width = panelRef.current?.offsetWidth || PANEL_WIDTH;
+        const height = panelRef.current?.offsetHeight || PANEL_HEIGHT;
+        const viewportWidth = typeof window === 'undefined' ? width + 2 * EDGE_MARGIN : window.innerWidth;
+        const viewportHeight = typeof window === 'undefined' ? height + 10 * EDGE_MARGIN : window.innerHeight;
+        const maxX = viewportWidth - width - EDGE_MARGIN;
+        const maxY = viewportHeight - height - EDGE_MARGIN;
+        const x = event.clientX - dragRef.current.offsetX;
+        const y = event.clientY - dragRef.current.offsetY;
+        setPosition({
+            x: clamp(x, EDGE_MARGIN, Math.max(maxX, EDGE_MARGIN)),
+            y: clamp(y, EDGE_MARGIN, Math.max(maxY, EDGE_MARGIN)),
+        });
+    }, []);
+
+    const handleDragEnd = useCallback(() => {
+        if (!dragRef.current.active) {
+            return;
+        }
+        dragRef.current.active = false;
+        document.removeEventListener('mousemove', handleDragging);
+        document.removeEventListener('mouseup', handleDragEnd);
+    }, [handleDragging]);
+
+    const handleDragStart = useCallback((event) => {
+        if (!panelRef.current) {
+            return;
+        }
+        const rect = panelRef.current.getBoundingClientRect();
+        dragRef.current = {
+            active: true,
+            offsetX: event.clientX - rect.left,
+            offsetY: event.clientY - rect.top,
+        };
+        document.addEventListener('mousemove', handleDragging);
+        document.addEventListener('mouseup', handleDragEnd);
+    }, [handleDragging, handleDragEnd]);
+
+    const handleResizing = useCallback((event) => {
+        if (!resizeRef.current.active) {
+            return;
+        }
+        event.preventDefault();
+        const deltaX = event.clientX - resizeRef.current.startX;
+        const deltaY = event.clientY - resizeRef.current.startY;
+        const { maxWidth, maxHeight } = getSizeLimits();
+        const initialWidth = resizeRef.current.initialWidth;
+        const initialHeight = resizeRef.current.initialHeight;
+        const initialX = resizeRef.current.initialX;
+        const initialY = resizeRef.current.initialY;
+        const initialRight = initialX + initialWidth;
+        const maxWidthAllowed = Math.min(maxWidth, initialRight - EDGE_MARGIN);
+        const widthRaw = initialWidth - deltaX;
+        const newWidth = clamp(widthRaw, MIN_WIDTH, Math.max(MIN_WIDTH, maxWidthAllowed));
+        const newX = clamp(initialRight - newWidth, EDGE_MARGIN, Math.max(initialRight - MIN_WIDTH, EDGE_MARGIN));
+        const newHeight = clamp(initialHeight + deltaY, MIN_HEIGHT, maxHeight);
+        setSize({
+            width: newWidth,
+            height: newHeight,
+        });
+        setPosition({
+            x: newX,
+            y: initialY,
+        });
+    }, [getSizeLimits]);
+
+    const handleResizeEnd = useCallback(() => {
+        if (!resizeRef.current.active) {
+            return;
+        }
+        resizeRef.current.active = false;
+        document.removeEventListener('mousemove', handleResizing);
+        document.removeEventListener('mouseup', handleResizeEnd);
+    }, [handleResizing]);
+
+    const handleResizeStart = useCallback((event) => {
+        event.preventDefault();
+        resizeRef.current.active = true;
+        resizeRef.current.startX = event.clientX;
+        resizeRef.current.startY = event.clientY;
+        const rect = panelRef.current?.getBoundingClientRect();
+        const resolvedX = position?.x ?? rect?.left ?? EDGE_MARGIN;
+        const resolvedY = position?.y ?? rect?.top ?? EDGE_MARGIN;
+        resizeRef.current.initialWidth = panelRef.current?.offsetWidth || size?.width || PANEL_WIDTH;
+        resizeRef.current.initialHeight = panelRef.current?.offsetHeight || size?.height || PANEL_HEIGHT;
+        resizeRef.current.initialX = resolvedX;
+        resizeRef.current.initialY = resolvedY;
+        setPosition({ x: resolvedX, y: resolvedY });
+        document.addEventListener('mousemove', handleResizing);
+        document.addEventListener('mouseup', handleResizeEnd);
+    }, [handleResizing, handleResizeEnd, size, position]);
+
+    useEffect(() => {
+        return () => {
+            document.removeEventListener('mousemove', handleDragging);
+            document.removeEventListener('mouseup', handleDragEnd);
+            document.removeEventListener('mousemove', handleResizing);
+            document.removeEventListener('mouseup', handleResizeEnd);
+        };
+    }, [handleDragging, handleDragEnd, handleResizing, handleResizeEnd]);
+
+    const timeLabel = useMemo(() => {
+        if (!lastUpdated) {
+            return translate('page.map.monitor.log.lastUpdate.empty', { _: 'No Updates' });
+        }
+        return lastUpdated.toLocaleTimeString();
+    }, [lastUpdated, translate]);
+
+    const panelPositionStyle = position
+        ? { top: position.y, left: position.x }
+        : { top: 140, right: 24 };
+
+    if (!open) {
+        return null;
+    }
+
+    const panelWidth = size?.width ?? PANEL_WIDTH;
+    const panelHeight = size?.height ?? PANEL_HEIGHT;
+
+    return (
+        <Box
+            sx={{
+                position: 'fixed',
+                zIndex: theme.zIndex.drawer + 10,
+                pointerEvents: 'none',
+                ...panelPositionStyle,
+            }}
+        >
+            <Paper
+                ref={panelRef}
+                elevation={16}
+                sx={{
+                    width: panelWidth,
+                    height: panelHeight,
+                    minWidth: MIN_WIDTH,
+                    minHeight: MIN_HEIGHT,
+                    display: 'flex',
+                    flexDirection: 'column',
+                    borderRadius: 2,
+                    overflow: 'hidden',
+                    pointerEvents: 'auto',
+                    backdropFilter: 'blur(4px)',
+                    position: 'relative',
+                }}
+            >
+                <Box
+                    sx={{
+                        display: 'flex',
+                        alignItems: 'center',
+                        padding: '12px 16px',
+                        borderBottom: `1px solid ${theme.palette.divider}`,
+                        cursor: 'move',
+                        background: theme.palette.mode === 'light'
+                            ? theme.palette.grey[50]
+                            : theme.palette.grey[900],
+                    }}
+                    onMouseDown={handleDragStart}
+                >
+                    <Stack direction="row" spacing={1} alignItems="center">
+                        <PushPinIcon fontSize="small" color="primary" />
+                        <Typography variant="subtitle1" fontWeight={600}>
+                            {translate('page.map.monitor.log.title', { _: 'Real-time Logs' })}
+                        </Typography>
+                    </Stack>
+                    <Box sx={{ flexGrow: 1 }} />
+                    <FormControlLabel
+                        sx={{ mr: 2, userSelect: 'none' }}
+                        control={(
+                            <Switch
+                                size="small"
+                                checked={autoScroll}
+                                onChange={(event) => setAutoScroll(event.target.checked)}
+                            />
+                        )}
+                        label={translate('page.map.monitor.log.autoScroll', { _: 'Auto Scroll' })}
+                    />
+                    <Chip
+                        label={timeLabel}
+                        size="small"
+                        color="primary"
+                        variant="outlined"
+                        sx={{ mr: 1, fontWeight: 500 }}
+                    />
+                    <IconButton size="small" onClick={handleClose}>
+                        <CloseIcon fontSize="small" />
+                    </IconButton>
+                </Box>
+                <Box
+                    ref={scrollRef}
+                    onScroll={handleScroll}
+                    sx={{
+                        flex: 1,
+                        minHeight: 0,
+                        overflowY: 'auto',
+                        px: 3,
+                        py: 2,
+                        backgroundColor: theme.palette.background.default,
+                    }}
+                >
+                    {logs.length === 0 ? (
+                        <Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 6 }}>
+                            {translate('page.map.monitor.log.empty', { _: 'No Logs' })}
+                        </Typography>
+                    ) : (
+                        <Stack spacing={1.25}>
+                            {logs.map((item, index) => {
+                                const level = item?.l;
+                                const levelMeta = LOG_LEVEL_META[level] || LOG_LEVEL_META[1];
+                                return (
+                                    <Box
+                                        key={`${item?.t}-${index}`}
+                                        sx={{
+                                            borderRadius: 1.5,
+                                            border: `1px solid ${theme.palette.divider}`,
+                                            backgroundColor: theme.palette.background.paper,
+                                            px: 2,
+                                            py: 1.25,
+                                        }}
+                                    >
+                                        <Stack direction="row" spacing={1} alignItems="center">
+                                            <Chip
+                                                label={levelMeta.label}
+                                                size="small"
+                                                color={levelMeta.color}
+                                                variant="outlined"
+                                            />
+                                            <Divider orientation="vertical" flexItem />
+                                            <Typography variant="caption" color="text.secondary">
+                                                {item?.t || '--'}
+                                            </Typography>
+                                        </Stack>
+                                        <Typography variant="body2" sx={{ mt: 1, whiteSpace: 'pre-wrap' }}>
+                                            {item?.v || ''}
+                                        </Typography>
+                                    </Box>
+                                );
+                            })}
+                        </Stack>
+                    )}
+                </Box>
+                <Divider />
+                <Box
+                    sx={{
+                        display: 'flex',
+                        alignItems: 'center',
+                        justifyContent: 'space-between',
+                        py: 1,
+                        px: 2,
+                        backgroundColor: theme.palette.background.paper,
+                    }}
+                >
+                    {!autoScroll && logs.length > 0 ? (
+                        <Button onClick={handleJumpLatest} size="small">
+                            {translate('page.map.monitor.log.jumpLatest', { _: 'Jump to latest' })}
+                        </Button>
+                    ) : (
+                        <span />
+                    )}
+                        <Button onClick={handleClose} size="small">
+                        {translate('ra.action.close', { _: 'ra.action.close' })}
+                    </Button>
+                </Box>
+                <Box
+                    onMouseDown={handleResizeStart}
+                    sx={{
+                        position: 'absolute',
+                        width: 18,
+                        height: 18,
+                        left: 6,
+                        bottom: 6,
+                        cursor: 'nesw-resize',
+                        borderLeft: `2px solid ${theme.palette.divider}`,
+                        borderBottom: `2px solid ${theme.palette.divider}`,
+                        borderBottomLeftRadius: 2,
+                    }}
+                />
+            </Paper>
+        </Box>
+    );
+};
+
+export default NewsLogDialog;
diff --git a/zy-acs-flow/src/map/http.js b/zy-acs-flow/src/map/http.js
index f1be56e..bf59dcc 100644
--- a/zy-acs-flow/src/map/http.js
+++ b/zy-acs-flow/src/map/http.js
@@ -497,3 +497,18 @@
     }
     return [];
 }
+
+export const fetchNewsLogs = async () => {
+    try {
+        const res = await request.get('/news/print');
+        const { code, msg, data } = res.data;
+        if (code === 200) {
+            return Array.isArray(data) ? data : [];
+        }
+        notify?.error(msg);
+    } catch (error) {
+        notify?.error(error.message);
+        console.error(error.message);
+    }
+    return null;
+}
\ No newline at end of file
diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/common/CodeBuilder.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/common/CodeBuilder.java
index c45fd63..634781e 100644
--- a/zy-acs-manager/src/main/java/com/zy/acs/manager/common/CodeBuilder.java
+++ b/zy-acs-manager/src/main/java/com/zy/acs/manager/common/CodeBuilder.java
@@ -49,7 +49,6 @@
 /**
  *
   # dual
-
   TRUNCATE man_bus;
   TRUNCATE man_task;
   TRUNCATE man_travel;
@@ -62,7 +61,6 @@
   TRUNCATE man_veh_fault_rec;
 
   # log
-
   TRUNCATE man_bus_log;
   TRUNCATE man_jam_log;
   TRUNCATE man_lane;
@@ -73,7 +71,6 @@
   TRUNCATE man_action_log;
 
   # init
-
   TRUNCATE man_code;
   TRUNCATE man_code_gap;
   TRUNCATE man_route;
diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/core/controller/NewsController.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/controller/NewsController.java
index ce6eca9..af609e4 100644
--- a/zy-acs-manager/src/main/java/com/zy/acs/manager/core/controller/NewsController.java
+++ b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/controller/NewsController.java
@@ -11,7 +11,7 @@
  */
 @Slf4j
 @RestController
-@RequestMapping("/news")
+@RequestMapping("/api/news")
 public class NewsController {
 
     @RequestMapping("/print")
diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/TrafficService.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/TrafficService.java
index dbf3a7b..98bbfda 100644
--- a/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/TrafficService.java
+++ b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/TrafficService.java
@@ -2,6 +2,7 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zy.acs.common.utils.GsonUtils;
+import com.zy.acs.common.utils.News;
 import com.zy.acs.common.utils.Utils;
 import com.zy.acs.framework.common.Cools;
 import com.zy.acs.framework.common.SnowflakeIdWorker;
@@ -337,7 +338,7 @@
 
                     boolean hasUnavoidableBlocks = blockVehicleList.stream().anyMatch(blockVehicleDto -> !blockVehicleDto.isAvoidable());
                     if (hasUnavoidableBlocks && pathList.size() <= MapDataConstant.MIN_SLICE_PATH_LENGTH) {
-                        log.info("AGV[{}] waiting in place, because the path list is too short...", agvNo);
+                        News.info("AGV[{}] waiting in place, because the path list is too short...", agvNo);
                         pathList.clear();
                     }
 

--
Gitblit v1.9.1