<!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;
|
}
|
|
.panel-actions {
|
display: flex;
|
align-items: center;
|
justify-content: flex-end;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.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);
|
}
|
|
.network-mini-ok {
|
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);
|
}
|
|
.network-mini-warning {
|
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);
|
}
|
|
.network-mini-offline {
|
background: linear-gradient(180deg, rgba(222, 92, 92, 0.10) 0%, rgba(222, 92, 92, 0.03) 100%);
|
border-color: rgba(222, 92, 92, 0.18);
|
}
|
|
.network-mini-latency {
|
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);
|
}
|
|
.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-subtitle {
|
margin-bottom: 10px;
|
font-size: 12px;
|
color: #7d90a4;
|
line-height: 1.6;
|
}
|
|
.chart-box {
|
width: 100%;
|
height: 280px;
|
}
|
|
.panel-device .mini-grid,
|
.panel-network .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,
|
.network-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;
|
}
|
|
.network-note {
|
margin-top: 6px;
|
font-size: 12px;
|
line-height: 1.5;
|
border-radius: 12px;
|
padding: 8px 10px;
|
}
|
|
.network-note-danger {
|
color: #c15b5b;
|
background: rgba(222, 92, 92, 0.08);
|
}
|
|
.network-note-warning {
|
color: #b67632;
|
background: rgba(245, 154, 74, 0.12);
|
}
|
|
.network-note-info {
|
color: #657d95;
|
background: rgba(125, 144, 164, 0.10);
|
}
|
|
.network-healthy-state {
|
margin-top: 14px;
|
padding: 14px 16px;
|
border-radius: 16px;
|
border: 1px solid rgba(87, 186, 128, 0.20);
|
background: linear-gradient(135deg, rgba(87, 186, 128, 0.10) 0%, rgba(87, 186, 128, 0.03) 100%);
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 14px;
|
}
|
|
.network-healthy-main {
|
min-width: 0;
|
flex: 1;
|
}
|
|
.network-healthy-title {
|
font-size: 14px;
|
font-weight: 700;
|
color: #2d7f56;
|
line-height: 1.5;
|
}
|
|
.network-healthy-desc {
|
margin-top: 4px;
|
font-size: 12px;
|
color: #698399;
|
line-height: 1.6;
|
}
|
|
.network-healthy-tags {
|
display: flex;
|
align-items: center;
|
justify-content: flex-end;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.network-healthy-tag {
|
padding: 6px 10px;
|
border-radius: 999px;
|
background: rgba(255, 255, 255, 0.72);
|
border: 1px solid rgba(87, 186, 128, 0.18);
|
font-size: 12px;
|
color: #557160;
|
white-space: nowrap;
|
}
|
|
.recent-panel {
|
min-height: 0;
|
}
|
|
.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-network .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;
|
}
|
|
.network-healthy-state {
|
flex-direction: column;
|
align-items: flex-start;
|
}
|
|
.network-healthy-tags {
|
justify-content: flex-start;
|
}
|
|
.panel-actions {
|
width: 100%;
|
justify-content: flex-start;
|
}
|
}
|
</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">{{ i18n('dashboard.title', '系统仪表盘') }}</h1>
|
</div>
|
<div class="hero-actions">
|
<el-button size="small" plain @click="openMonitor">{{ i18n('dashboard.openMonitor', '打开监控画面') }}</el-button>
|
<el-button
|
size="small"
|
:type="overview.systemRunning ? 'danger' : 'success'"
|
plain
|
:disabled="switchingSystem"
|
:loading="switchingSystem"
|
@click="toggleSystem">{{ overview.systemRunning ? i18n('dashboard.stopSystem', '停止系统') : i18n('dashboard.startSystem', '启动系统') }}</el-button>
|
<el-button size="small" type="primary" :loading="refreshing" @click="loadDashboard(true)">{{ i18n('dashboard.refreshNow', '立即刷新') }}</el-button>
|
</div>
|
</div>
|
|
<div class="hero-stat-grid">
|
<div class="hero-stat-row">
|
<div class="hero-row-head">
|
<div class="hero-row-kicker">{{ i18n('dashboard.overviewKicker', '状态概览') }}</div>
|
<div class="hero-row-note">{{ i18n('dashboard.overviewNote', '系统与刷新节奏') }}</div>
|
</div>
|
<div class="hero-status-grid">
|
<div class="hero-meta">
|
<div class="hero-meta-label">{{ i18n('dashboard.systemStatusLabel', '系统状态') }}</div>
|
<div class="hero-meta-value">{{ overview.systemRunning ? i18n('dashboard.systemRunning', '运行中') : i18n('dashboard.systemPaused', '已暂停') }}</div>
|
<div class="hero-meta-desc">{{ i18n('dashboard.systemStatusDesc', 'WCS 主服务当前状态') }}</div>
|
</div>
|
<div class="hero-meta">
|
<div class="hero-meta-label">{{ i18n('dashboard.lastRefreshLabel', '最近刷新') }}</div>
|
<div class="hero-meta-value">{{ displayText(overview.generatedAt, '-') }}</div>
|
<div class="hero-meta-desc">{{ i18n('dashboard.lastRefreshDesc', '最近一次聚合数据生成时间') }}</div>
|
</div>
|
<div class="hero-meta">
|
<div class="hero-meta-label">{{ i18n('dashboard.autoRefreshLabel', '自动刷新') }}</div>
|
<div class="hero-meta-value">{{ i18n('dashboard.autoRefreshValue', '{0}s 后刷新', [countdown]) }}</div>
|
<div class="hero-meta-desc">{{ i18n('dashboard.autoRefreshDesc', '页面自动更新倒计时') }}</div>
|
</div>
|
</div>
|
</div>
|
<div class="hero-stat-row">
|
<div class="hero-row-head">
|
<div class="hero-row-kicker">{{ i18n('dashboard.coreMetricsKicker', '核心指标') }}</div>
|
<div class="hero-row-note">{{ i18n('dashboard.coreMetricsNote', '任务、设备与 AI 总览') }}</div>
|
</div>
|
<div class="hero-metric-grid">
|
<div class="summary-card">
|
<div class="label">{{ i18n('dashboard.taskTotalLabel', '任务总数') }}</div>
|
<div class="value">{{ formatNumber(overview.taskTotal) }}</div>
|
<div class="desc">{{ i18n('dashboard.taskTotalDesc', '当前执行中 {0}', [formatNumber(overview.taskRunning)]) }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="label">{{ i18n('dashboard.deviceOnlineLabel', '在线设备') }}</div>
|
<div class="value">{{ formatNumber(overview.deviceOnline) }}</div>
|
<div class="desc">{{ i18n('dashboard.deviceOnlineDesc', '总设备 {0},告警 {1}', [formatNumber(overview.deviceTotal), formatNumber(overview.deviceAlarm)]) }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="label">{{ i18n('dashboard.aiTokenTotalLabel', 'AI 累计 Tokens') }}</div>
|
<div class="value">{{ formatNumber(overview.aiTokenTotal) }}</div>
|
<div class="desc">{{ i18n('dashboard.aiTokenTotalDesc', '按 AI 会话累计统计') }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="label">{{ i18n('dashboard.aiCallTotalLabel', 'LLM 调用次数') }}</div>
|
<div class="value">{{ formatNumber(overview.aiCallTotal) }}</div>
|
<div class="desc">{{ i18n('dashboard.aiCallTotalDesc', '最近一轮运行情况已纳入下方 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">{{ i18n('dashboard.taskPanelTitle', '任务态势') }}</h2>
|
<div class="panel-desc">{{ i18n('dashboard.taskPanelDesc', '从任务类型、执行阶段和最近流转记录快速判断当前作业压力。') }}</div>
|
</div>
|
</div>
|
|
<div class="mini-grid">
|
<div class="mini-card task-mini-running">
|
<div class="mini-label">{{ i18n('dashboard.taskRunningLabel', '执行中') }}</div>
|
<div class="mini-value">{{ formatNumber(tasks.overview.running) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.taskRunningHint', '当前正在流转的任务') }}</div>
|
</div>
|
<div class="mini-card task-mini-manual">
|
<div class="mini-label">{{ i18n('dashboard.taskManualLabel', '待人工') }}</div>
|
<div class="mini-value">{{ formatNumber(tasks.overview.manual) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.taskManualHint', '需人工关注或回滚') }}</div>
|
</div>
|
<div class="mini-card task-mini-completed">
|
<div class="mini-label">{{ i18n('dashboard.taskCompletedLabel', '已完成') }}</div>
|
<div class="mini-value">{{ formatNumber(tasks.overview.completed) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.taskCompletedHint', '已经完成或落账') }}</div>
|
</div>
|
<div class="mini-card task-mini-new">
|
<div class="mini-label">{{ i18n('dashboard.taskNewLabel', '新建') }}</div>
|
<div class="mini-value">{{ formatNumber(tasks.overview.newCreated) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.taskNewHint', '刚进入调度流程') }}</div>
|
</div>
|
</div>
|
|
<div class="chart-grid">
|
<div class="chart-card">
|
<div class="chart-title">{{ i18n('dashboard.taskDirectionChartTitle', '任务类型分布') }}</div>
|
<div ref="taskDirectionChart" class="chart-box"></div>
|
</div>
|
<div class="chart-card">
|
<div class="chart-title">{{ i18n('dashboard.taskStageChartTitle', '任务阶段概览') }}</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">{{ i18n('dashboard.recentPanelTitle', '最近任务') }}</h2>
|
<div class="panel-desc">{{ i18n('dashboard.recentPanelDesc', '帮助快速判断任务是否堆积、是否被设备接手,以及最近的任务目标位置。') }}</div>
|
</div>
|
</div>
|
|
<div class="recent-table">
|
<el-table
|
:data="tasks.recentTasks"
|
stripe
|
size="mini"
|
height="360"
|
:empty-text="i18n('dashboard.recentEmpty', '暂无任务记录')">
|
<el-table-column prop="wrkNo" :label="i18n('dashboard.column.workNo', '任务号')" min-width="100"></el-table-column>
|
<el-table-column prop="taskType" :label="i18n('dashboard.column.taskType', '任务类型')" min-width="110"></el-table-column>
|
<el-table-column prop="status" :label="i18n('dashboard.column.status', '状态')" min-width="160" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="source" :label="i18n('dashboard.column.source', '来源')" min-width="170" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="target" :label="i18n('dashboard.column.target', '目标')" min-width="170" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="device" :label="i18n('dashboard.column.device', '执行设备')" min-width="180" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="barcode" :label="i18n('dashboard.column.barcode', '条码')" min-width="150" show-overflow-tooltip></el-table-column>
|
<el-table-column prop="updateTime" :label="i18n('dashboard.column.updateTime', '最近更新时间')" min-width="170"></el-table-column>
|
</el-table>
|
</div>
|
</section>
|
|
<section class="panel panel-ai">
|
<div class="panel-header">
|
<div>
|
<div class="panel-kicker">AI</div>
|
<h2 class="panel-title">{{ i18n('dashboard.aiPanelTitle', 'AI 运行情况') }}</h2>
|
<div class="panel-desc">{{ i18n('dashboard.aiPanelDesc', '查看 AI 会话累计 Tokens、LLM 调用量,以及路由的可用与冷却状态。') }}</div>
|
</div>
|
<el-tag size="small" type="success">{{ i18n('dashboard.availableRoutesTag', '可用路由 {0}', [formatNumber(ai.overview.availableRouteCount)]) }}</el-tag>
|
</div>
|
|
<div class="mini-grid">
|
<div class="mini-card">
|
<div class="mini-label">{{ i18n('dashboard.aiTokenCardLabel', '累计 Tokens') }}</div>
|
<div class="mini-value">{{ formatNumber(ai.overview.tokenTotal) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.aiTokenCardHint', 'Prompt + Completion') }}</div>
|
</div>
|
<div class="mini-card">
|
<div class="mini-label">{{ i18n('dashboard.aiAskCountLabel', '提问轮次') }}</div>
|
<div class="mini-value">{{ formatNumber(ai.overview.askCount) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.aiAskCountHint', 'AI 对话累计轮次') }}</div>
|
</div>
|
<div class="mini-card">
|
<div class="mini-label">{{ i18n('dashboard.aiLlmCallLabel', 'LLM 调用') }}</div>
|
<div class="mini-value">{{ formatNumber(ai.overview.llmCallTotal) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.aiLlmCallHint', '成功 {0} / 失败 {1}', [formatNumber(ai.overview.successCallTotal), formatNumber(ai.overview.failCallTotal)]) }}</div>
|
</div>
|
<div class="mini-card">
|
<div class="mini-label">{{ i18n('dashboard.aiSessionCountLabel', '会话数') }}</div>
|
<div class="mini-value">{{ formatNumber(ai.overview.sessionCount) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.aiSessionCountHint', '最近调用 {0}', [displayText(ai.overview.lastCallTime, '-')]) }}</div>
|
</div>
|
</div>
|
|
<div class="chart-card">
|
<div class="chart-title">{{ i18n('dashboard.aiRouteChartTitle', '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">{{ i18n('dashboard.aiRouteDesc', '模型 {0},优先级 {1}', [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">{{ i18n('dashboard.aiRouteResult', '成功 {0} / 失败 {1}', [formatNumber(route.successCount), formatNumber(route.failCount)]) }}</div>
|
<div class="route-extra">{{ i18n('dashboard.aiRouteLastUsed', '最近使用 {0}', [displayText(route.lastUsedTime, '-')]) }}</div>
|
</div>
|
</div>
|
</div>
|
<el-empty v-else :description="i18n('dashboard.aiRouteEmpty', '暂无 AI 路由数据')"></el-empty>
|
</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">{{ i18n('dashboard.devicePanelTitle', '设备态势') }}</h2>
|
<div class="panel-desc">{{ i18n('dashboard.devicePanelDesc', '汇总输送站点、堆垛机、双工位堆垛机与 RGV 的在线、忙碌和告警情况。') }}</div>
|
</div>
|
<el-tag size="small" type="info">{{ i18n('dashboard.deviceOnlineRate', '在线率 {0}', [formatPercentValue(devices.overview.onlineRate || 0)]) }}</el-tag>
|
</div>
|
|
<div class="mini-grid">
|
<div class="mini-card">
|
<div class="mini-label">{{ i18n('dashboard.deviceTotalLabel', '设备总数') }}</div>
|
<div class="mini-value">{{ formatNumber(devices.overview.total) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.deviceTotalHint', '已启用配置设备') }}</div>
|
</div>
|
<div class="mini-card">
|
<div class="mini-label">{{ i18n('dashboard.deviceOnlineCardLabel', '在线设备') }}</div>
|
<div class="mini-value">{{ formatNumber(devices.overview.online) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.deviceOnlineCardHint', '实时连通设备数量') }}</div>
|
</div>
|
<div class="mini-card">
|
<div class="mini-label">{{ i18n('dashboard.deviceBusyLabel', '忙碌设备') }}</div>
|
<div class="mini-value">{{ formatNumber(devices.overview.busy) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.deviceBusyHint', '当前承载任务的设备') }}</div>
|
</div>
|
<div class="mini-card">
|
<div class="mini-label">{{ i18n('dashboard.deviceAlarmLabel', '告警设备') }}</div>
|
<div class="mini-value">{{ formatNumber(devices.overview.alarm) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.deviceAlarmHint', '含阻塞或报警状态') }}</div>
|
</div>
|
</div>
|
|
<div class="chart-card">
|
<div class="chart-title">{{ i18n('dashboard.deviceTypeChartTitle', '设备在线分布') }}</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">{{ i18n('dashboard.deviceTypeDesc', '在线 {0} / 总数 {1},离线 {2}', [formatNumber(item.online), formatNumber(item.total), formatNumber(item.offline)]) }}</div>
|
</div>
|
<div class="type-row-side">
|
<el-tag size="mini" type="success">{{ i18n('dashboard.deviceBusyTag', '忙碌 {0}', [formatNumber(item.busy)]) }}</el-tag>
|
<el-tag size="mini" :type="item.alarm > 0 ? 'danger' : 'info'">{{ i18n('dashboard.deviceAlarmTag', '告警 {0}', [formatNumber(item.alarm)]) }}</el-tag>
|
</div>
|
</div>
|
</div>
|
</section>
|
|
<section class="panel panel-network">
|
<div class="panel-header">
|
<div>
|
<div class="panel-kicker">Network</div>
|
<h2 class="panel-title">{{ i18n('devicePingLog.title', '设备网络分析') }}</h2>
|
<div class="panel-desc">{{ i18n('dashboard.networkPanelDesc', '汇总最新 Ping 样本的连通性、延迟与异常设备,帮助快速发现网络波动。') }}</div>
|
</div>
|
<div class="panel-actions">
|
<el-tag size="small" :type="network.overview.attentionDevices > 0 ? 'warning' : 'success'">
|
{{ i18n('dashboard.networkAttentionTag', '需关注 {0}', [formatNumber(network.overview.attentionDevices)]) }}
|
</el-tag>
|
<el-button size="mini" plain @click="openDevicePingAnalysis">{{ i18n('dashboard.networkViewDetail', '查看明细') }}</el-button>
|
</div>
|
</div>
|
|
<div class="mini-grid">
|
<div class="mini-card network-mini-ok">
|
<div class="mini-label">{{ i18n('dashboard.networkOkLabel', '正常') }}</div>
|
<div class="mini-value">{{ formatNumber(network.overview.okDevices) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.networkOkHint', '最新样本状态 OK') }}</div>
|
</div>
|
<div class="mini-card network-mini-warning">
|
<div class="mini-label">{{ i18n('dashboard.networkUnstableLabel', '波动') }}</div>
|
<div class="mini-value">{{ formatNumber(network.overview.unstableDevices) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.networkUnstableHint', '部分探测成功') }}</div>
|
</div>
|
<div class="mini-card network-mini-offline">
|
<div class="mini-label">{{ i18n('dashboard.networkOfflineLabel', '超时/异常') }}</div>
|
<div class="mini-value">{{ formatNumber(network.overview.offlineDevices) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.networkNoDataHint', '暂无数据 {0}', [formatNumber(network.overview.noDataDevices)]) }}</div>
|
</div>
|
<div class="mini-card network-mini-latency">
|
<div class="mini-label">{{ i18n('dashboard.networkAvgLatencyLabel', '平均延迟') }}</div>
|
<div class="mini-value">{{ formatLatency(network.overview.avgLatencyMs) }}</div>
|
<div class="mini-hint">{{ i18n('dashboard.networkPeakLatencyHint', '峰值 {0}', [formatLatency(network.overview.maxLatencyMs)]) }}</div>
|
</div>
|
</div>
|
|
<div class="chart-card">
|
<div class="chart-title">{{ i18n('dashboard.networkStatusChartTitle', '连通状态分布') }}</div>
|
<div class="chart-subtitle">{{ networkSamplingText() }}</div>
|
<div ref="networkStatusChart" class="network-chart-box"></div>
|
</div>
|
|
<div class="route-list" v-if="network.focusDevices.length">
|
<div v-for="item in network.focusDevices" :key="item.name + '-' + item.ip" class="route-row">
|
<div class="route-row-main">
|
<div class="route-row-name">{{ item.name }}</div>
|
<div class="route-row-desc">{{ displayText(item.ip, '-') }}</div>
|
<div v-if="item.message" :class="['network-note', 'network-note-' + (item.statusType || 'info')]">{{ item.message }}</div>
|
</div>
|
<div class="route-row-side">
|
<el-tag size="mini" :type="item.statusType">{{ item.statusText }}</el-tag>
|
<div class="route-extra">{{ i18n('dashboard.networkAvgLatencyTag', '平均 {0}', [formatLatency(item.avgLatencyMs)]) }}</div>
|
<div class="route-extra">{{ i18n('dashboard.networkLatestSampleTag', '最近样本 {0}', [displayText(item.latestTimeLabel, '-')]) }}</div>
|
</div>
|
</div>
|
</div>
|
<div v-else-if="network.overview.totalDevices > 0" class="network-healthy-state">
|
<div class="network-healthy-main">
|
<div class="network-healthy-title">{{ i18n('dashboard.networkHealthyTitle', '当前网络探测稳定') }}</div>
|
<div class="network-healthy-desc">{{ i18n('dashboard.networkHealthyDesc', '已纳入 {0} 台设备,最近一轮未发现超时或波动。', [formatNumber(network.overview.totalDevices)]) }}</div>
|
</div>
|
<div class="network-healthy-tags">
|
<div class="network-healthy-tag">{{ i18n('dashboard.networkHealthyOk', '正常 {0}', [formatNumber(network.overview.okDevices)]) }}</div>
|
<div class="network-healthy-tag">{{ i18n('dashboard.networkHealthyAvg', '平均 {0}', [formatLatency(network.overview.avgLatencyMs)]) }}</div>
|
<div class="network-healthy-tag">{{ i18n('dashboard.networkHealthyPeak', '峰值 {0}', [formatLatency(network.overview.maxLatencyMs)]) }}</div>
|
</div>
|
</div>
|
<el-empty v-else :description="i18n('dashboard.networkEmpty', '暂无设备网络样本')"></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">{{ i18n('dashboard.loadingTitle', '正在加载仪表盘') }}</div>
|
<div class="loading-desc">{{ i18n('dashboard.loadingDesc', '汇总任务、设备与 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?v=20260317-dashboard-stop-password-mask"></script>
|
</body>
|
</html>
|