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:
@@ -53,7 +53,7 @@
|
||||
{
|
||||
"route": "/tmtt-defect",
|
||||
"name": "TMTT印字腳型不良分析",
|
||||
"status": "dev"
|
||||
"status": "released"
|
||||
}
|
||||
],
|
||||
"api_public": true,
|
||||
@@ -63,4 +63,4 @@
|
||||
"object_count": 19,
|
||||
"source": "tools/query_table_schema.py"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,85 +352,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<details class="drawer" open>
|
||||
<summary>報表類</summary>
|
||||
<div class="drawer-content">
|
||||
{% if can_view_page('/wip-overview') %}
|
||||
<button class="tab" data-target="wipOverviewFrame">WIP 即時概況</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/resource') %}
|
||||
<button class="tab" data-target="resourceFrame">設備即時概況</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/resource-history') %}
|
||||
<button class="tab" data-target="resourceHistoryFrame">設備歷史績效</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
<div class="main-layout">
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-group-title">報表類</div>
|
||||
{% if can_view_page('/wip-overview') %}
|
||||
<button class="sidebar-item" data-target="wipOverviewFrame">WIP 即時概況</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/resource') %}
|
||||
<button class="sidebar-item" data-target="resourceFrame">設備即時概況</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/resource-history') %}
|
||||
<button class="sidebar-item" data-target="resourceHistoryFrame">設備歷史績效</button>
|
||||
{% endif %}
|
||||
|
||||
<details class="drawer" open>
|
||||
<summary>查詢類</summary>
|
||||
<div class="drawer-content">
|
||||
{% if can_view_page('/tables') %}
|
||||
<button class="tab" data-target="tableFrame">數據表查詢工具</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/excel-query') %}
|
||||
<button class="tab" data-target="excelQueryFrame">Excel 批次查詢</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/job-query') %}
|
||||
<button class="tab" data-target="jobQueryFrame">設備維修查詢</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/query-tool') %}
|
||||
<button class="tab" data-target="queryToolFrame">批次追蹤工具</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/tmtt-defect') %}
|
||||
<button class="tab" data-target="tmttDefectFrame">TMTT不良分析</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
<div class="sidebar-group-title">查詢類</div>
|
||||
{% if can_view_page('/tables') %}
|
||||
<button class="sidebar-item" data-target="tableFrame">數據表查詢工具</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/excel-query') %}
|
||||
<button class="sidebar-item" data-target="excelQueryFrame">Excel 批次查詢</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/job-query') %}
|
||||
<button class="sidebar-item" data-target="jobQueryFrame">設備維修查詢</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/query-tool') %}
|
||||
<button class="sidebar-item" data-target="queryToolFrame">批次追蹤工具</button>
|
||||
{% endif %}
|
||||
{% if can_view_page('/tmtt-defect') %}
|
||||
<button class="sidebar-item" data-target="tmttDefectFrame">TMTT不良分析</button>
|
||||
{% endif %}
|
||||
|
||||
<details class="drawer">
|
||||
<summary>開發工具</summary>
|
||||
<div class="drawer-content">
|
||||
{% 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>
|
||||
{% else %}
|
||||
<a class="drawer-link" href="{{ url_for('auth.login') }}">管理員登入</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="sidebar-group-title">開發工具</div>
|
||||
{% if is_admin %}
|
||||
<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="sidebar-item" href="{{ url_for('auth.login') }}">管理員登入</a>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
{% if can_view_page('/resource') %}
|
||||
<iframe id="resourceFrame" data-src="/resource" title="設備即時概況"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/tables') %}
|
||||
<iframe id="tableFrame" data-src="/tables" title="數據表查詢工具"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/excel-query') %}
|
||||
<iframe id="excelQueryFrame" data-src="/excel-query" title="Excel 批次查詢"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/resource-history') %}
|
||||
<iframe id="resourceHistoryFrame" data-src="/resource-history" title="設備歷史績效"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/job-query') %}
|
||||
<iframe id="jobQueryFrame" data-src="/job-query" title="設備維修查詢"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/query-tool') %}
|
||||
<iframe id="queryToolFrame" data-src="/query-tool" title="批次追蹤工具"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/tmtt-defect') %}
|
||||
<iframe id="tmttDefectFrame" data-src="/tmtt-defect" title="TMTT不良分析"></iframe>
|
||||
{% endif %}
|
||||
{% if is_admin %}
|
||||
<iframe id="toolFrame" title="開發工具"></iframe>
|
||||
{% endif %}
|
||||
<div class="panel">
|
||||
{% if can_view_page('/wip-overview') %}
|
||||
<iframe id="wipOverviewFrame" data-src="/wip-overview" title="WIP 即時概況"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/resource') %}
|
||||
<iframe id="resourceFrame" data-src="/resource" title="設備即時概況"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/tables') %}
|
||||
<iframe id="tableFrame" data-src="/tables" title="數據表查詢工具"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/excel-query') %}
|
||||
<iframe id="excelQueryFrame" data-src="/excel-query" title="Excel 批次查詢"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/resource-history') %}
|
||||
<iframe id="resourceHistoryFrame" data-src="/resource-history" title="設備歷史績效"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/job-query') %}
|
||||
<iframe id="jobQueryFrame" data-src="/job-query" title="設備維修查詢"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/query-tool') %}
|
||||
<iframe id="queryToolFrame" data-src="/query-tool" title="批次追蹤工具"></iframe>
|
||||
{% endif %}
|
||||
{% if can_view_page('/tmtt-defect') %}
|
||||
<iframe id="tmttDefectFrame" data-src="/tmtt-defect" title="TMTT不良分析"></iframe>
|
||||
{% endif %}
|
||||
{% if is_admin %}
|
||||
<iframe id="toolFrame" title="開發工具"></iframe>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user