feat(portal): refactor navigation from drawer to sidebar layout

Replace collapsible <details> drawers with a persistent left sidebar for
報表類, 查詢類, and 開發工具 categories. Unify dev tools handling via
data-tool-src attribute instead of onclick openTool(). Also release
tmtt-defect page status from dev to released.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-09 10:16:14 +08:00
parent e88427f4b4
commit 706c8ba52c
4 changed files with 210 additions and 221 deletions

View File

@@ -53,7 +53,7 @@
{ {
"route": "/tmtt-defect", "route": "/tmtt-defect",
"name": "TMTT印字腳型不良分析", "name": "TMTT印字腳型不良分析",
"status": "dev" "status": "released"
} }
], ],
"api_public": true, "api_public": true,
@@ -63,4 +63,4 @@
"object_count": 19, "object_count": 19,
"source": "tools/query_table_schema.py" "source": "tools/query_table_schema.py"
} }
} }

View File

@@ -1,13 +1,11 @@
import './portal.css'; import './portal.css';
(function initPortal() { (function initPortal() {
const tabs = document.querySelectorAll('.tab'); const sidebarItems = document.querySelectorAll('.sidebar-item[data-target]');
const frames = document.querySelectorAll('iframe'); const frames = document.querySelectorAll('iframe');
const toolFrame = document.getElementById('toolFrame');
const healthDot = document.getElementById('healthDot'); const healthDot = document.getElementById('healthDot');
const healthLabel = document.getElementById('healthLabel'); const healthLabel = document.getElementById('healthLabel');
const healthPopup = document.getElementById('healthPopup'); const healthPopup = document.getElementById('healthPopup');
const healthStatus = document.getElementById('healthStatus');
const dbStatus = document.getElementById('dbStatus'); const dbStatus = document.getElementById('dbStatus');
const redisStatus = document.getElementById('redisStatus'); const redisStatus = document.getElementById('redisStatus');
const cacheEnabled = document.getElementById('cacheEnabled'); const cacheEnabled = document.getElementById('cacheEnabled');
@@ -22,41 +20,37 @@ import './portal.css';
function setFrameHeight() { function setFrameHeight() {
const header = document.querySelector('.header'); const header = document.querySelector('.header');
const tabArea = document.querySelector('.tabs'); if (!header) return;
if (!header || !tabArea) return; const height = Math.max(600, window.innerHeight - header.offsetHeight - 52);
const height = Math.max(600, window.innerHeight - header.offsetHeight - tabArea.offsetHeight - 60);
frames.forEach((frame) => { frames.forEach((frame) => {
frame.style.height = `${height}px`; frame.style.height = `${height}px`;
}); });
} }
function activateTab(targetId) { function activateTab(targetId, toolSrc) {
tabs.forEach((tab) => tab.classList.remove('active')); sidebarItems.forEach((item) => item.classList.remove('active'));
frames.forEach((frame) => frame.classList.remove('active')); frames.forEach((frame) => frame.classList.remove('active'));
const tabBtn = document.querySelector(`[data-target="${targetId}"]`); const activeItems = document.querySelectorAll(`.sidebar-item[data-target="${targetId}"]`);
const targetFrame = document.getElementById(targetId); activeItems.forEach((item) => {
if (!toolSrc || item.dataset.toolSrc === toolSrc) {
item.classList.add('active');
}
});
if (tabBtn) tabBtn.classList.add('active'); const targetFrame = document.getElementById(targetId);
if (targetFrame) { if (targetFrame) {
targetFrame.classList.add('active'); targetFrame.classList.add('active');
if (targetFrame.dataset.src && !targetFrame.src) { if (toolSrc) {
if (targetFrame.src !== toolSrc && !targetFrame.src.endsWith(toolSrc)) {
targetFrame.src = toolSrc;
}
} else if (targetFrame.dataset.src && !targetFrame.src) {
targetFrame.src = targetFrame.dataset.src; targetFrame.src = targetFrame.dataset.src;
} }
} }
} }
function openTool(path) {
if (!toolFrame) return false;
tabs.forEach((tab) => tab.classList.remove('active'));
frames.forEach((frame) => frame.classList.remove('active'));
toolFrame.classList.add('active');
if (toolFrame.src !== path) {
toolFrame.src = path;
}
return false;
}
function toggleHealthPopup() { function toggleHealthPopup() {
if (!healthPopup) return; if (!healthPopup) return;
healthPopup.classList.toggle('show'); healthPopup.classList.toggle('show');
@@ -167,18 +161,17 @@ import './portal.css';
} }
} }
tabs.forEach((tab) => { sidebarItems.forEach((item) => {
tab.addEventListener('click', () => activateTab(tab.dataset.target)); item.addEventListener('click', () => {
activateTab(item.dataset.target, item.dataset.toolSrc || null);
});
}); });
if (tabs.length > 0) { if (sidebarItems.length > 0) {
activateTab(tabs[0].dataset.target); activateTab(sidebarItems[0].dataset.target);
} }
window.openTool = openTool;
window.toggleHealthPopup = toggleHealthPopup; window.toggleHealthPopup = toggleHealthPopup;
// Click handler is wired via inline onclick in template for fallback compatibility.
// Avoid duplicate binding here, otherwise a single click toggles twice.
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (!e.target.closest('#healthStatus') && !e.target.closest('#healthPopup') && healthPopup) { if (!e.target.closest('#healthStatus') && !e.target.closest('#healthPopup') && healthPopup) {
healthPopup.classList.remove('show'); healthPopup.classList.remove('show');

View File

@@ -1,29 +1,55 @@
.drawer { /* Sidebar Navigation */
.sidebar {
width: 200px;
min-width: 200px;
background: #fff; background: #fff;
border-radius: 10px; border-radius: 10px;
border: 1px solid #e3e8f2; border: 1px solid #e3e8f2;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
overflow: hidden; overflow-y: auto;
align-self: flex-start;
position: sticky;
top: 20px;
} }
.drawer > summary { .sidebar-group-title {
list-style: none; padding: 10px 14px 6px;
font-size: 11px;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sidebar-group-title:not(:first-child) {
border-top: 1px solid #e2e8f0;
margin-top: 4px;
padding-top: 12px;
}
.sidebar-item {
display: block;
width: 100%;
border: none;
background: none;
text-align: left;
padding: 8px 14px;
font-size: 13px;
color: #475569;
cursor: pointer; cursor: pointer;
padding: 10px 14px; transition: all 0.15s ease;
font-size: 14px; text-decoration: none;
font-family: inherit;
}
.sidebar-item:hover {
background: #f1f5f9;
color: #667eea;
}
.sidebar-item.active {
background: #eef2ff;
color: #667eea;
font-weight: 600; font-weight: 600;
color: #334155; border-right: 3px solid #667eea;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.drawer > summary::-webkit-details-marker {
display: none;
}
.drawer-content {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 12px;
} }

View File

@@ -214,86 +214,75 @@
position: relative; position: relative;
} }
.tabs { /* Sidebar + Panel Layout */
margin-bottom: 12px; .main-layout {
display: grid; display: flex;
gap: 10px; gap: 12px;
min-height: calc(100vh - 140px);
} }
.drawer { .sidebar {
width: 200px;
min-width: 200px;
background: white; background: white;
border-radius: 10px; border-radius: 10px;
border: 1px solid #e3e8f2; border: 1px solid #e3e8f2;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
overflow: hidden; overflow-y: auto;
align-self: flex-start;
position: sticky;
top: 20px;
} }
.drawer > summary { .sidebar-group-title {
list-style: none; padding: 10px 14px 6px;
font-size: 11px;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sidebar-group-title:not(:first-child) {
border-top: 1px solid #e2e8f0;
margin-top: 4px;
padding-top: 12px;
}
.sidebar-item {
display: block;
width: 100%;
border: none;
background: none;
text-align: left;
padding: 8px 14px;
font-size: 13px;
color: #475569;
cursor: pointer; cursor: pointer;
padding: 10px 14px; transition: all 0.15s ease;
font-size: 14px;
font-weight: 600;
color: #334155;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.drawer > summary::-webkit-details-marker {
display: none;
}
.drawer-content {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 12px;
}
.tab {
border: 2px solid #d9d9d9;
background: white;
color: #333;
padding: 10px 18px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.tab:hover {
border-color: #667eea;
color: #667eea;
}
.tab.active {
background: #667eea;
border-color: #667eea;
color: white;
}
.drawer-link {
border: 1px solid #d9d9d9;
background: white;
color: #333;
padding: 10px 18px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
text-decoration: none; text-decoration: none;
transition: all 0.2s ease; font-family: inherit;
} }
.drawer-link:hover { .sidebar-item:hover {
border-color: #667eea; background: #f1f5f9;
color: #667eea; color: #667eea;
} }
.sidebar-item.active {
background: #eef2ff;
color: #667eea;
font-weight: 600;
border-right: 3px solid #667eea;
}
.panel { .panel {
flex: 1;
background: white; background: white;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.08); box-shadow: 0 2px 10px rgba(0,0,0,0.08);
overflow: hidden; overflow: hidden;
min-width: 0;
} }
.panel iframe { .panel iframe {
@@ -363,85 +352,74 @@
</div> </div>
</div> </div>
<div class="tabs"> <div class="main-layout">
<details class="drawer" open> <nav class="sidebar">
<summary>報表類</summary> <div class="sidebar-group-title">報表類</div>
<div class="drawer-content"> {% if can_view_page('/wip-overview') %}
{% if can_view_page('/wip-overview') %} <button class="sidebar-item" data-target="wipOverviewFrame">WIP 即時概況</button>
<button class="tab" data-target="wipOverviewFrame">WIP 即時概況</button> {% endif %}
{% endif %} {% if can_view_page('/resource') %}
{% if can_view_page('/resource') %} <button class="sidebar-item" data-target="resourceFrame">設備即時概況</button>
<button class="tab" data-target="resourceFrame">設備即時概況</button> {% endif %}
{% endif %} {% if can_view_page('/resource-history') %}
{% if can_view_page('/resource-history') %} <button class="sidebar-item" data-target="resourceHistoryFrame">設備歷史績效</button>
<button class="tab" data-target="resourceHistoryFrame">設備歷史績效</button> {% endif %}
{% endif %}
</div>
</details>
<details class="drawer" open> <div class="sidebar-group-title">查詢類</div>
<summary>查詢類</summary> {% if can_view_page('/tables') %}
<div class="drawer-content"> <button class="sidebar-item" data-target="tableFrame">數據表查詢工具</button>
{% if can_view_page('/tables') %} {% endif %}
<button class="tab" data-target="tableFrame">數據表查詢工具</button> {% if can_view_page('/excel-query') %}
{% endif %} <button class="sidebar-item" data-target="excelQueryFrame">Excel 批次查詢</button>
{% if can_view_page('/excel-query') %} {% endif %}
<button class="tab" data-target="excelQueryFrame">Excel 批次查詢</button> {% if can_view_page('/job-query') %}
{% endif %} <button class="sidebar-item" data-target="jobQueryFrame">設備維修查詢</button>
{% if can_view_page('/job-query') %} {% endif %}
<button class="tab" data-target="jobQueryFrame">設備維修查詢</button> {% if can_view_page('/query-tool') %}
{% endif %} <button class="sidebar-item" data-target="queryToolFrame">批次追蹤工具</button>
{% if can_view_page('/query-tool') %} {% endif %}
<button class="tab" data-target="queryToolFrame">批次追蹤工具</button> {% if can_view_page('/tmtt-defect') %}
{% endif %} <button class="sidebar-item" data-target="tmttDefectFrame">TMTT不良分析</button>
{% if can_view_page('/tmtt-defect') %} {% endif %}
<button class="tab" data-target="tmttDefectFrame">TMTT不良分析</button>
{% endif %}
</div>
</details>
<details class="drawer"> <div class="sidebar-group-title">開發工具</div>
<summary>開發工具</summary> {% if is_admin %}
<div class="drawer-content"> <button class="sidebar-item" data-target="toolFrame" data-tool-src="/admin/pages">頁面管理</button>
{% if is_admin %} <button class="sidebar-item" data-target="toolFrame" data-tool-src="/admin/performance">效能監控</button>
<a class="drawer-link" href="#" onclick="return openTool('/admin/pages')">頁面管理</a> {% else %}
<a class="drawer-link" href="#" onclick="return openTool('/admin/performance')">效能監控</a> <a class="sidebar-item" href="{{ url_for('auth.login') }}">管理員登入</a>
{% else %} {% endif %}
<a class="drawer-link" href="{{ url_for('auth.login') }}">管理員登入</a> </nav>
{% endif %}
</div>
</details>
</div>
<div class="panel"> <div class="panel">
<!-- Lazy load: iframes load on tab activation --> {% if can_view_page('/wip-overview') %}
{% if can_view_page('/wip-overview') %} <iframe id="wipOverviewFrame" data-src="/wip-overview" title="WIP 即時概況"></iframe>
<iframe id="wipOverviewFrame" data-src="/wip-overview" title="WIP 即時概況"></iframe> {% endif %}
{% endif %} {% if can_view_page('/resource') %}
{% if can_view_page('/resource') %} <iframe id="resourceFrame" data-src="/resource" title="設備即時概況"></iframe>
<iframe id="resourceFrame" data-src="/resource" title="設備即時概況"></iframe> {% endif %}
{% endif %} {% if can_view_page('/tables') %}
{% if can_view_page('/tables') %} <iframe id="tableFrame" data-src="/tables" title="數據表查詢工具"></iframe>
<iframe id="tableFrame" data-src="/tables" title="數據表查詢工具"></iframe> {% endif %}
{% endif %} {% if can_view_page('/excel-query') %}
{% if can_view_page('/excel-query') %} <iframe id="excelQueryFrame" data-src="/excel-query" title="Excel 批次查詢"></iframe>
<iframe id="excelQueryFrame" data-src="/excel-query" title="Excel 批次查詢"></iframe> {% endif %}
{% endif %} {% if can_view_page('/resource-history') %}
{% if can_view_page('/resource-history') %} <iframe id="resourceHistoryFrame" data-src="/resource-history" title="設備歷史績效"></iframe>
<iframe id="resourceHistoryFrame" data-src="/resource-history" title="設備歷史績效"></iframe> {% endif %}
{% endif %} {% if can_view_page('/job-query') %}
{% if can_view_page('/job-query') %} <iframe id="jobQueryFrame" data-src="/job-query" title="設備維修查詢"></iframe>
<iframe id="jobQueryFrame" data-src="/job-query" title="設備維修查詢"></iframe> {% endif %}
{% endif %} {% if can_view_page('/query-tool') %}
{% if can_view_page('/query-tool') %} <iframe id="queryToolFrame" data-src="/query-tool" title="批次追蹤工具"></iframe>
<iframe id="queryToolFrame" data-src="/query-tool" title="批次追蹤工具"></iframe> {% endif %}
{% endif %} {% if can_view_page('/tmtt-defect') %}
{% if can_view_page('/tmtt-defect') %} <iframe id="tmttDefectFrame" data-src="/tmtt-defect" title="TMTT不良分析"></iframe>
<iframe id="tmttDefectFrame" data-src="/tmtt-defect" title="TMTT不良分析"></iframe> {% endif %}
{% endif %} {% if is_admin %}
{% if is_admin %} <iframe id="toolFrame" title="開發工具"></iframe>
<iframe id="toolFrame" title="開發工具"></iframe> {% endif %}
{% endif %} </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
@@ -452,61 +430,53 @@
<script type="module" src="{{ portal_js }}"></script> <script type="module" src="{{ portal_js }}"></script>
{% else %} {% else %}
<script> <script>
const tabs = document.querySelectorAll('.tab'); const sidebarItems = document.querySelectorAll('.sidebar-item[data-target]');
const frames = document.querySelectorAll('iframe'); const frames = document.querySelectorAll('iframe');
const toolFrame = document.getElementById('toolFrame'); const toolFrame = document.getElementById('toolFrame');
function setFrameHeight() { function setFrameHeight() {
const headerHeight = document.querySelector('.header').offsetHeight; const headerHeight = document.querySelector('.header').offsetHeight;
const tabsHeight = document.querySelector('.tabs').offsetHeight; const padding = 52;
const padding = 60; const height = Math.max(600, window.innerHeight - headerHeight - padding);
const height = Math.max(600, window.innerHeight - headerHeight - tabsHeight - padding);
frames.forEach(frame => { frames.forEach(frame => {
frame.style.height = `${height}px`; frame.style.height = `${height}px`;
}); });
} }
function activateTab(targetId) { function activateTab(targetId, toolSrc) {
tabs.forEach(tab => tab.classList.remove('active')); sidebarItems.forEach(item => item.classList.remove('active'));
frames.forEach(frame => frame.classList.remove('active')); frames.forEach(frame => frame.classList.remove('active'));
const tabBtn = document.querySelector(`[data-target="${targetId}"]`); const activeItems = document.querySelectorAll(`.sidebar-item[data-target="${targetId}"]`);
const targetFrame = document.getElementById(targetId); activeItems.forEach(item => {
if (!toolSrc || item.dataset.toolSrc === toolSrc) {
item.classList.add('active');
}
});
if (tabBtn) tabBtn.classList.add('active'); const targetFrame = document.getElementById(targetId);
if (targetFrame) { if (targetFrame) {
targetFrame.classList.add('active'); targetFrame.classList.add('active');
// Lazy load: load iframe src on first activation if (toolSrc) {
if (targetFrame.dataset.src && !targetFrame.src) { // Dev tools: load the specific tool URL
if (targetFrame.src !== toolSrc && !targetFrame.src.endsWith(toolSrc)) {
targetFrame.src = toolSrc;
}
} else if (targetFrame.dataset.src && !targetFrame.src) {
targetFrame.src = targetFrame.dataset.src; targetFrame.src = targetFrame.dataset.src;
} }
} }
} }
function openTool(path) { sidebarItems.forEach(item => {
if (!toolFrame) return false; item.addEventListener('click', () => {
tabs.forEach(tab => tab.classList.remove('active')); activateTab(item.dataset.target, item.dataset.toolSrc || null);
frames.forEach(frame => frame.classList.remove('active')); });
toolFrame.classList.add('active');
if (toolFrame.src !== path) {
toolFrame.src = path;
}
return false;
}
window.openTool = openTool;
tabs.forEach(tab => {
tab.addEventListener('click', () => activateTab(tab.dataset.target));
}); });
// Auto-activate first available tab // Auto-activate first available item
if (tabs.length > 0) { if (sidebarItems.length > 0) {
const firstTab = tabs[0]; activateTab(sidebarItems[0].dataset.target);
// Clear any pre-set active states
tabs.forEach(tab => tab.classList.remove('active'));
frames.forEach(frame => frame.classList.remove('active'));
// Activate first tab
activateTab(firstTab.dataset.target);
} }
window.addEventListener('resize', setFrameHeight); window.addEventListener('resize', setFrameHeight);