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