From 75775c96801e548ecf3368865124c33a03e32dcf Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期一, 16 三月 2026 22:26:34 +0800
Subject: [PATCH] #

---
 src/main/resources/i18n/zh-CN/messages.properties               |    6 
 src/main/webapp/static/js/dashboard/dashboard.js                |  396 ++++++++++++
 src/main/resources/i18n/en-US/messages.properties               |    4 
 src/main/webapp/views/index.html                                |   78 +
 src/main/webapp/views/dashboard/dashboard.html                  |  863 ++++++++++++++++++++++++++
 src/main/java/com/zy/system/controller/DashboardController.java |  596 ++++++++++++++++++
 6 files changed, 1,921 insertions(+), 22 deletions(-)

diff --git a/src/main/java/com/zy/system/controller/DashboardController.java b/src/main/java/com/zy/system/controller/DashboardController.java
new file mode 100644
index 0000000..fa97e4d
--- /dev/null
+++ b/src/main/java/com/zy/system/controller/DashboardController.java
@@ -0,0 +1,596 @@
+package com.zy.system.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.annotations.ManagerAuth;
+import com.core.common.R;
+import com.zy.ai.entity.AiChatSession;
+import com.zy.ai.entity.LlmCallLog;
+import com.zy.ai.entity.LlmRouteConfig;
+import com.zy.ai.mapper.AiChatSessionMapper;
+import com.zy.ai.service.LlmCallLogService;
+import com.zy.ai.service.LlmRouteConfigService;
+import com.zy.asrs.entity.*;
+import com.zy.asrs.service.*;
+import com.zy.core.cache.SlaveConnection;
+import com.zy.core.enums.SlaveType;
+import com.zy.core.enums.WrkStsType;
+import com.zy.core.model.protocol.CrnProtocol;
+import com.zy.core.model.protocol.DualCrnProtocol;
+import com.zy.core.model.protocol.RgvProtocol;
+import com.zy.core.model.protocol.StationProtocol;
+import com.zy.core.properties.SystemProperties;
+import com.zy.core.thread.CrnThread;
+import com.zy.core.thread.DualCrnThread;
+import com.zy.core.thread.RgvThread;
+import com.zy.core.thread.StationThread;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/dashboard")
+public class DashboardController {
+
+    private static final Set<Long> MANUAL_TASK_STATUS = new HashSet<>(Arrays.asList(6L, 106L, 506L));
+    private static final Set<Long> COMPLETED_TASK_STATUS = new HashSet<>(Arrays.asList(9L, 10L, 109L, 110L, 509L));
+    private static final Set<Long> NEW_TASK_STATUS = new HashSet<>(Arrays.asList(1L, 101L, 501L));
+
+    @Autowired
+    private WrkMastService wrkMastService;
+    @Autowired
+    private DeviceConfigService deviceConfigService;
+    @Autowired
+    private BasCrnpService basCrnpService;
+    @Autowired
+    private BasDualCrnpService basDualCrnpService;
+    @Autowired
+    private BasRgvService basRgvService;
+    @Autowired
+    private BasStationService basStationService;
+    @Autowired
+    private LlmRouteConfigService llmRouteConfigService;
+    @Autowired
+    private LlmCallLogService llmCallLogService;
+    @Autowired
+    private AiChatSessionMapper aiChatSessionMapper;
+
+    @GetMapping("/summary/auth")
+    @ManagerAuth(memo = "绯荤粺浠〃鐩樼粺璁�")
+    public R summary() {
+        Map<String, Object> tasks = buildTaskStats();
+        Map<String, Object> devices = buildDeviceStats();
+        Map<String, Object> ai = buildAiStats();
+
+        Map<String, Object> overview = new LinkedHashMap<>();
+        overview.put("systemRunning", Boolean.TRUE.equals(SystemProperties.WCS_RUNNING_STATUS.get()));
+        overview.put("taskTotal", getNestedLong(tasks, "overview", "total"));
+        overview.put("taskRunning", getNestedLong(tasks, "overview", "running"));
+        overview.put("deviceTotal", getNestedLong(devices, "overview", "total"));
+        overview.put("deviceOnline", getNestedLong(devices, "overview", "online"));
+        overview.put("deviceAlarm", getNestedLong(devices, "overview", "alarm"));
+        overview.put("aiTokenTotal", getNestedLong(ai, "overview", "tokenTotal"));
+        overview.put("aiCallTotal", getNestedLong(ai, "overview", "llmCallTotal"));
+        overview.put("generatedAt", formatDateTime(new Date()));
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("overview", overview);
+        result.put("tasks", tasks);
+        result.put("devices", devices);
+        result.put("ai", ai);
+        return R.ok(result);
+    }
+
+    private Map<String, Object> buildTaskStats() {
+        Map<String, Object> result = new LinkedHashMap<>();
+        long total = safeCount(wrkMastService.count(new QueryWrapper<WrkMast>()));
+        long running = 0L;
+        long manual = 0L;
+        long completed = 0L;
+        long newCreated = 0L;
+        long inbound = 0L;
+        long outbound = 0L;
+        long move = 0L;
+
+        List<Map<String, Object>> statusStats = new ArrayList<>();
+        try {
+            List<Map<String, Object>> grouped = wrkMastService.listMaps(new QueryWrapper<WrkMast>()
+                    .select("wrk_sts", "count(*) as total")
+                    .groupBy("wrk_sts"));
+            for (Map<String, Object> row : grouped) {
+                Long wrkSts = toLong(row.get("wrk_sts"));
+                long count = toLong(row.get("total"));
+                if (count <= 0) {
+                    continue;
+                }
+                if (isInboundTask(wrkSts)) {
+                    inbound += count;
+                } else if (isMoveTask(wrkSts)) {
+                    move += count;
+                } else {
+                    outbound += count;
+                }
+
+                if (isManualTask(wrkSts)) {
+                    manual += count;
+                } else if (isCompletedTask(wrkSts)) {
+                    completed += count;
+                } else if (isNewTask(wrkSts)) {
+                    newCreated += count;
+                } else {
+                    running += count;
+                }
+
+                statusStats.add(metric(resolveTaskStatusName(wrkSts), count));
+            }
+        } catch (Exception e) {
+            log.warn("dashboard task group stats load failed: {}", safeMessage(e));
+        }
+
+        statusStats.sort((a, b) -> Long.compare(toLong(b.get("value")), toLong(a.get("value"))));
+        if (statusStats.size() > 8) {
+            statusStats = new ArrayList<>(statusStats.subList(0, 8));
+        }
+
+        List<Map<String, Object>> directionStats = new ArrayList<>();
+        directionStats.add(metric("鍏ュ簱浠诲姟", inbound));
+        directionStats.add(metric("鍑哄簱浠诲姟", outbound));
+        directionStats.add(metric("绉诲簱浠诲姟", move));
+
+        List<Map<String, Object>> stageStats = new ArrayList<>();
+        stageStats.add(metric("鎵ц涓�", running));
+        stageStats.add(metric("寰呬汉宸�", manual));
+        stageStats.add(metric("宸插畬鎴�", completed));
+        stageStats.add(metric("鏂板缓", newCreated));
+
+        List<Map<String, Object>> recentTasks = new ArrayList<>();
+        try {
+            List<Map<String, Object>> rows = wrkMastService.listMaps(new QueryWrapper<WrkMast>()
+                    .select("wrk_no", "wrk_sts", "sta_no", "source_sta_no", "loc_no", "source_loc_no",
+                            "crn_no", "dual_crn_no", "rgv_no", "barcode", "appe_time", "modi_time")
+                    .orderByDesc("modi_time")
+                    .orderByDesc("wrk_no")
+                    .last("limit 8"));
+            for (Map<String, Object> row : rows) {
+                Long wrkSts = toLong(row.get("wrk_sts"));
+                Map<String, Object> item = new LinkedHashMap<>();
+                item.put("wrkNo", toLong(row.get("wrk_no")));
+                item.put("taskType", resolveTaskDirectionName(wrkSts));
+                item.put("status", resolveTaskStatusName(wrkSts));
+                item.put("source", formatSiteAndLoc(row.get("source_sta_no"), row.get("source_loc_no")));
+                item.put("target", formatSiteAndLoc(row.get("sta_no"), row.get("loc_no")));
+                item.put("device", formatTaskDevice(row));
+                item.put("barcode", toText(row.get("barcode")));
+                item.put("updateTime", formatDateTime(firstNonNull(row.get("modi_time"), row.get("appe_time"))));
+                recentTasks.add(item);
+            }
+        } catch (Exception e) {
+            log.warn("dashboard recent tasks load failed: {}", safeMessage(e));
+        }
+
+        Map<String, Object> overview = new LinkedHashMap<>();
+        overview.put("total", total);
+        overview.put("running", running);
+        overview.put("manual", manual);
+        overview.put("completed", completed);
+        overview.put("newCreated", newCreated);
+        overview.put("inbound", inbound);
+        overview.put("outbound", outbound);
+        overview.put("move", move);
+
+        result.put("overview", overview);
+        result.put("directionStats", directionStats);
+        result.put("stageStats", stageStats);
+        result.put("statusStats", statusStats);
+        result.put("recentTasks", recentTasks);
+        return result;
+    }
+
+    private Map<String, Object> buildDeviceStats() {
+        Map<String, Object> result = new LinkedHashMap<>();
+
+        long stationTotal = basStationService.count(new QueryWrapper<BasStation>().eq("status", 1));
+        long crnTotal = basCrnpService.count(new QueryWrapper<BasCrnp>().eq("status", 1));
+        long dualCrnTotal = basDualCrnpService.count(new QueryWrapper<BasDualCrnp>().eq("status", 1));
+        long rgvTotal = basRgvService.count(new QueryWrapper<BasRgv>().eq("status", 1));
+
+        Set<Integer> onlineStationIds = new HashSet<>();
+        long stationBusy = 0L;
+        long stationAlarm = 0L;
+        for (DeviceConfig cfg : listDeviceConfig(SlaveType.Devp)) {
+            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, cfg.getDeviceNo());
+            if (stationThread == null) {
+                continue;
+            }
+            List<StationProtocol> statusList = stationThread.getStatus();
+            if (statusList == null) {
+                continue;
+            }
+            for (StationProtocol protocol : statusList) {
+                if (protocol == null || protocol.getStationId() == null) {
+                    continue;
+                }
+                if (onlineStationIds.add(protocol.getStationId())) {
+                    if (protocol.getTaskNo() != null && protocol.getTaskNo() > 0) {
+                        stationBusy++;
+                    }
+                    if ((protocol.getError() != null && protocol.getError() > 0) || protocol.isRunBlock()) {
+                        stationAlarm++;
+                    }
+                }
+            }
+        }
+
+        long crnOnline = 0L;
+        long crnBusy = 0L;
+        long crnAlarm = 0L;
+        for (DeviceConfig cfg : listDeviceConfig(SlaveType.Crn)) {
+            CrnThread thread = (CrnThread) SlaveConnection.get(SlaveType.Crn, cfg.getDeviceNo());
+            if (thread == null || thread.getStatus() == null) {
+                continue;
+            }
+            CrnProtocol protocol = thread.getStatus();
+            crnOnline++;
+            if (protocol.getTaskNo() != null && protocol.getTaskNo() > 0) {
+                crnBusy++;
+            }
+            if (protocol.getAlarm() != null && protocol.getAlarm() > 0) {
+                crnAlarm++;
+            }
+        }
+
+        long dualCrnOnline = 0L;
+        long dualCrnBusy = 0L;
+        long dualCrnAlarm = 0L;
+        for (DeviceConfig cfg : listDeviceConfig(SlaveType.DualCrn)) {
+            DualCrnThread thread = (DualCrnThread) SlaveConnection.get(SlaveType.DualCrn, cfg.getDeviceNo());
+            if (thread == null || thread.getStatus() == null) {
+                continue;
+            }
+            DualCrnProtocol protocol = thread.getStatus();
+            dualCrnOnline++;
+            if ((protocol.getTaskNo() != null && protocol.getTaskNo() > 0)
+                    || (protocol.getTaskNoTwo() != null && protocol.getTaskNoTwo() > 0)) {
+                dualCrnBusy++;
+            }
+            if (protocol.getAlarm() != null && protocol.getAlarm() > 0) {
+                dualCrnAlarm++;
+            }
+        }
+
+        long rgvOnline = 0L;
+        long rgvBusy = 0L;
+        long rgvAlarm = 0L;
+        for (DeviceConfig cfg : listDeviceConfig(SlaveType.Rgv)) {
+            RgvThread thread = (RgvThread) SlaveConnection.get(SlaveType.Rgv, cfg.getDeviceNo());
+            if (thread == null || thread.getStatus() == null) {
+                continue;
+            }
+            RgvProtocol protocol = thread.getStatus();
+            rgvOnline++;
+            if (protocol.getTaskNo() != null && protocol.getTaskNo() > 0) {
+                rgvBusy++;
+            }
+            if (protocol.getAlarm() != null && protocol.getAlarm() > 0) {
+                rgvAlarm++;
+            }
+        }
+
+        List<Map<String, Object>> typeStats = new ArrayList<>();
+        typeStats.add(deviceMetric("杈撻�佺珯鐐�", stationTotal, onlineStationIds.size(), stationBusy, stationAlarm));
+        typeStats.add(deviceMetric("鍫嗗灈鏈�", crnTotal, crnOnline, crnBusy, crnAlarm));
+        typeStats.add(deviceMetric("鍙屽伐浣嶅爢鍨涙満", dualCrnTotal, dualCrnOnline, dualCrnBusy, dualCrnAlarm));
+        typeStats.add(deviceMetric("RGV", rgvTotal, rgvOnline, rgvBusy, rgvAlarm));
+
+        long total = stationTotal + crnTotal + dualCrnTotal + rgvTotal;
+        long online = onlineStationIds.size() + crnOnline + dualCrnOnline + rgvOnline;
+        long busy = stationBusy + crnBusy + dualCrnBusy + rgvBusy;
+        long alarm = stationAlarm + crnAlarm + dualCrnAlarm + rgvAlarm;
+
+        Map<String, Object> overview = new LinkedHashMap<>();
+        overview.put("total", total);
+        overview.put("online", online);
+        overview.put("offline", Math.max(total - online, 0L));
+        overview.put("busy", busy);
+        overview.put("alarm", alarm);
+        overview.put("onlineRate", total <= 0 ? 0D : round2((double) online * 100D / (double) total));
+
+        result.put("overview", overview);
+        result.put("typeStats", typeStats);
+        return result;
+    }
+
+    private Map<String, Object> buildAiStats() {
+        Map<String, Object> result = new LinkedHashMap<>();
+
+        long tokenTotal = 0L;
+        long promptTokenTotal = 0L;
+        long completionTokenTotal = 0L;
+        long askCount = 0L;
+        long sessionCount = 0L;
+        try {
+            List<AiChatSession> sessions = aiChatSessionMapper.selectList(new QueryWrapper<AiChatSession>()
+                    .select("id", "sum_prompt_tokens", "sum_completion_tokens", "sum_total_tokens", "ask_count"));
+            sessionCount = sessions == null ? 0L : sessions.size();
+            if (sessions != null) {
+                for (AiChatSession session : sessions) {
+                    promptTokenTotal += safeCount(session == null ? null : session.getSumPromptTokens());
+                    completionTokenTotal += safeCount(session == null ? null : session.getSumCompletionTokens());
+                    tokenTotal += safeCount(session == null ? null : session.getSumTotalTokens());
+                    askCount += safeCount(session == null ? null : session.getAskCount());
+                }
+            }
+        } catch (Exception e) {
+            log.warn("dashboard ai session stats load failed: {}", safeMessage(e));
+        }
+
+        List<LlmRouteConfig> routes = Collections.emptyList();
+        try {
+            routes = llmRouteConfigService.list(new QueryWrapper<LlmRouteConfig>()
+                    .orderBy(true, true, "priority")
+                    .orderBy(true, true, "id"));
+        } catch (Exception e) {
+            log.warn("dashboard ai route stats load failed: {}", safeMessage(e));
+        }
+
+        Date now = new Date();
+        long routeTotal = 0L;
+        long enabledRouteCount = 0L;
+        long coolingRouteCount = 0L;
+        long availableRouteCount = 0L;
+        long disabledRouteCount = 0L;
+        List<Map<String, Object>> routeList = new ArrayList<>();
+        if (routes != null) {
+            routeTotal = routes.size();
+            for (LlmRouteConfig route : routes) {
+                boolean enabled = route != null && route.getStatus() != null && route.getStatus() == 1;
+                boolean cooling = enabled && route.getCooldownUntil() != null && route.getCooldownUntil().after(now);
+                if (enabled) {
+                    enabledRouteCount++;
+                } else {
+                    disabledRouteCount++;
+                }
+                if (cooling) {
+                    coolingRouteCount++;
+                } else if (enabled) {
+                    availableRouteCount++;
+                }
+
+                if (route != null) {
+                    Map<String, Object> item = new LinkedHashMap<>();
+                    item.put("name", defaultText(route.getName(), "鏈懡鍚嶈矾鐢�"));
+                    item.put("model", defaultText(route.getModel(), "-"));
+                    item.put("priority", route.getPriority());
+                    item.put("statusText", enabled ? (cooling ? "鍐峰嵈涓�" : "鍙敤") : "宸茬鐢�");
+                    item.put("statusType", enabled ? (cooling ? "warning" : "success") : "info");
+                    item.put("successCount", safeCount(route.getSuccessCount()));
+                    item.put("failCount", safeCount(route.getFailCount()));
+                    item.put("lastUsedTime", formatDateTime(route.getLastUsedTime()));
+                    item.put("lastError", defaultText(route.getLastError(), ""));
+                    routeList.add(item);
+                }
+            }
+        }
+
+        long llmCallTotal = 0L;
+        long successCallTotal = 0L;
+        long failCallTotal = 0L;
+        String lastCallTime = "";
+        try {
+            llmCallTotal = safeCount(llmCallLogService.count(new QueryWrapper<LlmCallLog>()));
+            successCallTotal = safeCount(llmCallLogService.count(new QueryWrapper<LlmCallLog>().eq("success", 1)));
+            failCallTotal = safeCount(llmCallLogService.count(new QueryWrapper<LlmCallLog>().eq("success", 0)));
+
+            List<Map<String, Object>> latestLog = llmCallLogService.listMaps(new QueryWrapper<LlmCallLog>()
+                    .select("create_time")
+                    .orderByDesc("id")
+                    .last("limit 1"));
+            if (latestLog != null && !latestLog.isEmpty()) {
+                lastCallTime = formatDateTime(latestLog.get(0).get("create_time"));
+            }
+        } catch (Exception e) {
+            log.warn("dashboard ai log stats load failed: {}", safeMessage(e));
+        }
+
+        Map<String, Object> overview = new LinkedHashMap<>();
+        overview.put("tokenTotal", tokenTotal);
+        overview.put("promptTokenTotal", promptTokenTotal);
+        overview.put("completionTokenTotal", completionTokenTotal);
+        overview.put("askCount", askCount);
+        overview.put("sessionCount", sessionCount);
+        overview.put("routeTotal", routeTotal);
+        overview.put("enabledRouteCount", enabledRouteCount);
+        overview.put("coolingRouteCount", coolingRouteCount);
+        overview.put("availableRouteCount", availableRouteCount);
+        overview.put("disabledRouteCount", disabledRouteCount);
+        overview.put("llmCallTotal", llmCallTotal);
+        overview.put("successCallTotal", successCallTotal);
+        overview.put("failCallTotal", failCallTotal);
+        overview.put("lastCallTime", lastCallTime);
+
+        List<Map<String, Object>> routeStats = new ArrayList<>();
+        routeStats.add(metric("鍙敤", availableRouteCount));
+        routeStats.add(metric("鍐峰嵈涓�", coolingRouteCount));
+        routeStats.add(metric("宸茬鐢�", disabledRouteCount));
+
+        result.put("overview", overview);
+        result.put("routeStats", routeStats);
+        result.put("routeList", routeList);
+        return result;
+    }
+
+    private List<DeviceConfig> listDeviceConfig(SlaveType type) {
+        try {
+            return deviceConfigService.list(new QueryWrapper<DeviceConfig>()
+                    .eq("device_type", String.valueOf(type)));
+        } catch (Exception e) {
+            log.warn("dashboard device config load failed, type={}, msg={}", type, safeMessage(e));
+            return Collections.emptyList();
+        }
+    }
+
+    private boolean isInboundTask(Long wrkSts) {
+        return wrkSts != null && wrkSts > 0 && wrkSts < 100;
+    }
+
+    private boolean isMoveTask(Long wrkSts) {
+        return wrkSts != null && wrkSts >= 500;
+    }
+
+    private boolean isManualTask(Long wrkSts) {
+        return wrkSts != null && MANUAL_TASK_STATUS.contains(wrkSts);
+    }
+
+    private boolean isCompletedTask(Long wrkSts) {
+        return wrkSts != null && COMPLETED_TASK_STATUS.contains(wrkSts);
+    }
+
+    private boolean isNewTask(Long wrkSts) {
+        return wrkSts != null && NEW_TASK_STATUS.contains(wrkSts);
+    }
+
+    private String resolveTaskDirectionName(Long wrkSts) {
+        if (isInboundTask(wrkSts)) {
+            return "鍏ュ簱浠诲姟";
+        }
+        if (isMoveTask(wrkSts)) {
+            return "绉诲簱浠诲姟";
+        }
+        return "鍑哄簱浠诲姟";
+    }
+
+    private String resolveTaskStatusName(Long wrkSts) {
+        if (wrkSts == null) {
+            return "鏈畾涔夌姸鎬�";
+        }
+        try {
+            return WrkStsType.query(wrkSts).desc;
+        } catch (Exception ignore) {
+            return "鐘舵��" + wrkSts;
+        }
+    }
+
+    private Map<String, Object> metric(String name, long value) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("name", name);
+        item.put("value", value);
+        return item;
+    }
+
+    private Map<String, Object> deviceMetric(String name, long total, long online, long busy, long alarm) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("name", name);
+        item.put("total", total);
+        item.put("online", online);
+        item.put("offline", Math.max(total - online, 0L));
+        item.put("busy", busy);
+        item.put("alarm", alarm);
+        return item;
+    }
+
+    @SuppressWarnings("unchecked")
+    private long getNestedLong(Map<String, Object> source, String key, String nestedKey) {
+        if (source == null) {
+            return 0L;
+        }
+        Object value = source.get(key);
+        if (!(value instanceof Map)) {
+            return 0L;
+        }
+        return toLong(((Map<String, Object>) value).get(nestedKey));
+    }
+
+    private Object firstNonNull(Object first, Object second) {
+        return first != null ? first : second;
+    }
+
+    private long safeCount(Number value) {
+        return value == null ? 0L : value.longValue();
+    }
+
+    private long toLong(Object value) {
+        if (value == null) {
+            return 0L;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        try {
+            return Long.parseLong(String.valueOf(value));
+        } catch (Exception e) {
+            return 0L;
+        }
+    }
+
+    private double round2(double value) {
+        return Math.round(value * 100D) / 100D;
+    }
+
+    private String formatSiteAndLoc(Object stationNo, Object locNo) {
+        String site = toText(stationNo);
+        String loc = toText(locNo);
+        if (site.isEmpty() && loc.isEmpty()) {
+            return "-";
+        }
+        if (site.isEmpty()) {
+            return loc;
+        }
+        if (loc.isEmpty()) {
+            return "绔欑偣" + site;
+        }
+        return "绔欑偣" + site + " / " + loc;
+    }
+
+    private String formatTaskDevice(Map<String, Object> row) {
+        long crnNo = toLong(row.get("crn_no"));
+        long dualCrnNo = toLong(row.get("dual_crn_no"));
+        long rgvNo = toLong(row.get("rgv_no"));
+        List<String> parts = new ArrayList<>();
+        if (crnNo > 0) {
+            parts.add("鍫嗗灈鏈�#" + crnNo);
+        }
+        if (dualCrnNo > 0) {
+            parts.add("鍙屽伐浣�#" + dualCrnNo);
+        }
+        if (rgvNo > 0) {
+            parts.add("RGV#" + rgvNo);
+        }
+        return parts.isEmpty() ? "-" : String.join(" / ", parts);
+    }
+
+    private String formatDateTime(Object value) {
+        if (value == null) {
+            return "";
+        }
+        Date date = null;
+        if (value instanceof Date) {
+            date = (Date) value;
+        } else if (value instanceof Number) {
+            date = new Date(((Number) value).longValue());
+        }
+        if (date == null) {
+            return String.valueOf(value);
+        }
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
+    }
+
+    private String toText(Object value) {
+        return value == null ? "" : String.valueOf(value).trim();
+    }
+
+    private String defaultText(String value, String fallback) {
+        return value == null || value.trim().isEmpty() ? fallback : value.trim();
+    }
+
+    private String safeMessage(Exception e) {
+        if (e == null || e.getMessage() == null) {
+            return "";
+        }
+        return e.getMessage();
+    }
+}
diff --git a/src/main/resources/i18n/en-US/messages.properties b/src/main/resources/i18n/en-US/messages.properties
index e961a33..834d853 100644
--- a/src/main/resources/i18n/en-US/messages.properties
+++ b/src/main/resources/i18n/en-US/messages.properties
@@ -90,8 +90,8 @@
 index.fakeRunning=Simulation Running
 index.fakeStopped=Simulation Stopped
 index.licenseExpiring=License Expiring Soon
-index.homeTab=Control Center
-index.homeGroup=Real-time Monitoring
+index.homeTab=System Dashboard
+index.homeGroup=System Overview
 index.profileGroup=Account Center
 index.versionLoading=Version loading...
 index.licenseExpireAt=The license will expire on {0}. Remaining validity: {1} day(s).
diff --git a/src/main/resources/i18n/zh-CN/messages.properties b/src/main/resources/i18n/zh-CN/messages.properties
index bfabe23..058262b 100644
--- a/src/main/resources/i18n/zh-CN/messages.properties
+++ b/src/main/resources/i18n/zh-CN/messages.properties
@@ -11,7 +11,7 @@
 common.profile=鍩烘湰璧勬枡
 common.logout=閫�鍑虹櫥褰�
 common.closeOtherTabs=鍏抽棴鍏朵粬椤电
-common.backHome=杩斿洖鎺у埗涓績
+common.backHome=杩斿洖浠〃鐩�
 common.aiAssistant=AI鍔╂墜
 common.workPage=宸ヤ綔椤甸潰
 common.businessPage=涓氬姟椤甸潰
@@ -90,8 +90,8 @@
 index.fakeRunning=浠跨湡杩愯涓�
 index.fakeStopped=浠跨湡鏈繍琛�
 index.licenseExpiring=璁稿彲璇佸嵆灏嗚繃鏈�
-index.homeTab=鎺у埗涓績
-index.homeGroup=瀹炴椂鐩戞帶
+index.homeTab=绯荤粺浠〃鐩�
+index.homeGroup=绯荤粺姒傝
 index.profileGroup=璐︽埛涓績
 index.versionLoading=鐗堟湰鍔犺浇涓�...
 index.licenseExpireAt=璁稿彲璇佸皢浜� {0} 杩囨湡锛屽墿浣欐湁鏁堟湡锛歿1} 澶┿��
diff --git a/src/main/webapp/static/js/dashboard/dashboard.js b/src/main/webapp/static/js/dashboard/dashboard.js
new file mode 100644
index 0000000..dc8cf57
--- /dev/null
+++ b/src/main/webapp/static/js/dashboard/dashboard.js
@@ -0,0 +1,396 @@
+(function () {
+  "use strict";
+
+  var REFRESH_SECONDS = 30;
+
+  function cloneMetricList(source) {
+    return Array.isArray(source) ? source : [];
+  }
+
+  new Vue({
+    el: "#app",
+    data: function () {
+      return {
+        loading: true,
+        refreshing: false,
+        countdown: REFRESH_SECONDS,
+        countdownTimer: null,
+        resizeHandler: null,
+        charts: {
+          taskDirection: null,
+          taskStage: null,
+          deviceType: null,
+          aiRoute: null
+        },
+        overview: {
+          systemRunning: false,
+          generatedAt: "",
+          taskTotal: 0,
+          taskRunning: 0,
+          deviceTotal: 0,
+          deviceOnline: 0,
+          deviceAlarm: 0,
+          aiTokenTotal: 0,
+          aiCallTotal: 0
+        },
+        tasks: {
+          overview: {
+            running: 0,
+            manual: 0,
+            completed: 0,
+            newCreated: 0
+          },
+          directionStats: [],
+          stageStats: [],
+          statusStats: [],
+          recentTasks: []
+        },
+        devices: {
+          overview: {
+            total: 0,
+            online: 0,
+            offline: 0,
+            busy: 0,
+            alarm: 0,
+            onlineRate: 0
+          },
+          typeStats: []
+        },
+        ai: {
+          overview: {
+            tokenTotal: 0,
+            askCount: 0,
+            llmCallTotal: 0,
+            successCallTotal: 0,
+            failCallTotal: 0,
+            sessionCount: 0,
+            availableRouteCount: 0,
+            lastCallTime: ""
+          },
+          routeStats: [],
+          routeList: []
+        }
+      };
+    },
+    mounted: function () {
+      var self = this;
+      this.$nextTick(function () {
+        self.initCharts();
+        self.loadDashboard(false);
+        self.startAutoRefresh();
+        self.bindResize();
+      });
+    },
+    beforeDestroy: function () {
+      this.clearTimers();
+      this.unbindResize();
+      this.disposeCharts();
+    },
+    methods: {
+      loadDashboard: function (manual) {
+        var self = this;
+        if (this.refreshing) {
+          return;
+        }
+        this.refreshing = true;
+        $.ajax({
+          url: baseUrl + "/dashboard/summary/auth",
+          method: "GET",
+          headers: { token: localStorage.getItem("token") },
+          success: function (res) {
+            if (res && res.code === 200) {
+              self.applyData(res.data || {});
+              self.countdown = REFRESH_SECONDS;
+              return;
+            }
+            self.$message.error((res && res.msg) || "浠〃鐩樻暟鎹姞杞藉け璐�");
+          },
+          error: function () {
+            if (manual) {
+              self.$message.error("浠〃鐩樻暟鎹姞杞藉け璐ワ紝璇锋鏌ユ帴鍙g姸鎬�");
+            }
+          },
+          complete: function () {
+            self.loading = false;
+            self.refreshing = false;
+          }
+        });
+      },
+      applyData: function (payload) {
+        this.overview = payload.overview || this.overview;
+        this.tasks = payload.tasks || this.tasks;
+        this.devices = payload.devices || this.devices;
+        this.ai = payload.ai || this.ai;
+        this.updateCharts();
+      },
+      initCharts: function () {
+        this.disposeCharts();
+        this.charts.taskDirection = echarts.init(this.$refs.taskDirectionChart);
+        this.charts.taskStage = echarts.init(this.$refs.taskStageChart);
+        this.charts.deviceType = echarts.init(this.$refs.deviceTypeChart);
+        this.charts.aiRoute = echarts.init(this.$refs.aiRouteChart);
+      },
+      updateCharts: function () {
+        if (!this.charts.taskDirection || !this.charts.taskStage || !this.charts.deviceType || !this.charts.aiRoute) {
+          return;
+        }
+
+        this.charts.taskDirection.setOption(this.buildTaskDirectionOption());
+        this.charts.taskStage.setOption(this.buildTaskStageOption());
+        this.charts.deviceType.setOption(this.buildDeviceTypeOption());
+        this.charts.aiRoute.setOption(this.buildAiRouteOption());
+        this.resizeCharts();
+      },
+      buildTaskDirectionOption: function () {
+        var data = cloneMetricList(this.tasks.directionStats);
+        return {
+          color: ["#1f6fb2", "#f59a4a", "#2fa38e"],
+          tooltip: {
+            trigger: "item",
+            formatter: "{b}<br/>{c} ({d}%)"
+          },
+          legend: {
+            bottom: 0,
+            itemWidth: 10,
+            itemHeight: 10,
+            textStyle: { color: "#60778d", fontSize: 12 }
+          },
+          series: [{
+            type: "pie",
+            radius: ["48%", "72%"],
+            center: ["50%", "46%"],
+            data: data,
+            label: {
+              color: "#40596f",
+              formatter: "{b}\n{c}"
+            },
+            labelLine: {
+              lineStyle: { color: "#c6d5e3" }
+            }
+          }]
+        };
+      },
+      buildTaskStageOption: function () {
+        var data = cloneMetricList(this.tasks.stageStats);
+        return {
+          color: ["#1f6fb2"],
+          grid: {
+            left: 48,
+            right: 18,
+            top: 24,
+            bottom: 28
+          },
+          tooltip: {
+            trigger: "axis",
+            axisPointer: { type: "shadow" }
+          },
+          xAxis: {
+            type: "category",
+            data: data.map(function (item) { return item.name; }),
+            axisLine: { lineStyle: { color: "#d4deea" } },
+            axisTick: { show: false },
+            axisLabel: { color: "#63798f", fontSize: 12 }
+          },
+          yAxis: {
+            type: "value",
+            splitLine: { lineStyle: { color: "#edf2f7" } },
+            axisLine: { show: false },
+            axisTick: { show: false },
+            axisLabel: { color: "#8699ad", fontSize: 12 }
+          },
+          series: [{
+            type: "bar",
+            barWidth: 32,
+            data: data.map(function (item) { return item.value; }),
+            itemStyle: {
+              borderRadius: [8, 8, 0, 0],
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+                offset: 0,
+                color: "#3388d2"
+              }, {
+                offset: 1,
+                color: "#1f6fb2"
+              }])
+            },
+            label: {
+              show: true,
+              position: "top",
+              color: "#36506d",
+              fontWeight: 600
+            }
+          }]
+        };
+      },
+      buildDeviceTypeOption: function () {
+        var data = cloneMetricList(this.devices.typeStats);
+        return {
+          color: ["#2fa38e", "#d8e2ec"],
+          legend: {
+            right: 0,
+            top: 0,
+            itemWidth: 10,
+            itemHeight: 10,
+            textStyle: { color: "#60778d", fontSize: 12 },
+            data: ["鍦ㄧ嚎", "绂荤嚎"]
+          },
+          grid: {
+            left: 76,
+            right: 12,
+            top: 42,
+            bottom: 18
+          },
+          tooltip: {
+            trigger: "axis",
+            axisPointer: { type: "shadow" }
+          },
+          xAxis: {
+            type: "value",
+            splitLine: { lineStyle: { color: "#edf2f7" } },
+            axisLine: { show: false },
+            axisTick: { show: false },
+            axisLabel: { color: "#8398ab", fontSize: 12 }
+          },
+          yAxis: {
+            type: "category",
+            data: data.map(function (item) { return item.name; }),
+            axisLine: { show: false },
+            axisTick: { show: false },
+            axisLabel: { color: "#60778d", fontSize: 12 }
+          },
+          series: [{
+            name: "鍦ㄧ嚎",
+            type: "bar",
+            stack: "device",
+            barWidth: 18,
+            data: data.map(function (item) { return item.online; }),
+            itemStyle: {
+              borderRadius: [9, 0, 0, 9]
+            }
+          }, {
+            name: "绂荤嚎",
+            type: "bar",
+            stack: "device",
+            barWidth: 18,
+            data: data.map(function (item) { return item.offline; }),
+            itemStyle: {
+              borderRadius: [0, 9, 9, 0]
+            }
+          }]
+        };
+      },
+      buildAiRouteOption: function () {
+        var data = cloneMetricList(this.ai.routeStats);
+        return {
+          color: ["#2fa38e", "#f59a4a", "#c8d4e1"],
+          tooltip: {
+            trigger: "item",
+            formatter: "{b}<br/>{c} ({d}%)"
+          },
+          graphic: [{
+            type: "text",
+            left: "center",
+            top: "39%",
+            style: {
+              text: this.formatNumber(this.ai.overview.availableRouteCount || 0),
+              fill: "#1f3142",
+              fontSize: 28,
+              fontWeight: 700
+            }
+          }, {
+            type: "text",
+            left: "center",
+            top: "55%",
+            style: {
+              text: "鍙敤璺敱",
+              fill: "#7c8fa4",
+              fontSize: 12
+            }
+          }],
+          legend: {
+            bottom: 0,
+            itemWidth: 10,
+            itemHeight: 10,
+            textStyle: { color: "#60778d", fontSize: 12 }
+          },
+          series: [{
+            type: "pie",
+            radius: ["56%", "76%"],
+            center: ["50%", "43%"],
+            avoidLabelOverlap: false,
+            label: { show: false },
+            labelLine: { show: false },
+            data: data
+          }]
+        };
+      },
+      formatNumber: function (value) {
+        var num = Number(value || 0);
+        if (!isFinite(num)) {
+          return "0";
+        }
+        return num.toLocaleString("zh-CN");
+      },
+      displayText: function (value, fallback) {
+        return value == null || value === "" ? (fallback || "") : value;
+      },
+      startAutoRefresh: function () {
+        var self = this;
+        this.clearTimers();
+        this.countdownTimer = setInterval(function () {
+          if (self.countdown <= 1) {
+            self.countdown = REFRESH_SECONDS;
+            self.loadDashboard(false);
+            return;
+          }
+          self.countdown -= 1;
+        }, 1000);
+      },
+      clearTimers: function () {
+        if (this.countdownTimer) {
+          clearInterval(this.countdownTimer);
+          this.countdownTimer = null;
+        }
+      },
+      bindResize: function () {
+        var self = this;
+        this.resizeHandler = function () {
+          self.resizeCharts();
+        };
+        window.addEventListener("resize", this.resizeHandler);
+      },
+      unbindResize: function () {
+        if (this.resizeHandler) {
+          window.removeEventListener("resize", this.resizeHandler);
+          this.resizeHandler = null;
+        }
+      },
+      resizeCharts: function () {
+        var key;
+        for (key in this.charts) {
+          if (this.charts.hasOwnProperty(key) && this.charts[key]) {
+            this.charts[key].resize();
+          }
+        }
+      },
+      disposeCharts: function () {
+        var key;
+        for (key in this.charts) {
+          if (this.charts.hasOwnProperty(key) && this.charts[key]) {
+            this.charts[key].dispose();
+            this.charts[key] = null;
+          }
+        }
+      },
+      openMonitor: function () {
+        if (window.parent && window.parent.index && typeof window.parent.index.loadView === "function") {
+          window.parent.index.loadView({
+            menuPath: "/views/watch/console.html",
+            menuName: "鐩戞帶宸ヤ綔鍙�"
+          });
+          return;
+        }
+        window.open(baseUrl + "/views/watch/console.html", "_blank");
+      }
+    }
+  });
+}());
diff --git a/src/main/webapp/views/dashboard/dashboard.html b/src/main/webapp/views/dashboard/dashboard.html
new file mode 100644
index 0000000..857578d
--- /dev/null
+++ b/src/main/webapp/views/dashboard/dashboard.html
@@ -0,0 +1,863 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>绯荤粺浠〃鐩�</title>
+  <link rel="stylesheet" href="../../static/vue/element/element.css" />
+  <style>
+    :root {
+      --bg-main: #eef3f7;
+      --panel-bg: rgba(255, 255, 255, 0.92);
+      --panel-border: rgba(203, 216, 228, 0.92);
+      --panel-shadow: 0 18px 38px rgba(34, 61, 92, 0.08);
+      --text-main: #1f3142;
+      --text-sub: #6d7f92;
+      --accent: #1f6fb2;
+      --accent-2: #2fa38e;
+      --accent-3: #f59a4a;
+      --accent-4: #de5c5c;
+    }
+
+    [v-cloak] {
+      display: none;
+    }
+
+    * {
+      box-sizing: border-box;
+    }
+
+    html,
+    body {
+      margin: 0;
+      min-height: 100%;
+      background:
+        radial-gradient(900px 340px at -10% -10%, rgba(31, 111, 178, 0.16), transparent 54%),
+        radial-gradient(780px 320px at 110% 0%, rgba(47, 163, 142, 0.14), transparent 58%),
+        linear-gradient(180deg, #f3f7fb 0%, #edf2f6 100%);
+      color: var(--text-main);
+      font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
+    }
+
+    body {
+      padding: 16px;
+    }
+
+    .dashboard-shell {
+      max-width: 1680px;
+      margin: 0 auto;
+    }
+
+    .hero {
+      position: relative;
+      overflow: hidden;
+      border-radius: 24px;
+      padding: 22px 24px 20px;
+      background:
+        radial-gradient(400px 180px at 0% 0%, rgba(255, 255, 255, 0.16), transparent 60%),
+        linear-gradient(135deg, #0f4c81 0%, #1e6aa3 48%, #239a87 100%);
+      box-shadow: 0 22px 42px rgba(18, 57, 92, 0.18);
+      color: #fff;
+    }
+
+    .hero::after {
+      content: "";
+      position: absolute;
+      right: -60px;
+      top: -70px;
+      width: 240px;
+      height: 240px;
+      border-radius: 50%;
+      background: rgba(255, 255, 255, 0.08);
+      filter: blur(6px);
+    }
+
+    .hero-main {
+      position: relative;
+      z-index: 1;
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-start;
+      gap: 18px;
+      flex-wrap: wrap;
+    }
+
+    .hero-copy {
+      min-width: 0;
+      max-width: 760px;
+    }
+
+    .hero-eyebrow {
+      font-size: 12px;
+      letter-spacing: 0.16em;
+      text-transform: uppercase;
+      opacity: 0.82;
+    }
+
+    .hero-title {
+      margin: 8px 0 0;
+      font-size: 30px;
+      line-height: 1.15;
+      font-weight: 700;
+    }
+
+    .hero-desc {
+      margin: 10px 0 0;
+      max-width: 720px;
+      font-size: 14px;
+      line-height: 1.7;
+      color: rgba(255, 255, 255, 0.88);
+    }
+
+    .hero-stat-grid {
+      position: relative;
+      z-index: 1;
+      margin-top: 14px;
+      display: grid;
+      grid-template-columns: 1fr;
+      gap: 12px;
+    }
+
+    .hero-stat-row {
+      display: grid;
+      gap: 8px;
+    }
+
+    .hero-row-head {
+      display: flex;
+      justify-content: space-between;
+      align-items: baseline;
+      gap: 12px;
+    }
+
+    .hero-row-kicker {
+      font-size: 11px;
+      color: rgba(255, 255, 255, 0.78);
+      letter-spacing: 0.14em;
+      text-transform: uppercase;
+      font-weight: 700;
+    }
+
+    .hero-row-note {
+      font-size: 11px;
+      color: rgba(255, 255, 255, 0.62);
+      line-height: 1.4;
+    }
+
+    .hero-status-grid {
+      display: grid;
+      grid-template-columns: repeat(3, minmax(0, 1fr));
+      gap: 10px;
+    }
+
+    .hero-metric-grid {
+      display: grid;
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      gap: 10px;
+    }
+
+    .hero-actions {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      flex-wrap: wrap;
+      justify-content: flex-end;
+      text-align: right;
+    }
+
+    .hero-meta,
+    .summary-card {
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
+      gap: 6px;
+      min-height: 78px;
+      padding: 10px 14px;
+      border-radius: 16px;
+      background: linear-gradient(180deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.08) 100%);
+      border: 1px solid rgba(255, 255, 255, 0.18);
+      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
+      backdrop-filter: blur(4px);
+      min-width: 0;
+    }
+
+    .hero-meta-label,
+    .summary-card .label {
+      font-size: 10px;
+      color: rgba(255, 255, 255, 0.72);
+      letter-spacing: 0.08em;
+      text-transform: uppercase;
+    }
+
+    .hero-meta-value,
+    .summary-card .value {
+      margin-top: 4px;
+      font-size: 18px;
+      line-height: 1.15;
+      font-weight: 700;
+      color: #fff;
+      word-break: break-word;
+    }
+
+    .hero-meta-desc,
+    .summary-card .desc {
+      margin-top: 4px;
+      font-size: 11px;
+      line-height: 1.35;
+      color: rgba(255, 255, 255, 0.84);
+    }
+
+    .hero-actions .el-button {
+      min-width: 120px;
+      height: 40px;
+      padding: 0 18px;
+      border-radius: 12px;
+      font-size: 13px;
+      box-shadow: 0 8px 16px rgba(16, 53, 86, 0.12);
+    }
+
+    .dashboard-main {
+      display: grid;
+      grid-template-columns: minmax(0, 1.22fr) minmax(380px, 0.86fr);
+      gap: 16px;
+      margin-top: 16px;
+      align-items: start;
+    }
+
+    .dashboard-column {
+      display: flex;
+      flex-direction: column;
+      gap: 16px;
+      min-width: 0;
+    }
+
+    .panel {
+      border-radius: 22px;
+      background:
+        linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(250, 252, 255, 0.92) 100%);
+      border: 1px solid var(--panel-border);
+      box-shadow: var(--panel-shadow);
+      padding: 18px;
+      min-height: 0;
+    }
+
+    .panel-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: flex-start;
+      gap: 12px;
+      margin-bottom: 14px;
+    }
+
+    .panel-title {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 700;
+      color: var(--text-main);
+    }
+
+    .panel-desc {
+      margin-top: 6px;
+      font-size: 12px;
+      color: var(--text-sub);
+      line-height: 1.6;
+    }
+
+    .panel-kicker {
+      font-size: 11px;
+      color: #88a0b9;
+      letter-spacing: 0.1em;
+      text-transform: uppercase;
+      font-weight: 700;
+    }
+
+    .mini-grid {
+      display: grid;
+      grid-template-columns: repeat(4, minmax(0, 1fr));
+      gap: 10px;
+      margin-bottom: 14px;
+    }
+
+    .mini-card {
+      padding: 12px 12px 10px;
+      border-radius: 16px;
+      background: #f7fafc;
+      border: 1px solid #e4edf5;
+    }
+
+    .mini-card .mini-label {
+      font-size: 11px;
+      color: #7a8fa6;
+    }
+
+    .mini-card .mini-value {
+      margin-top: 8px;
+      font-size: 24px;
+      line-height: 1.05;
+      font-weight: 700;
+      color: #213547;
+    }
+
+    .mini-card .mini-hint {
+      margin-top: 8px;
+      font-size: 11px;
+      color: #92a2b3;
+    }
+
+    .task-mini-running {
+      background: linear-gradient(180deg, rgba(31, 111, 178, 0.09) 0%, rgba(31, 111, 178, 0.02) 100%);
+      border-color: rgba(31, 111, 178, 0.16);
+    }
+
+    .task-mini-manual {
+      background: linear-gradient(180deg, rgba(245, 154, 74, 0.12) 0%, rgba(245, 154, 74, 0.03) 100%);
+      border-color: rgba(245, 154, 74, 0.18);
+    }
+
+    .task-mini-completed {
+      background: linear-gradient(180deg, rgba(47, 163, 142, 0.11) 0%, rgba(47, 163, 142, 0.03) 100%);
+      border-color: rgba(47, 163, 142, 0.18);
+    }
+
+    .task-mini-new {
+      background: linear-gradient(180deg, rgba(151, 110, 204, 0.10) 0%, rgba(151, 110, 204, 0.03) 100%);
+      border-color: rgba(151, 110, 204, 0.18);
+    }
+
+    .chart-grid {
+      display: grid;
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+      gap: 12px;
+    }
+
+    .chart-card {
+      border-radius: 18px;
+      background: #fbfdff;
+      border: 1px solid #e5edf6;
+      padding: 12px;
+    }
+
+    .chart-title {
+      font-size: 13px;
+      font-weight: 700;
+      color: #31506f;
+      margin-bottom: 8px;
+    }
+
+    .chart-box {
+      width: 100%;
+      height: 280px;
+    }
+
+    .panel-device .mini-grid,
+    .panel-ai .mini-grid {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+
+    .status-flow {
+      margin-top: 12px;
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+    }
+
+    .status-chip {
+      min-width: 144px;
+      padding: 10px 12px;
+      border-radius: 14px;
+      background: #f5f8fb;
+      border: 1px solid #e2eaf2;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .status-chip-name {
+      font-size: 12px;
+      color: #5f7488;
+      line-height: 1.5;
+    }
+
+    .status-chip-value {
+      font-size: 18px;
+      font-weight: 700;
+      color: #203647;
+      white-space: nowrap;
+    }
+
+    .device-chart-box,
+    .ai-chart-box {
+      width: 100%;
+      height: 250px;
+    }
+
+    .type-list,
+    .route-list {
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+      margin-top: 14px;
+    }
+
+    .type-row,
+    .route-row {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 12px;
+      padding: 12px 14px;
+      border-radius: 16px;
+      background: #f9fbfd;
+      border: 1px solid #e2eaf3;
+    }
+
+    .type-row-main,
+    .route-row-main {
+      min-width: 0;
+      flex: 1;
+    }
+
+    .type-row-name,
+    .route-row-name {
+      font-size: 14px;
+      font-weight: 700;
+      color: #28425c;
+      line-height: 1.4;
+    }
+
+    .type-row-desc,
+    .route-row-desc {
+      margin-top: 4px;
+      font-size: 12px;
+      color: #8092a5;
+      line-height: 1.6;
+    }
+
+    .type-row-side {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      flex-wrap: wrap;
+      justify-content: flex-end;
+    }
+
+    .route-row-side {
+      display: flex;
+      flex-direction: column;
+      align-items: flex-end;
+      gap: 6px;
+      text-align: right;
+    }
+
+    .route-extra {
+      font-size: 12px;
+      color: #7d90a4;
+      line-height: 1.5;
+    }
+
+    .route-error {
+      margin-top: 6px;
+      font-size: 12px;
+      color: #c15b5b;
+      line-height: 1.5;
+      background: rgba(222, 92, 92, 0.08);
+      border-radius: 12px;
+      padding: 8px 10px;
+    }
+
+    .recent-panel {
+      min-height: 100%;
+    }
+
+    .recent-table {
+      border-radius: 16px;
+      overflow: hidden;
+      border: 1px solid #e5edf6;
+    }
+
+    .loading-mask {
+      position: fixed;
+      inset: 0;
+      z-index: 90;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: rgba(241, 246, 251, 0.72);
+      backdrop-filter: blur(3px);
+    }
+
+    .loading-card {
+      min-width: 240px;
+      padding: 18px 22px;
+      border-radius: 18px;
+      background: rgba(255, 255, 255, 0.92);
+      border: 1px solid #d8e4ef;
+      box-shadow: 0 18px 34px rgba(38, 60, 87, 0.12);
+      text-align: center;
+      color: #36506d;
+    }
+
+    .loading-title {
+      margin-top: 12px;
+      font-size: 14px;
+      font-weight: 600;
+    }
+
+    .loading-desc {
+      margin-top: 6px;
+      font-size: 12px;
+      color: #7e92a7;
+    }
+
+    @media (max-width: 1360px) {
+      .hero-copy {
+        max-width: none;
+      }
+
+      .dashboard-main {
+        grid-template-columns: 1fr;
+      }
+    }
+
+    @media (max-width: 1080px) {
+      body {
+        padding: 12px;
+      }
+
+      .mini-grid,
+      .chart-grid {
+        grid-template-columns: 1fr;
+      }
+
+      .hero {
+        padding: 18px;
+      }
+
+      .hero-title {
+        font-size: 26px;
+      }
+
+      .hero-status-grid,
+      .hero-metric-grid {
+        grid-template-columns: repeat(2, minmax(0, 1fr));
+      }
+
+      .hero-actions {
+        justify-content: flex-start;
+        text-align: left;
+      }
+
+      .panel {
+        padding: 16px;
+      }
+
+      .panel-device .mini-grid,
+      .panel-ai .mini-grid {
+        grid-template-columns: 1fr;
+      }
+    }
+
+    @media (max-width: 640px) {
+      .hero-actions .el-button {
+        width: 100%;
+      }
+
+      .hero-status-grid,
+      .hero-metric-grid {
+        grid-template-columns: 1fr;
+      }
+
+      .status-chip,
+      .type-row,
+      .route-row {
+        flex-direction: column;
+        align-items: flex-start;
+      }
+
+      .type-row-side,
+      .route-row-side {
+        align-items: flex-start;
+        text-align: left;
+      }
+    }
+  </style>
+</head>
+<body>
+<div id="app" class="dashboard-shell" v-cloak>
+  <section class="hero">
+    <div class="hero-main">
+      <div class="hero-copy">
+        <div class="hero-eyebrow">WCS Dashboard</div>
+        <h1 class="hero-title">绯荤粺浠〃鐩�</h1>
+      </div>
+      <div class="hero-actions">
+        <el-button size="small" plain @click="openMonitor">鎵撳紑鐩戞帶鐢婚潰</el-button>
+        <el-button size="small" type="primary" :loading="refreshing" @click="loadDashboard(true)">绔嬪嵆鍒锋柊</el-button>
+      </div>
+    </div>
+
+    <div class="hero-stat-grid">
+      <div class="hero-stat-row">
+        <div class="hero-row-head">
+          <div class="hero-row-kicker">鐘舵�佹瑙�</div>
+          <div class="hero-row-note">绯荤粺涓庡埛鏂拌妭濂�</div>
+        </div>
+        <div class="hero-status-grid">
+          <div class="hero-meta">
+            <div class="hero-meta-label">绯荤粺鐘舵��</div>
+            <div class="hero-meta-value">{{ overview.systemRunning ? '杩愯涓�' : '宸叉殏鍋�' }}</div>
+            <div class="hero-meta-desc">WCS 涓绘湇鍔″綋鍓嶇姸鎬�</div>
+          </div>
+          <div class="hero-meta">
+            <div class="hero-meta-label">鏈�杩戝埛鏂�</div>
+            <div class="hero-meta-value">{{ displayText(overview.generatedAt, '-') }}</div>
+            <div class="hero-meta-desc">鏈�杩戜竴娆¤仛鍚堟暟鎹敓鎴愭椂闂�</div>
+          </div>
+          <div class="hero-meta">
+            <div class="hero-meta-label">鑷姩鍒锋柊</div>
+            <div class="hero-meta-value">{{ countdown }}s 鍚庡埛鏂�</div>
+            <div class="hero-meta-desc">椤甸潰鑷姩鏇存柊鍊掕鏃�</div>
+          </div>
+        </div>
+      </div>
+      <div class="hero-stat-row">
+        <div class="hero-row-head">
+          <div class="hero-row-kicker">鏍稿績鎸囨爣</div>
+          <div class="hero-row-note">浠诲姟銆佽澶囦笌 AI 鎬昏</div>
+        </div>
+        <div class="hero-metric-grid">
+          <div class="summary-card">
+            <div class="label">浠诲姟鎬绘暟</div>
+            <div class="value">{{ formatNumber(overview.taskTotal) }}</div>
+            <div class="desc">褰撳墠鎵ц涓� {{ formatNumber(overview.taskRunning) }}</div>
+          </div>
+          <div class="summary-card">
+            <div class="label">鍦ㄧ嚎璁惧</div>
+            <div class="value">{{ formatNumber(overview.deviceOnline) }}</div>
+            <div class="desc">鎬昏澶� {{ formatNumber(overview.deviceTotal) }}锛屽憡璀� {{ formatNumber(overview.deviceAlarm) }}</div>
+          </div>
+          <div class="summary-card">
+            <div class="label">AI 绱 Tokens</div>
+            <div class="value">{{ formatNumber(overview.aiTokenTotal) }}</div>
+            <div class="desc">鎸� AI 浼氳瘽绱缁熻</div>
+          </div>
+          <div class="summary-card">
+            <div class="label">LLM 璋冪敤娆℃暟</div>
+            <div class="value">{{ formatNumber(overview.aiCallTotal) }}</div>
+            <div class="desc">鏈�杩戜竴杞繍琛屾儏鍐靛凡绾冲叆涓嬫柟 AI 鍖哄煙</div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </section>
+
+  <div class="dashboard-main">
+    <div class="dashboard-column">
+      <section class="panel panel-task">
+        <div class="panel-header">
+          <div>
+            <div class="panel-kicker">Task</div>
+            <h2 class="panel-title">浠诲姟鎬佸娍</h2>
+            <div class="panel-desc">浠庝换鍔$被鍨嬨�佹墽琛岄樁娈靛拰鏈�杩戞祦杞褰曞揩閫熷垽鏂綋鍓嶄綔涓氬帇鍔涖��</div>
+          </div>
+        </div>
+
+        <div class="mini-grid">
+          <div class="mini-card task-mini-running">
+            <div class="mini-label">鎵ц涓�</div>
+            <div class="mini-value">{{ formatNumber(tasks.overview.running) }}</div>
+            <div class="mini-hint">褰撳墠姝e湪娴佽浆鐨勪换鍔�</div>
+          </div>
+          <div class="mini-card task-mini-manual">
+            <div class="mini-label">寰呬汉宸�</div>
+            <div class="mini-value">{{ formatNumber(tasks.overview.manual) }}</div>
+            <div class="mini-hint">闇�浜哄伐鍏虫敞鎴栧洖婊�</div>
+          </div>
+          <div class="mini-card task-mini-completed">
+            <div class="mini-label">宸插畬鎴�</div>
+            <div class="mini-value">{{ formatNumber(tasks.overview.completed) }}</div>
+            <div class="mini-hint">宸茬粡瀹屾垚鎴栬惤璐�</div>
+          </div>
+          <div class="mini-card task-mini-new">
+            <div class="mini-label">鏂板缓</div>
+            <div class="mini-value">{{ formatNumber(tasks.overview.newCreated) }}</div>
+            <div class="mini-hint">鍒氳繘鍏ヨ皟搴︽祦绋�</div>
+          </div>
+        </div>
+
+        <div class="chart-grid">
+          <div class="chart-card">
+            <div class="chart-title">浠诲姟绫诲瀷鍒嗗竷</div>
+            <div ref="taskDirectionChart" class="chart-box"></div>
+          </div>
+          <div class="chart-card">
+            <div class="chart-title">浠诲姟闃舵姒傝</div>
+            <div ref="taskStageChart" class="chart-box"></div>
+          </div>
+        </div>
+
+        <div class="status-flow">
+          <div v-for="item in tasks.statusStats" :key="item.name" class="status-chip">
+            <div class="status-chip-name">{{ item.name }}</div>
+            <div class="status-chip-value">{{ formatNumber(item.value) }}</div>
+          </div>
+        </div>
+      </section>
+
+      <section class="panel recent-panel panel-recent">
+        <div class="panel-header">
+          <div>
+            <div class="panel-kicker">Recent</div>
+            <h2 class="panel-title">鏈�杩戜换鍔�</h2>
+            <div class="panel-desc">甯姪蹇�熷垽鏂换鍔℃槸鍚﹀爢绉�佹槸鍚﹁璁惧鎺ユ墜锛屼互鍙婃渶杩戠殑浠诲姟鐩爣浣嶇疆銆�</div>
+          </div>
+        </div>
+
+        <div class="recent-table">
+          <el-table
+              :data="tasks.recentTasks"
+              stripe
+              size="mini"
+              height="360"
+              empty-text="鏆傛棤浠诲姟璁板綍">
+            <el-table-column prop="wrkNo" label="浠诲姟鍙�" min-width="100"></el-table-column>
+            <el-table-column prop="taskType" label="浠诲姟绫诲瀷" min-width="110"></el-table-column>
+            <el-table-column prop="status" label="鐘舵��" min-width="160" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="source" label="鏉ユ簮" min-width="170" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="target" label="鐩爣" min-width="170" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="device" label="鎵ц璁惧" min-width="180" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="barcode" label="鏉$爜" min-width="150" show-overflow-tooltip></el-table-column>
+            <el-table-column prop="updateTime" label="鏈�杩戞洿鏂版椂闂�" min-width="170"></el-table-column>
+          </el-table>
+        </div>
+      </section>
+    </div>
+
+    <div class="dashboard-column">
+      <section class="panel panel-device">
+        <div class="panel-header">
+          <div>
+            <div class="panel-kicker">Devices</div>
+            <h2 class="panel-title">璁惧鎬佸娍</h2>
+            <div class="panel-desc">姹囨�昏緭閫佺珯鐐广�佸爢鍨涙満銆佸弻宸ヤ綅鍫嗗灈鏈轰笌 RGV 鐨勫湪绾裤�佸繖纰屽拰鍛婅鎯呭喌銆�</div>
+          </div>
+          <el-tag size="small" type="info">鍦ㄧ嚎鐜� {{ devices.overview.onlineRate || 0 }}%</el-tag>
+        </div>
+
+        <div class="mini-grid">
+          <div class="mini-card">
+            <div class="mini-label">璁惧鎬绘暟</div>
+            <div class="mini-value">{{ formatNumber(devices.overview.total) }}</div>
+            <div class="mini-hint">宸插惎鐢ㄩ厤缃澶�</div>
+          </div>
+          <div class="mini-card">
+            <div class="mini-label">鍦ㄧ嚎璁惧</div>
+            <div class="mini-value">{{ formatNumber(devices.overview.online) }}</div>
+            <div class="mini-hint">瀹炴椂杩為�氳澶囨暟閲�</div>
+          </div>
+          <div class="mini-card">
+            <div class="mini-label">蹇欑璁惧</div>
+            <div class="mini-value">{{ formatNumber(devices.overview.busy) }}</div>
+            <div class="mini-hint">褰撳墠鎵胯浇浠诲姟鐨勮澶�</div>
+          </div>
+          <div class="mini-card">
+            <div class="mini-label">鍛婅璁惧</div>
+            <div class="mini-value">{{ formatNumber(devices.overview.alarm) }}</div>
+            <div class="mini-hint">鍚樆濉炴垨鎶ヨ鐘舵��</div>
+          </div>
+        </div>
+
+        <div class="chart-card">
+          <div class="chart-title">璁惧鍦ㄧ嚎鍒嗗竷</div>
+          <div ref="deviceTypeChart" class="device-chart-box"></div>
+        </div>
+
+        <div class="type-list">
+          <div v-for="item in devices.typeStats" :key="item.name" class="type-row">
+            <div class="type-row-main">
+              <div class="type-row-name">{{ item.name }}</div>
+              <div class="type-row-desc">鍦ㄧ嚎 {{ formatNumber(item.online) }} / 鎬绘暟 {{ formatNumber(item.total) }}锛岀绾� {{ formatNumber(item.offline) }}</div>
+            </div>
+            <div class="type-row-side">
+              <el-tag size="mini" type="success">蹇欑 {{ formatNumber(item.busy) }}</el-tag>
+              <el-tag size="mini" :type="item.alarm > 0 ? 'danger' : 'info'">鍛婅 {{ formatNumber(item.alarm) }}</el-tag>
+            </div>
+          </div>
+        </div>
+      </section>
+
+      <section class="panel panel-ai">
+        <div class="panel-header">
+          <div>
+            <div class="panel-kicker">AI</div>
+            <h2 class="panel-title">AI 杩愯鎯呭喌</h2>
+            <div class="panel-desc">鏌ョ湅 AI 浼氳瘽绱 Tokens銆丩LM 璋冪敤閲忥紝浠ュ強璺敱鐨勫彲鐢ㄤ笌鍐峰嵈鐘舵�併��</div>
+          </div>
+          <el-tag size="small" type="success">鍙敤璺敱 {{ formatNumber(ai.overview.availableRouteCount) }}</el-tag>
+        </div>
+
+        <div class="mini-grid">
+          <div class="mini-card">
+            <div class="mini-label">绱 Tokens</div>
+            <div class="mini-value">{{ formatNumber(ai.overview.tokenTotal) }}</div>
+            <div class="mini-hint">Prompt + Completion</div>
+          </div>
+          <div class="mini-card">
+            <div class="mini-label">鎻愰棶杞</div>
+            <div class="mini-value">{{ formatNumber(ai.overview.askCount) }}</div>
+            <div class="mini-hint">AI 瀵硅瘽绱杞</div>
+          </div>
+          <div class="mini-card">
+            <div class="mini-label">LLM 璋冪敤</div>
+            <div class="mini-value">{{ formatNumber(ai.overview.llmCallTotal) }}</div>
+            <div class="mini-hint">鎴愬姛 {{ formatNumber(ai.overview.successCallTotal) }} / 澶辫触 {{ formatNumber(ai.overview.failCallTotal) }}</div>
+          </div>
+          <div class="mini-card">
+            <div class="mini-label">浼氳瘽鏁�</div>
+            <div class="mini-value">{{ formatNumber(ai.overview.sessionCount) }}</div>
+            <div class="mini-hint">鏈�杩戣皟鐢� {{ displayText(ai.overview.lastCallTime, '-') }}</div>
+          </div>
+        </div>
+
+        <div class="chart-card">
+          <div class="chart-title">AI 璺敱鐘舵��</div>
+          <div ref="aiRouteChart" class="ai-chart-box"></div>
+        </div>
+
+        <div class="route-list" v-if="ai.routeList.length">
+          <div v-for="route in ai.routeList.slice(0, 6)" :key="route.name + '-' + route.model + '-' + route.priority" class="route-row">
+            <div class="route-row-main">
+              <div class="route-row-name">{{ route.name }}</div>
+              <div class="route-row-desc">妯″瀷 {{ displayText(route.model, '-') }}锛屼紭鍏堢骇 {{ displayText(route.priority, '-') }}</div>
+              <div v-if="route.lastError" class="route-error">{{ route.lastError }}</div>
+            </div>
+            <div class="route-row-side">
+              <el-tag size="mini" :type="route.statusType">{{ route.statusText }}</el-tag>
+              <div class="route-extra">鎴愬姛 {{ formatNumber(route.successCount) }} / 澶辫触 {{ formatNumber(route.failCount) }}</div>
+              <div class="route-extra">鏈�杩戜娇鐢� {{ displayText(route.lastUsedTime, '-') }}</div>
+            </div>
+          </div>
+        </div>
+        <el-empty v-else description="鏆傛棤 AI 璺敱鏁版嵁"></el-empty>
+      </section>
+    </div>
+  </div>
+
+  <div v-if="loading" class="loading-mask">
+    <div class="loading-card">
+      <i class="el-icon-loading" style="font-size: 26px;"></i>
+      <div class="loading-title">姝e湪鍔犺浇浠〃鐩�</div>
+      <div class="loading-desc">姹囨�讳换鍔°�佽澶囦笌 AI 杩愯鏁版嵁锛岃绋嶅��...</div>
+    </div>
+  </div>
+</div>
+
+<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="../../static/js/common.js"></script>
+<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+<script type="text/javascript" src="../../static/vue/element/element.js"></script>
+<script type="text/javascript" src="../../static/js/echarts/echarts.min.js"></script>
+<script type="text/javascript" src="../../static/js/dashboard/dashboard.js"></script>
+</body>
+</html>
diff --git a/src/main/webapp/views/index.html b/src/main/webapp/views/index.html
index 37d8d39..12d725e 100644
--- a/src/main/webapp/views/index.html
+++ b/src/main/webapp/views/index.html
@@ -862,13 +862,15 @@
 <script type="text/javascript" src="../static/vue/js/vue.min.js"></script>
 <script type="text/javascript" src="../static/vue/element/element.js"></script>
 <script>
+  var DASHBOARD_VIEW_VERSION = "20260316-hero-two-rows-flat-compact";
   var HOME_TAB_CONFIG = {
-    title: "鎺у埗涓績",
-    url: baseUrl + "/views/watch/console.html",
+    title: "绯荤粺浠〃鐩�",
+    url: baseUrl + "/views/dashboard/dashboard.html?layoutVersion=" + encodeURIComponent(DASHBOARD_VIEW_VERSION),
     home: true,
-    group: "瀹炴椂鐩戞帶",
+    group: "绯荤粺姒傝",
     menuKey: ""
   };
+  var LEGACY_HOME_TAB_URL = baseUrl + "/views/watch/console.html";
   var PROFILE_TAB_CONFIG = {
     title: "鍩烘湰璧勬枡",
     url: baseUrl + "/views/detail.html?resourceId=8",
@@ -891,7 +893,7 @@
     "common.profile": "鍩烘湰璧勬枡",
     "common.logout": "閫�鍑虹櫥褰�",
     "common.closeOtherTabs": "鍏抽棴鍏朵粬椤电",
-    "common.backHome": "杩斿洖鎺у埗涓績",
+    "common.backHome": "杩斿洖浠〃鐩�",
     "common.aiAssistant": "AI鍔╂墜",
     "common.workPage": "宸ヤ綔椤甸潰",
     "common.businessPage": "涓氬姟椤甸潰",
@@ -902,8 +904,8 @@
     "index.fakeRunning": "浠跨湡杩愯涓�",
     "index.fakeStopped": "浠跨湡鏈繍琛�",
     "index.licenseExpiring": "璁稿彲璇佸嵆灏嗚繃鏈�",
-    "index.homeTab": "鎺у埗涓績",
-    "index.homeGroup": "瀹炴椂鐩戞帶",
+    "index.homeTab": "绯荤粺浠〃鐩�",
+    "index.homeGroup": "绯荤粺姒傝",
     "index.profileGroup": "璐︽埛涓績",
     "index.versionLoading": "Version loading...",
     "index.licenseExpireAt": "璁稿彲璇佸皢浜� {0} 杩囨湡锛屽墿浣欐湁鏁堟湡锛歿1} 澶┿��",
@@ -1257,12 +1259,16 @@
         };
       },
       normalizeStoredTab: function (tab) {
+        var homeConfig = this.resolveHomeConfig();
+        var resolvedUrl = this.resolveViewSrc(tab.url);
+        var isLegacyHome = resolvedUrl === this.resolveViewSrc(LEGACY_HOME_TAB_URL);
+        var isHome = !!tab.home || isLegacyHome;
         var created = this.createTab({
-          title: this.translateTabTitle(tab.title),
-          url: this.resolveViewSrc(tab.url),
-          home: !!tab.home,
-          group: this.tl(tab.group || ""),
-          menuKey: tab.menuKey || ""
+          title: isHome ? homeConfig.title : this.translateTabTitle(tab.title),
+          url: isHome ? homeConfig.url : resolvedUrl,
+          home: isHome,
+          group: isHome ? homeConfig.group : this.tl(tab.group || ""),
+          menuKey: isHome ? homeConfig.menuKey : (tab.menuKey || "")
         });
         created.loaded = false;
         return created;
@@ -1285,6 +1291,9 @@
               }
             }
             active = parsed ? parsed.activeTab : "";
+            if (this.resolveViewSrc(active) === this.resolveViewSrc(LEGACY_HOME_TAB_URL)) {
+              active = homeTab.name;
+            }
           } catch (e) {
             tabs = [];
             active = "";
@@ -1628,6 +1637,43 @@
         }
         return "";
       },
+      injectDashboardMenu: function (menus) {
+        var homeConfig = this.resolveHomeConfig();
+        var dashboardUrl = this.resolveViewSrc(homeConfig.url);
+        var i;
+        var j;
+        var group;
+        var item;
+
+        for (i = 0; i < menus.length; i++) {
+          group = menus[i];
+          for (j = 0; j < group.subMenu.length; j++) {
+            item = group.subMenu[j];
+            if (item.url === dashboardUrl) {
+              item.name = homeConfig.title;
+              group.menu = homeConfig.group;
+              group.menuCode = group.menuCode || "index";
+              HOME_TAB_CONFIG.menuKey = item.tabKey || dashboardUrl;
+              return menus;
+            }
+          }
+        }
+
+        HOME_TAB_CONFIG.menuKey = dashboardUrl;
+        menus.unshift({
+          menuId: "dashboard-home",
+          menu: homeConfig.group,
+          menuCode: "index",
+          subMenu: [{
+            id: "dashboard-home-tab",
+            name: homeConfig.title,
+            code: "dashboard/dashboard.html",
+            url: dashboardUrl,
+            tabKey: dashboardUrl
+          }]
+        });
+        return menus;
+      },
       normalizeMenuData: function (data) {
         var result = [];
         var i;
@@ -1659,7 +1705,7 @@
           });
         }
 
-        return result;
+        return this.injectDashboardMenu(result);
       },
       buildMenuSrc: function (code, resourceId) {
         var normalized = code || "";
@@ -1720,11 +1766,9 @@
         var syncVersion;
         var applyMenuState;
 
-        if (!this.isHomeTabUrl(targetUrl)) {
-          activeMenuKey = this.findMenuKeyByUrl(targetUrl);
-          if (activeMenuKey) {
-            groupIndex = this.findMenuGroupIndexByUrl(targetUrl);
-          }
+        activeMenuKey = this.findMenuKeyByUrl(targetUrl);
+        if (activeMenuKey) {
+          groupIndex = this.findMenuGroupIndexByUrl(targetUrl);
         }
 
         this.activeMenuKey = activeMenuKey;

--
Gitblit v1.9.1