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",
"name": "TMTT印字腳型不良分析",
"status": "dev"
"status": "released"
}
],
"api_public": true,

View File

@@ -1,13 +1,11 @@
import './portal.css';
(function initPortal() {
const tabs = document.querySelectorAll('.tab');
const sidebarItems = document.querySelectorAll('.sidebar-item[data-target]');
const frames = document.querySelectorAll('iframe');
const toolFrame = document.getElementById('toolFrame');
const healthDot = document.getElementById('healthDot');
const healthLabel = document.getElementById('healthLabel');
const healthPopup = document.getElementById('healthPopup');
const healthStatus = document.getElementById('healthStatus');
const dbStatus = document.getElementById('dbStatus');
const redisStatus = document.getElementById('redisStatus');
const cacheEnabled = document.getElementById('cacheEnabled');
@@ -22,41 +20,37 @@ import './portal.css';
function setFrameHeight() {
const header = document.querySelector('.header');
const tabArea = document.querySelector('.tabs');
if (!header || !tabArea) return;
const height = Math.max(600, window.innerHeight - header.offsetHeight - tabArea.offsetHeight - 60);
if (!header) return;
const height = Math.max(600, window.innerHeight - header.offsetHeight - 52);
frames.forEach((frame) => {
frame.style.height = `${height}px`;
});
}
function activateTab(targetId) {
tabs.forEach((tab) => tab.classList.remove('active'));
function activateTab(targetId, toolSrc) {
sidebarItems.forEach((item) => item.classList.remove('active'));
frames.forEach((frame) => frame.classList.remove('active'));
const tabBtn = document.querySelector(`[data-target="${targetId}"]`);
const targetFrame = document.getElementById(targetId);
const activeItems = document.querySelectorAll(`.sidebar-item[data-target="${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) {
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;
}
}
}
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() {
if (!healthPopup) return;
healthPopup.classList.toggle('show');
@@ -167,18 +161,17 @@ import './portal.css';
}
}
tabs.forEach((tab) => {
tab.addEventListener('click', () => activateTab(tab.dataset.target));
sidebarItems.forEach((item) => {
item.addEventListener('click', () => {
activateTab(item.dataset.target, item.dataset.toolSrc || null);
});
});
if (tabs.length > 0) {
activateTab(tabs[0].dataset.target);
if (sidebarItems.length > 0) {
activateTab(sidebarItems[0].dataset.target);
}
window.openTool = openTool;
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) => {
if (!e.target.closest('#healthStatus') && !e.target.closest('#healthPopup') && healthPopup) {
healthPopup.classList.remove('show');

View File

@@ -1,29 +1,55 @@
.drawer {
/* Sidebar Navigation */
.sidebar {
width: 200px;
min-width: 200px;
background: #fff;
border-radius: 10px;
border: 1px solid #e3e8f2;
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 {
list-style: none;
.sidebar-group-title {
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;
padding: 10px 14px;
font-size: 14px;
transition: all 0.15s ease;
text-decoration: none;
font-family: inherit;
}
.sidebar-item:hover {
background: #f1f5f9;
color: #667eea;
}
.sidebar-item.active {
background: #eef2ff;
color: #667eea;
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;
border-right: 3px solid #667eea;
}

View File

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