| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | 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). |
| | |
| | | common.profile=基本资料 |
| | | common.logout=退出登录 |
| | | common.closeOtherTabs=关闭其他页签 |
| | | common.backHome=返回控制中心 |
| | | common.backHome=返回仪表盘 |
| | | common.aiAssistant=AI助手 |
| | | common.workPage=工作页面 |
| | | common.businessPage=业务页面 |
| | |
| | | index.fakeRunning=仿真运行中 |
| | | index.fakeStopped=仿真未运行 |
| | | index.licenseExpiring=许可证即将过期 |
| | | index.homeTab=控制中心 |
| | | index.homeGroup=实时监控 |
| | | index.homeTab=系统仪表盘 |
| | | index.homeGroup=系统概览 |
| | | index.profileGroup=账户中心 |
| | | index.versionLoading=版本加载中... |
| | | index.licenseExpireAt=许可证将于 {0} 过期,剩余有效期:{1} 天。 |
| New file |
| | |
| | | (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("仪表盘数据加载失败,请检查接口状态"); |
| | | } |
| | | }, |
| | | 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"); |
| | | } |
| | | } |
| | | }); |
| | | }()); |
| New file |
| | |
| | | <!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">当前正在流转的任务</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、LLM 调用量,以及路由的可用与冷却状态。</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">正在加载仪表盘</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> |
| | |
| | | <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", |
| | |
| | | "common.profile": "基本资料", |
| | | "common.logout": "退出登录", |
| | | "common.closeOtherTabs": "关闭其他页签", |
| | | "common.backHome": "返回控制中心", |
| | | "common.backHome": "返回仪表盘", |
| | | "common.aiAssistant": "AI助手", |
| | | "common.workPage": "工作页面", |
| | | "common.businessPage": "业务页面", |
| | |
| | | "index.fakeRunning": "仿真运行中", |
| | | "index.fakeStopped": "仿真未运行", |
| | | "index.licenseExpiring": "许可证即将过期", |
| | | "index.homeTab": "控制中心", |
| | | "index.homeGroup": "实时监控", |
| | | "index.homeTab": "系统仪表盘", |
| | | "index.homeGroup": "系统概览", |
| | | "index.profileGroup": "账户中心", |
| | | "index.versionLoading": "Version loading...", |
| | | "index.licenseExpireAt": "许可证将于 {0} 过期,剩余有效期:{1} 天。", |
| | |
| | | }; |
| | | }, |
| | | 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; |
| | |
| | | } |
| | | } |
| | | active = parsed ? parsed.activeTab : ""; |
| | | if (this.resolveViewSrc(active) === this.resolveViewSrc(LEGACY_HOME_TAB_URL)) { |
| | | active = homeTab.name; |
| | | } |
| | | } catch (e) { |
| | | tabs = []; |
| | | active = ""; |
| | |
| | | } |
| | | 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; |
| | |
| | | }); |
| | | } |
| | | |
| | | return result; |
| | | return this.injectDashboardMenu(result); |
| | | }, |
| | | buildMenuSrc: function (code, resourceId) { |
| | | var normalized = code || ""; |
| | |
| | | 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; |