|
<!doctype html>
|
|
<html lang="zh-Hant">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>CloudLink EMS|A17 營運儀表板 Mockup</title>
|
|
<style>
|
|
:root {
|
|
--ink: #10252b;
|
|
--muted: #6f8185;
|
|
--line: #dfe8e8;
|
|
--paper: #ffffff;
|
|
--canvas: #f3f7f6;
|
|
--nav: #0b2026;
|
|
--nav-soft: #17343b;
|
|
--teal: #11a891;
|
|
--teal-soft: #ddf6f0;
|
|
--cyan: #3f92a6;
|
|
--blue-soft: #e6f1f6;
|
|
--green: #2b956a;
|
|
--green-soft: #e4f5ec;
|
|
--amber: #d48a19;
|
|
--amber-soft: #fff2dc;
|
|
--red: #d14d4a;
|
|
--red-soft: #fdebea;
|
|
--shadow: 0 14px 34px rgba(17, 55, 61, 0.07);
|
|
--radius: 18px;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--canvas);
|
|
color: var(--ink);
|
|
font-family:
|
|
Inter, "Noto Sans TC", "PingFang TC", "Microsoft JhengHei", system-ui, -apple-system, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
button {
|
|
font: inherit;
|
|
}
|
|
|
|
.app-shell {
|
|
min-height: 100vh;
|
|
display: grid;
|
|
grid-template-columns: 238px minmax(0, 1fr);
|
|
}
|
|
|
|
.sidebar {
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
padding: 28px 18px 22px;
|
|
color: #d7e7e7;
|
|
background:
|
|
radial-gradient(circle at 12% 88%, rgba(17, 168, 145, 0.23), transparent 34%),
|
|
var(--nav);
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 10;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 11px;
|
|
padding: 0 10px 24px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.brand-mark {
|
|
position: relative;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 11px;
|
|
background: linear-gradient(145deg, #62e2ca, var(--teal));
|
|
box-shadow: 0 8px 24px rgba(17, 168, 145, 0.28);
|
|
}
|
|
|
|
.brand-mark::before,
|
|
.brand-mark::after {
|
|
content: "";
|
|
position: absolute;
|
|
background: var(--nav);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.brand-mark::before {
|
|
width: 7px;
|
|
height: 21px;
|
|
left: 15px;
|
|
top: 7px;
|
|
transform: skew(-17deg);
|
|
}
|
|
|
|
.brand-mark::after {
|
|
width: 17px;
|
|
height: 6px;
|
|
left: 9px;
|
|
top: 15px;
|
|
transform: skew(-17deg);
|
|
}
|
|
|
|
.brand strong {
|
|
display: block;
|
|
color: #fff;
|
|
font-size: 15px;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
.brand span {
|
|
display: block;
|
|
color: #84a2a7;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.site-switcher {
|
|
margin: 18px 7px 20px;
|
|
padding: 12px;
|
|
background: rgba(255, 255, 255, 0.06);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 13px;
|
|
}
|
|
|
|
.site-switcher small {
|
|
display: block;
|
|
color: #78979d;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.site-switcher strong {
|
|
color: #fff;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.nav-label {
|
|
padding: 0 12px;
|
|
margin: 5px 0 8px;
|
|
color: #5e7c82;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
letter-spacing: 0.13em;
|
|
}
|
|
|
|
.nav {
|
|
display: grid;
|
|
gap: 4px;
|
|
}
|
|
|
|
.nav a {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 11px;
|
|
min-height: 42px;
|
|
padding: 0 12px;
|
|
color: #a9c0c4;
|
|
text-decoration: none;
|
|
border-radius: 11px;
|
|
transition: 160ms ease;
|
|
}
|
|
|
|
.nav a:hover,
|
|
.nav a.active {
|
|
color: #fff;
|
|
background: var(--nav-soft);
|
|
}
|
|
|
|
.nav a.active {
|
|
box-shadow: inset 3px 0 0 #4de0c4;
|
|
}
|
|
|
|
.nav-icon {
|
|
width: 21px;
|
|
text-align: center;
|
|
color: #76bcb0;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.nav-count {
|
|
margin-left: auto;
|
|
min-width: 22px;
|
|
padding: 2px 6px;
|
|
color: #ffd7d5;
|
|
background: rgba(209, 77, 74, 0.28);
|
|
border-radius: 999px;
|
|
font-size: 10px;
|
|
text-align: center;
|
|
}
|
|
|
|
.sidebar-foot {
|
|
margin-top: auto;
|
|
padding: 14px 12px 0;
|
|
color: #69888e;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
font-size: 11px;
|
|
}
|
|
|
|
main {
|
|
min-width: 0;
|
|
padding: 28px 32px 44px;
|
|
}
|
|
|
|
.topbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 24px;
|
|
margin-bottom: 22px;
|
|
}
|
|
|
|
.title-block {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 13px;
|
|
}
|
|
|
|
.title-block h1 {
|
|
margin: 0 0 4px;
|
|
font-size: clamp(23px, 2vw, 30px);
|
|
line-height: 1.2;
|
|
letter-spacing: -0.03em;
|
|
}
|
|
|
|
.title-block p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.demo-badge {
|
|
flex: none;
|
|
margin-top: 2px;
|
|
padding: 5px 9px;
|
|
color: #8c5f12;
|
|
background: var(--amber-soft);
|
|
border: 1px solid #f4d69f;
|
|
border-radius: 999px;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.freshness {
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
text-align: right;
|
|
}
|
|
|
|
.freshness strong {
|
|
display: block;
|
|
color: var(--green);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.refresh-btn {
|
|
width: 38px;
|
|
height: 38px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 11px;
|
|
color: var(--ink);
|
|
background: var(--paper);
|
|
cursor: pointer;
|
|
box-shadow: 0 5px 15px rgba(17, 55, 61, 0.05);
|
|
}
|
|
|
|
.refresh-btn.spin {
|
|
animation: spin 480ms ease;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.attention {
|
|
display: grid;
|
|
grid-template-columns: 1.15fr repeat(3, minmax(150px, 0.72fr));
|
|
gap: 1px;
|
|
margin-bottom: 18px;
|
|
overflow: hidden;
|
|
border: 1px solid #f0d8d7;
|
|
border-radius: var(--radius);
|
|
background: #efd9d8;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.attention-intro,
|
|
.attention-item {
|
|
min-height: 92px;
|
|
padding: 18px 19px;
|
|
background: var(--paper);
|
|
}
|
|
|
|
.attention-intro {
|
|
background: linear-gradient(135deg, #fff3f2, #fffafa);
|
|
}
|
|
|
|
.eyebrow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--red);
|
|
font-size: 10px;
|
|
font-weight: 900;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.pulse-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: var(--red);
|
|
box-shadow: 0 0 0 5px rgba(209, 77, 74, 0.12);
|
|
}
|
|
|
|
.attention-intro h2 {
|
|
margin: 8px 0 0;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.attention-item {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
align-content: center;
|
|
gap: 3px 11px;
|
|
}
|
|
|
|
.attention-item .number {
|
|
grid-row: 1 / 3;
|
|
align-self: center;
|
|
font-size: 30px;
|
|
font-weight: 800;
|
|
line-height: 1;
|
|
}
|
|
|
|
.attention-item strong {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.attention-item span {
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, minmax(135px, 1fr));
|
|
gap: 12px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.stat-card,
|
|
.panel {
|
|
border: 1px solid var(--line);
|
|
border-radius: var(--radius);
|
|
background: var(--paper);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.stat-card {
|
|
position: relative;
|
|
min-height: 126px;
|
|
padding: 17px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-card::after {
|
|
content: "";
|
|
position: absolute;
|
|
width: 72px;
|
|
height: 72px;
|
|
right: -27px;
|
|
top: -25px;
|
|
border-radius: 50%;
|
|
background: var(--tone, var(--teal-soft));
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.stat-value {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 5px;
|
|
margin: 11px 0 6px;
|
|
font-size: 27px;
|
|
font-weight: 820;
|
|
letter-spacing: -0.03em;
|
|
}
|
|
|
|
.stat-value small {
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
font-weight: 650;
|
|
}
|
|
|
|
.stat-meta {
|
|
color: var(--muted);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.stat-meta.good {
|
|
color: var(--green);
|
|
}
|
|
|
|
.stat-meta.warn {
|
|
color: var(--red);
|
|
}
|
|
|
|
.dashboard-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.55fr) minmax(330px, 0.85fr);
|
|
gap: 18px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.panel {
|
|
min-width: 0;
|
|
padding: 20px;
|
|
}
|
|
|
|
.panel-head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 17px;
|
|
}
|
|
|
|
.panel-head h2 {
|
|
margin: 0 0 3px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.panel-head p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.segmented {
|
|
display: inline-flex;
|
|
padding: 3px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
background: #f7f9f9;
|
|
}
|
|
|
|
.segmented button {
|
|
min-width: 42px;
|
|
padding: 5px 9px;
|
|
border: 0;
|
|
border-radius: 7px;
|
|
color: var(--muted);
|
|
background: transparent;
|
|
font-size: 10px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.segmented button.active {
|
|
color: #fff;
|
|
background: var(--nav-soft);
|
|
box-shadow: 0 4px 11px rgba(17, 55, 61, 0.12);
|
|
}
|
|
|
|
.load-summary {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
align-items: end;
|
|
gap: 18px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.load-number {
|
|
font-size: 34px;
|
|
font-weight: 820;
|
|
letter-spacing: -0.04em;
|
|
}
|
|
|
|
.load-number small {
|
|
margin-left: 5px;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.load-context {
|
|
padding-bottom: 5px;
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.load-context strong {
|
|
color: var(--teal);
|
|
}
|
|
|
|
.chart-wrap {
|
|
height: 180px;
|
|
border-radius: 13px;
|
|
background:
|
|
linear-gradient(#edf3f3 1px, transparent 1px) 0 0 / 100% 45px,
|
|
linear-gradient(90deg, #f1f5f5 1px, transparent 1px) 0 0 / 12.5% 100%;
|
|
}
|
|
|
|
canvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.groups {
|
|
display: grid;
|
|
gap: 15px;
|
|
}
|
|
|
|
.group-row {
|
|
display: grid;
|
|
grid-template-columns: 42px minmax(0, 1fr) auto;
|
|
gap: 11px;
|
|
align-items: center;
|
|
}
|
|
|
|
.group-code {
|
|
width: 40px;
|
|
height: 40px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 12px;
|
|
color: var(--nav);
|
|
background: var(--teal-soft);
|
|
font-size: 12px;
|
|
font-weight: 850;
|
|
}
|
|
|
|
.group-info strong,
|
|
.group-metric strong {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.group-info span,
|
|
.group-metric span {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.meter {
|
|
height: 7px;
|
|
margin-top: 7px;
|
|
border-radius: 99px;
|
|
background: #edf2f2;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.meter > span {
|
|
display: block;
|
|
height: 100%;
|
|
border-radius: inherit;
|
|
background: linear-gradient(90deg, var(--teal), #55d7be);
|
|
}
|
|
|
|
.group-metric {
|
|
text-align: right;
|
|
}
|
|
|
|
.slots-summary {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 8px;
|
|
margin-top: 20px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid var(--line);
|
|
}
|
|
|
|
.slots-summary div {
|
|
padding: 10px;
|
|
border-radius: 11px;
|
|
background: #f7f9f9;
|
|
text-align: center;
|
|
}
|
|
|
|
.slots-summary strong {
|
|
display: block;
|
|
font-size: 17px;
|
|
}
|
|
|
|
.slots-summary span {
|
|
color: var(--muted);
|
|
font-size: 9px;
|
|
}
|
|
|
|
.connector-panel {
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
color: var(--muted);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.legend span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.legend i,
|
|
.status-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.connector-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, minmax(125px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.connector {
|
|
position: relative;
|
|
min-height: 112px;
|
|
padding: 14px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 14px;
|
|
background: #fbfcfc;
|
|
cursor: pointer;
|
|
transition: 160ms ease;
|
|
}
|
|
|
|
.connector:hover,
|
|
.connector:focus-visible {
|
|
outline: none;
|
|
border-color: #8fcfc4;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 20px rgba(17, 55, 61, 0.08);
|
|
}
|
|
|
|
.connector.problem {
|
|
border-color: #efc4c2;
|
|
background: #fffafa;
|
|
}
|
|
|
|
.connector-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.connector-id {
|
|
font-size: 11px;
|
|
font-weight: 820;
|
|
}
|
|
|
|
.connector-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 9px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.connector-power {
|
|
margin: 15px 0 9px;
|
|
font-size: 20px;
|
|
font-weight: 820;
|
|
}
|
|
|
|
.connector-power small {
|
|
color: var(--muted);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.connector-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
color: var(--muted);
|
|
font-size: 9px;
|
|
}
|
|
|
|
.queue-pill {
|
|
padding: 2px 6px;
|
|
border-radius: 999px;
|
|
color: #7f5a17;
|
|
background: var(--amber-soft);
|
|
}
|
|
|
|
.detail-drawer {
|
|
display: none;
|
|
margin-top: 12px;
|
|
padding: 14px 16px;
|
|
border-radius: 13px;
|
|
color: #d4e5e5;
|
|
background: var(--nav-soft);
|
|
}
|
|
|
|
.detail-drawer.open {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 20px;
|
|
}
|
|
|
|
.detail-drawer strong {
|
|
color: #fff;
|
|
}
|
|
|
|
.detail-drawer span {
|
|
color: #9db6ba;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.detail-drawer button {
|
|
padding: 7px 12px;
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
border-radius: 9px;
|
|
color: #fff;
|
|
background: rgba(255, 255, 255, 0.08);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.bottom-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.3fr) minmax(320px, 0.7fr);
|
|
gap: 18px;
|
|
}
|
|
|
|
.usage-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 165px;
|
|
gap: 18px;
|
|
}
|
|
|
|
.usage-chart {
|
|
height: 170px;
|
|
}
|
|
|
|
.usage-facts {
|
|
display: grid;
|
|
align-content: center;
|
|
gap: 11px;
|
|
}
|
|
|
|
.fact {
|
|
padding: 11px 12px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
background: #fafcfc;
|
|
}
|
|
|
|
.fact span {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 9px;
|
|
}
|
|
|
|
.fact strong {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.pipeline {
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.pipeline-row {
|
|
display: grid;
|
|
grid-template-columns: 34px 1fr auto;
|
|
gap: 11px;
|
|
align-items: center;
|
|
padding: 9px 0;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
|
|
.pipeline-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.pipeline-index {
|
|
width: 30px;
|
|
height: 30px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
color: var(--green);
|
|
background: var(--green-soft);
|
|
font-size: 10px;
|
|
font-weight: 850;
|
|
}
|
|
|
|
.pipeline-index.pending {
|
|
color: var(--amber);
|
|
background: var(--amber-soft);
|
|
}
|
|
|
|
.pipeline-info strong {
|
|
display: block;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.pipeline-info span {
|
|
color: var(--muted);
|
|
font-size: 9px;
|
|
}
|
|
|
|
.pipeline-value {
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.footnote {
|
|
margin: 17px 2px 0;
|
|
color: #819094;
|
|
font-size: 10px;
|
|
text-align: right;
|
|
}
|
|
|
|
@media (max-width: 1240px) {
|
|
.stats-grid {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
|
|
.connector-grid {
|
|
grid-template-columns: repeat(4, 1fr);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 940px) {
|
|
.app-shell {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.sidebar {
|
|
position: static;
|
|
height: auto;
|
|
padding: 16px;
|
|
}
|
|
|
|
.brand {
|
|
padding-bottom: 14px;
|
|
}
|
|
|
|
.site-switcher,
|
|
.nav-label,
|
|
.sidebar-foot {
|
|
display: none;
|
|
}
|
|
|
|
.nav {
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.nav a {
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.nav a:nth-child(n + 5) {
|
|
display: none;
|
|
}
|
|
|
|
main {
|
|
padding: 22px 18px 36px;
|
|
}
|
|
|
|
.attention {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.dashboard-grid,
|
|
.bottom-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.topbar,
|
|
.panel-head {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.toolbar {
|
|
align-self: stretch;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.attention,
|
|
.stats-grid {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.attention-intro {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.connector-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.usage-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.usage-facts {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-shell">
|
|
<aside class="sidebar" aria-label="主要導覽">
|
|
<div class="brand">
|
|
<div class="brand-mark" aria-hidden="true"></div>
|
|
<div>
|
|
<strong>CloudLink EMS</strong>
|
|
<span>Energy operations</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="site-switcher">
|
|
<small>目前案場</small>
|
|
<strong>寶台 A17・地下停車場</strong>
|
|
</div>
|
|
|
|
<div class="nav-label">OPERATIONS</div>
|
|
<nav class="nav">
|
|
<a class="active" href="#overview"><span class="nav-icon">▦</span>營運總覽</a>
|
|
<a href="#connectors"><span class="nav-icon">ϟ</span>充電設備<span class="nav-count">1</span></a>
|
|
<a href="#groups"><span class="nav-icon">◫</span>負載與輪充<span class="nav-count">2</span></a>
|
|
<a href="#usage"><span class="nav-icon">⌁</span>用量分析</a>
|
|
<a href="#billing"><span class="nav-icon">$</span>結算與帳務<span class="nav-count">3</span></a>
|
|
<a href="#connectors"><span class="nav-icon">◎</span>住戶與綁定</a>
|
|
<a href="#overview"><span class="nav-icon">⌘</span>工程師工具</a>
|
|
<a href="#overview"><span class="nav-icon">⚙</span>系統設定</a>
|
|
</nav>
|
|
|
|
<div class="sidebar-foot">
|
|
CloudLink EMS 2.0<br />
|
|
Demo environment
|
|
</div>
|
|
</aside>
|
|
|
|
<main id="overview">
|
|
<header class="topbar">
|
|
<div class="title-block">
|
|
<div>
|
|
<h1>案場營運總覽</h1>
|
|
<p>即時掌握設備、供電、充電體驗與帳務狀態</p>
|
|
</div>
|
|
<span class="demo-badge">展示假資料</span>
|
|
</div>
|
|
|
|
<div class="toolbar">
|
|
<div class="freshness">
|
|
<strong>● 資料更新正常</strong>
|
|
<span id="updatedAt">剛剛更新・00:48:34</span>
|
|
</div>
|
|
<button class="refresh-btn" id="refreshBtn" aria-label="重新整理資料" title="重新整理">↻</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="attention" aria-labelledby="attention-title">
|
|
<div class="attention-intro">
|
|
<div class="eyebrow"><span class="pulse-dot"></span>NEEDS ATTENTION</div>
|
|
<h2 id="attention-title">目前有 5 個項目需要處理</h2>
|
|
</div>
|
|
<div class="attention-item">
|
|
<div class="number" style="color: var(--red)">1</div>
|
|
<strong>設備故障</strong>
|
|
<span>CP-04 / B2-04</span>
|
|
</div>
|
|
<div class="attention-item">
|
|
<div class="number" style="color: var(--amber)">2</div>
|
|
<strong>Queue 狀態不同步</strong>
|
|
<span>標示充電,設備已待機</span>
|
|
</div>
|
|
<div class="attention-item">
|
|
<div class="number" style="color: var(--amber)">2</div>
|
|
<strong>結算待確認</strong>
|
|
<span>6 月帳務流程未完成</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="stats-grid" aria-label="即時關鍵指標">
|
|
<article class="stat-card" style="--tone: var(--green-soft)">
|
|
<div class="stat-label">Charge Point 在線</div>
|
|
<div class="stat-value">4 <small>/ 4 台</small></div>
|
|
<div class="stat-meta good">全部 Heartbeat 正常</div>
|
|
</article>
|
|
<article class="stat-card" style="--tone: var(--blue-soft)">
|
|
<div class="stat-label">Connector 可服務</div>
|
|
<div class="stat-value">11 <small>/ 12 個</small></div>
|
|
<div class="stat-meta warn">1 個 Faulted</div>
|
|
</article>
|
|
<article class="stat-card" style="--tone: var(--teal-soft)">
|
|
<div class="stat-label">正在充電</div>
|
|
<div class="stat-value">3 <small>sessions</small></div>
|
|
<div class="stat-meta good">即時功率 21.84 kW</div>
|
|
</article>
|
|
<article class="stat-card" style="--tone: var(--amber-soft)">
|
|
<div class="stat-label">等待供電</div>
|
|
<div class="stat-value">3 <small>connectors</small></div>
|
|
<div class="stat-meta">2 eligible・1 未插槍</div>
|
|
</article>
|
|
<article class="stat-card" style="--tone: #eeeafb">
|
|
<div class="stat-label">今日累積電量</div>
|
|
<div class="stat-value">84.6 <small>kWh</small></div>
|
|
<div class="stat-meta good">較昨日同時段 +8.2%</div>
|
|
</article>
|
|
<article class="stat-card" style="--tone: #eef1f2">
|
|
<div class="stat-label">本月預估電費</div>
|
|
<div class="stat-value">8,920 <small>元</small></div>
|
|
<div class="stat-meta">結算前預估值</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="dashboard-grid">
|
|
<article class="panel" aria-labelledby="load-title">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 id="load-title">案場即時負載</h2>
|
|
<p>三個充電群組的合計功率・契約容量 297 kW</p>
|
|
</div>
|
|
<div class="segmented" aria-label="負載圖表時間範圍">
|
|
<button class="active" data-load-range="live">即時</button>
|
|
<button data-load-range="today">今日</button>
|
|
<button data-load-range="week">7 日</button>
|
|
</div>
|
|
</div>
|
|
<div class="load-summary">
|
|
<div class="load-number"><span id="loadValue">21.84</span><small>kW</small></div>
|
|
<div class="load-context">
|
|
目前使用契約容量 <strong id="loadPercent">7.4%</strong>・尚有 275.16 kW 餘裕
|
|
</div>
|
|
</div>
|
|
<div class="chart-wrap"><canvas id="loadChart" aria-label="即時負載折線圖"></canvas></div>
|
|
</article>
|
|
|
|
<article class="panel" id="groups" aria-labelledby="group-title">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 id="group-title">群組容量與 Slots</h2>
|
|
<p>即時功率、容量餘裕與輪充占用</p>
|
|
</div>
|
|
<span style="color: var(--green); font-size: 10px; font-weight: 800">離峰時段</span>
|
|
</div>
|
|
<div class="groups">
|
|
<div class="group-row">
|
|
<div class="group-code">B1</div>
|
|
<div class="group-info">
|
|
<strong>7.33 / 99 kW</strong>
|
|
<span>1 charging・1 eligible</span>
|
|
<div class="meter"><span style="width: 7.4%"></span></div>
|
|
</div>
|
|
<div class="group-metric"><strong>1 / 14</strong><span>slots</span></div>
|
|
</div>
|
|
<div class="group-row">
|
|
<div class="group-code">B2</div>
|
|
<div class="group-info">
|
|
<strong>7.24 / 99 kW</strong>
|
|
<span>1 charging・1 mismatch</span>
|
|
<div class="meter"><span style="width: 7.3%"></span></div>
|
|
</div>
|
|
<div class="group-metric"><strong>1 / 14</strong><span>slots</span></div>
|
|
</div>
|
|
<div class="group-row">
|
|
<div class="group-code">B3</div>
|
|
<div class="group-info">
|
|
<strong>7.28 / 99 kW</strong>
|
|
<span>1 charging・1 eligible</span>
|
|
<div class="meter"><span style="width: 7.4%"></span></div>
|
|
</div>
|
|
<div class="group-metric"><strong>1 / 14</strong><span>slots</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="slots-summary">
|
|
<div><strong>3</strong><span>OCCUPIED</span></div>
|
|
<div><strong>3</strong><span>WAITING</span></div>
|
|
<div><strong>0</strong><span>RESERVED</span></div>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="panel connector-panel" id="connectors" aria-labelledby="connector-title">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 id="connector-title">Connector 即時狀態</h2>
|
|
<p>同時呈現設備 Runtime、OCPP 連線與 Rotation Queue 狀態</p>
|
|
</div>
|
|
<div class="legend" aria-label="狀態圖例">
|
|
<span><i style="background: var(--green)"></i>Available</span>
|
|
<span><i style="background: var(--teal)"></i>Charging</span>
|
|
<span><i style="background: var(--amber)"></i>Waiting</span>
|
|
<span><i style="background: var(--red)"></i>Faulted</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="connector-grid">
|
|
<div class="connector" tabindex="0" data-id="B1-01" data-detail="私人・CloudLink・Queue: charging・最後更新 18 秒前">
|
|
<div class="connector-top"><span class="connector-id">B1-01</span><span class="connector-status"><i class="status-dot" style="background: var(--teal)"></i>CHARGING</span></div>
|
|
<div class="connector-power">7.33 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span>01:26</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B1-02" data-detail="私人・CloudLink・Queue: eligible・順位 1・最後更新 1 分前">
|
|
<div class="connector-top"><span class="connector-id">B1-02</span><span class="connector-status"><i class="status-dot" style="background: var(--green)"></i>AVAILABLE</span></div>
|
|
<div class="connector-power">0.00 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span class="queue-pill">QUEUE #1</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B1-03" data-detail="私人・CloudLink・未進入 Queue・最後更新 2 分前">
|
|
<div class="connector-top"><span class="connector-id">B1-03</span><span class="connector-status"><i class="status-dot" style="background: var(--green)"></i>AVAILABLE</span></div>
|
|
<div class="connector-power">0.00 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span>ONLINE</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B1-04" data-detail="公用・CloudLink・未進入 Queue・最後更新 36 秒前">
|
|
<div class="connector-top"><span class="connector-id">B1-04</span><span class="connector-status"><i class="status-dot" style="background: var(--green)"></i>AVAILABLE</span></div>
|
|
<div class="connector-power">0.00 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PUBLIC・CLOUDLINK</span><span>ONLINE</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B1-05" data-detail="私人・Fortune・未進入 Queue・最後更新 4 分前">
|
|
<div class="connector-top"><span class="connector-id">B1-05</span><span class="connector-status"><i class="status-dot" style="background: var(--green)"></i>AVAILABLE</span></div>
|
|
<div class="connector-power">0.00 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・FORTUNE</span><span>ONLINE</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B2-01" data-detail="私人・CloudLink・Queue: charging・最後更新 20 秒前">
|
|
<div class="connector-top"><span class="connector-id">B2-01</span><span class="connector-status"><i class="status-dot" style="background: var(--teal)"></i>CHARGING</span></div>
|
|
<div class="connector-power">7.24 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span>00:58</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B2-02" data-detail="私人・CloudLink・Queue 顯示 charging,但設備為 Available;建議檢查狀態同步">
|
|
<div class="connector-top"><span class="connector-id">B2-02</span><span class="connector-status"><i class="status-dot" style="background: var(--amber)"></i>MISMATCH</span></div>
|
|
<div class="connector-power">0.00 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span class="queue-pill">QUEUE !</span></div>
|
|
</div>
|
|
<div class="connector problem" tabindex="0" data-id="B2-04" data-detail="區域未設定・CloudLink・Faulted / GroundFailure・最後更新 9 分前">
|
|
<div class="connector-top"><span class="connector-id">B2-04</span><span class="connector-status" style="color: var(--red)"><i class="status-dot" style="background: var(--red)"></i>FAULTED</span></div>
|
|
<div class="connector-power">— <small>kW</small></div>
|
|
<div class="connector-meta"><span>AREA NOT SET</span><span style="color: var(--red)">ERROR</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B3-01" data-detail="私人・CloudLink・Queue: charging・最後更新 14 秒前">
|
|
<div class="connector-top"><span class="connector-id">B3-01</span><span class="connector-status"><i class="status-dot" style="background: var(--teal)"></i>CHARGING</span></div>
|
|
<div class="connector-power">7.28 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span>01:12</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B3-02" data-detail="私人・CloudLink・Queue: eligible・順位 2・最後更新 1 分前">
|
|
<div class="connector-top"><span class="connector-id">B3-02</span><span class="connector-status"><i class="status-dot" style="background: var(--green)"></i>AVAILABLE</span></div>
|
|
<div class="connector-power">0.00 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span class="queue-pill">QUEUE #2</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B3-03" data-detail="私人・CloudLink・未進入 Queue・最後更新 3 分前">
|
|
<div class="connector-top"><span class="connector-id">B3-03</span><span class="connector-status"><i class="status-dot" style="background: var(--green)"></i>AVAILABLE</span></div>
|
|
<div class="connector-power">0.00 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span>ONLINE</span></div>
|
|
</div>
|
|
<div class="connector" tabindex="0" data-id="B3-04" data-detail="私人・CloudLink・Queue: not_plugged / NO_PLUG・最後更新 2 分前">
|
|
<div class="connector-top"><span class="connector-id">B3-04</span><span class="connector-status"><i class="status-dot" style="background: var(--green)"></i>AVAILABLE</span></div>
|
|
<div class="connector-power">0.00 <small>kW</small></div>
|
|
<div class="connector-meta"><span>PRIVATE・CLOUDLINK</span><span class="queue-pill">NO PLUG</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="detail-drawer" id="connectorDetail" aria-live="polite">
|
|
<div><strong id="detailTitle">B1-01</strong><br /><span id="detailText"></span></div>
|
|
<button type="button">查看設備詳情 →</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="bottom-grid">
|
|
<article class="panel" id="usage" aria-labelledby="usage-title">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 id="usage-title">30 日充電用量</h2>
|
|
<p>Sessions、有效電量與 Connector 使用率</p>
|
|
</div>
|
|
<div class="segmented" aria-label="用量圖表指標">
|
|
<button class="active" data-usage-mode="energy">kWh</button>
|
|
<button data-usage-mode="sessions">Sessions</button>
|
|
</div>
|
|
</div>
|
|
<div class="usage-layout">
|
|
<div class="usage-chart"><canvas id="usageChart" aria-label="30 日充電用量長條圖"></canvas></div>
|
|
<div class="usage-facts">
|
|
<div class="fact"><span>有效充電量</span><strong>2,213.6 kWh</strong></div>
|
|
<div class="fact"><span>充電 Sessions</span><strong>136</strong></div>
|
|
<div class="fact"><span>時間使用率</span><strong>4.08%</strong></div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="panel" id="billing" aria-labelledby="billing-title">
|
|
<div class="panel-head">
|
|
<div>
|
|
<h2 id="billing-title">6 月結算流程</h2>
|
|
<p>從交易資料到住戶帳務的完成狀態</p>
|
|
</div>
|
|
<span style="color: var(--amber); font-size: 10px; font-weight: 800">待確認</span>
|
|
</div>
|
|
<div class="pipeline">
|
|
<div class="pipeline-row">
|
|
<div class="pipeline-index">✓</div>
|
|
<div class="pipeline-info"><strong>Connector 結算</strong><span>設備電量與費率計算</span></div>
|
|
<div class="pipeline-value">11 / 11</div>
|
|
</div>
|
|
<div class="pipeline-row">
|
|
<div class="pipeline-index">✓</div>
|
|
<div class="pipeline-info"><strong>門牌結算</strong><span>私人與公用費用歸戶</span></div>
|
|
<div class="pipeline-value">11 筆</div>
|
|
</div>
|
|
<div class="pipeline-row">
|
|
<div class="pipeline-index pending">3</div>
|
|
<div class="pipeline-info"><strong>帳務覆核</strong><span>7 paid・4 pending</span></div>
|
|
<div class="pipeline-value" style="color: var(--amber)">進行中</div>
|
|
</div>
|
|
<div class="pipeline-row">
|
|
<div class="pipeline-index pending">4</div>
|
|
<div class="pipeline-info"><strong>發票結算</strong><span>目前狀態 draft</span></div>
|
|
<div class="pipeline-value" style="color: var(--red)">未完成</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<div class="footnote">此頁所有數字皆為 UI 展示假資料,不代表 A17 即時營運結果。</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
const colors = {
|
|
teal: "#11a891",
|
|
tealFill: "rgba(17, 168, 145, 0.12)",
|
|
ink: "#10252b",
|
|
muted: "#718387",
|
|
line: "#dfe8e8",
|
|
amber: "#d48a19",
|
|
}
|
|
|
|
const loadSeries = {
|
|
live: [12.1, 12.4, 11.9, 14.2, 13.8, 14.6, 14.5, 18.3, 21.1, 21.5, 21.3, 21.84],
|
|
today: [6.2, 4.1, 3.8, 2.1, 1.6, 3.3, 7.4, 10.8, 13.6, 18.2, 21.0, 21.84],
|
|
week: [13.2, 19.8, 21.4, 17.1, 24.6, 20.2, 21.84],
|
|
}
|
|
|
|
const usageSeries = {
|
|
energy: [132, 212, 106, 38, 86, 127, 111, 101, 109, 226, 122, 75, 147, 11, 84],
|
|
sessions: [10, 10, 4, 4, 4, 5, 3, 4, 4, 14, 10, 4, 7, 4, 6],
|
|
}
|
|
|
|
function setupCanvas(canvas) {
|
|
const rect = canvas.getBoundingClientRect()
|
|
const ratio = window.devicePixelRatio || 1
|
|
canvas.width = Math.max(1, Math.floor(rect.width * ratio))
|
|
canvas.height = Math.max(1, Math.floor(rect.height * ratio))
|
|
const ctx = canvas.getContext("2d")
|
|
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
return { ctx, width: rect.width, height: rect.height }
|
|
}
|
|
|
|
function drawLineChart(canvas, values) {
|
|
const { ctx, width, height } = setupCanvas(canvas)
|
|
const pad = { top: 17, right: 12, bottom: 22, left: 8 }
|
|
const chartW = width - pad.left - pad.right
|
|
const chartH = height - pad.top - pad.bottom
|
|
const max = Math.max(...values) * 1.2
|
|
const points = values.map((value, i) => ({
|
|
x: pad.left + (i / (values.length - 1)) * chartW,
|
|
y: pad.top + chartH - (value / max) * chartH,
|
|
}))
|
|
|
|
const gradient = ctx.createLinearGradient(0, pad.top, 0, height)
|
|
gradient.addColorStop(0, "rgba(17,168,145,.24)")
|
|
gradient.addColorStop(1, "rgba(17,168,145,0)")
|
|
|
|
ctx.beginPath()
|
|
ctx.moveTo(points[0].x, pad.top + chartH)
|
|
points.forEach((point) => ctx.lineTo(point.x, point.y))
|
|
ctx.lineTo(points[points.length - 1].x, pad.top + chartH)
|
|
ctx.closePath()
|
|
ctx.fillStyle = gradient
|
|
ctx.fill()
|
|
|
|
ctx.beginPath()
|
|
points.forEach((point, i) => (i === 0 ? ctx.moveTo(point.x, point.y) : ctx.lineTo(point.x, point.y)))
|
|
ctx.strokeStyle = colors.teal
|
|
ctx.lineWidth = 2.5
|
|
ctx.lineJoin = "round"
|
|
ctx.lineCap = "round"
|
|
ctx.stroke()
|
|
|
|
const last = points[points.length - 1]
|
|
ctx.beginPath()
|
|
ctx.arc(last.x, last.y, 4.5, 0, Math.PI * 2)
|
|
ctx.fillStyle = "#fff"
|
|
ctx.fill()
|
|
ctx.lineWidth = 3
|
|
ctx.strokeStyle = colors.teal
|
|
ctx.stroke()
|
|
|
|
ctx.fillStyle = colors.muted
|
|
ctx.font = "10px system-ui"
|
|
ctx.textAlign = "left"
|
|
ctx.fillText("00:00", pad.left, height - 4)
|
|
ctx.textAlign = "right"
|
|
ctx.fillText("現在", width - pad.right, height - 4)
|
|
}
|
|
|
|
function drawBarChart(canvas, values, mode) {
|
|
const { ctx, width, height } = setupCanvas(canvas)
|
|
const pad = { top: 15, right: 8, bottom: 24, left: 8 }
|
|
const chartW = width - pad.left - pad.right
|
|
const chartH = height - pad.top - pad.bottom
|
|
const max = Math.max(...values) * 1.15
|
|
const gap = 6
|
|
const barW = Math.max(4, (chartW - gap * (values.length - 1)) / values.length)
|
|
|
|
values.forEach((value, index) => {
|
|
const h = (value / max) * chartH
|
|
const x = pad.left + index * (barW + gap)
|
|
const y = pad.top + chartH - h
|
|
const gradient = ctx.createLinearGradient(0, y, 0, pad.top + chartH)
|
|
gradient.addColorStop(0, mode === "energy" ? colors.teal : colors.amber)
|
|
gradient.addColorStop(1, mode === "energy" ? "#7ed9c7" : "#f0c374")
|
|
ctx.fillStyle = gradient
|
|
ctx.beginPath()
|
|
ctx.roundRect(x, y, barW, h, Math.min(4, barW / 2))
|
|
ctx.fill()
|
|
})
|
|
|
|
ctx.fillStyle = colors.muted
|
|
ctx.font = "10px system-ui"
|
|
ctx.textAlign = "left"
|
|
ctx.fillText("30 日前", pad.left, height - 4)
|
|
ctx.textAlign = "right"
|
|
ctx.fillText("今天", width - pad.right, height - 4)
|
|
}
|
|
|
|
const loadCanvas = document.getElementById("loadChart")
|
|
const usageCanvas = document.getElementById("usageChart")
|
|
let currentLoadRange = "live"
|
|
let currentUsageMode = "energy"
|
|
|
|
function renderCharts() {
|
|
drawLineChart(loadCanvas, loadSeries[currentLoadRange])
|
|
drawBarChart(usageCanvas, usageSeries[currentUsageMode], currentUsageMode)
|
|
}
|
|
|
|
document.querySelectorAll("[data-load-range]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
document.querySelectorAll("[data-load-range]").forEach((item) => item.classList.remove("active"))
|
|
button.classList.add("active")
|
|
currentLoadRange = button.dataset.loadRange
|
|
drawLineChart(loadCanvas, loadSeries[currentLoadRange])
|
|
})
|
|
})
|
|
|
|
document.querySelectorAll("[data-usage-mode]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
document.querySelectorAll("[data-usage-mode]").forEach((item) => item.classList.remove("active"))
|
|
button.classList.add("active")
|
|
currentUsageMode = button.dataset.usageMode
|
|
drawBarChart(usageCanvas, usageSeries[currentUsageMode], currentUsageMode)
|
|
})
|
|
})
|
|
|
|
document.querySelectorAll(".connector").forEach((item) => {
|
|
const openDetail = () => {
|
|
document.getElementById("detailTitle").textContent = item.dataset.id
|
|
document.getElementById("detailText").textContent = item.dataset.detail
|
|
document.getElementById("connectorDetail").classList.add("open")
|
|
}
|
|
item.addEventListener("click", openDetail)
|
|
item.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault()
|
|
openDetail()
|
|
}
|
|
})
|
|
})
|
|
|
|
document.getElementById("refreshBtn").addEventListener("click", (event) => {
|
|
const button = event.currentTarget
|
|
button.classList.remove("spin")
|
|
void button.offsetWidth
|
|
button.classList.add("spin")
|
|
const now = new Date()
|
|
document.getElementById("updatedAt").textContent =
|
|
`剛剛更新・${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`
|
|
})
|
|
|
|
let resizeTimer
|
|
window.addEventListener("resize", () => {
|
|
clearTimeout(resizeTimer)
|
|
resizeTimer = setTimeout(renderCharts, 120)
|
|
})
|
|
|
|
requestAnimationFrame(() => requestAnimationFrame(renderCharts))
|
|
</script>
|
|
</body>
|
|
</html>
|