專案

一般

配置概況

Feature #1422 » a17-operations-dashboard.html

Dashboard 假資料 HTML mockup - 陳國瑋, 2026-08-10 14:41

 
<!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>
(3-3/9)