早期 Web 門戶(如 Google iGoogle、Netvibes)普及了「可拖拽、可折疊、個性化佈局」的儀表盤模式。本文分析一份基於 HTML Table + JavaScript 實現的雙欄拖拽門戶頁面結構,探討如何保存用戶最後的佈局狀態。
提供的代碼為靜態 HTML 片段,核心特徵:
#parentTable 兩列 td[width="48%"],各自包含多個 class="dragTable" 表格作為可拖拽模塊容器。dragTable 首行 class="dragTR" 包含摺疊、添加、更多操作圖標,作為拖拽手柄。id="kpis", id="4graph", id="favorite", id="1graph" 等唯一標識,用於標識模塊類型與順序。bgcolor="#FFFFFF" 顯示具體內容或「無任何記錄」佔位。dragTR 上綁定 mousedown,document 上綁定 mousemove/mouseup。insertBefore)。td,移動對應的 dragTable 節點。{
"left": [{"id":"kpis", "collapsed":false}, {"id":"4graph", "collapsed":true}],
"right": [{"id":"favorite", "collapsed":false}, {"id":"1graph", "collapsed":false}]
}/api/portal/layout,綁定用戶 ID 存入 MySQL/Redis。localStorage.setItem('portal_layout', json),無需登錄即可記憶(但換瀏覽器失效)。appendChild 重建 DOM 結構,恢復摺疊類名。| 維度 | 原代碼 (2000s 風格) | 現代最佳實踐 |
|---|---|---|
| 佈局 | Table + 固定寬度 | CSS Grid / Flexbox + 響應式斷點 |
| 拖拽 | 原生 mousedown/move/up 手寫 | SortableJS、Vue Draggable、React Beautiful DnD |
| 組件化 | 內聯 HTML 字符串 | Vue/React/Svelte 組件,Props 驅動渲染 |
| 狀態管理 | 全局變量 + localStorage | Pinia/Redux/Zustand + 持久化中間件 |
| 模塊註冊 | 硬編碼 ID | 插件機制:定義 PortalModule 接口,動態註冊/發現 |
<script>
// 配置
const STORAGE_KEY = 'my_portal_layout_v1';
const leftCol = document.getElementById('left-column'); // 對應左側 td
const rightCol = document.getElementById('right-column'); // 對應右側 td
// 1. 初始化:從 localStorage 恢復
function restoreLayout() {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
if (!saved) return;
['left','right'].forEach(side => {
const col = side === 'left' ? leftCol : rightCol;
saved[side].forEach(mod => {
const el = document.getElementById(mod.id);
if (el) {
col.appendChild(el); // 移動現有節點到正確順序
if (mod.collapsed) el.classList.add('collapsed');
}
});
});
}
// 2. 拖拽核心 (精簡,生產建議用 SortableJS)
let dragged = null;
document.querySelectorAll('.dragTR').forEach(handle => {
handle.addEventListener('mousedown', e => {
dragged = handle.closest('.dragTable');
dragged.classList.add('dragging');
document.body.style.cursor = 'grabbing';
});
});
document.addEventListener('mousemove', e => {
if (!dragged) return;
// 簡易:懸停在哪個 column 上就移入該 column 末尾
const overCol = e.target.closest('#left-column, #right-column');
if (overCol && !overCol.contains(dragged)) {
overCol.appendChild(dragged);
}
});
document.addEventListener('mouseup', () => {
if (!dragged) return;
dragged.classList.remove('dragging');
document.body.style.cursor = '';
saveLayout();
dragged = null;
});
// 3. 摺疊切換
document.querySelectorAll('.dragTR [onclick*="collapseExpand"]').forEach(btn => {
btn.onclick = e => {
e.target.closest('.dragTable').classList.toggle('collapsed');
saveLayout();
};
});
// 4. 保存
function saveLayout() {
const layout = { left: [], right: [] };
['left','right'].forEach(side => {
const col = side === 'left' ? leftCol : rightCol;
col.querySelectorAll('.dragTable').forEach(tbl => {
layout[side].push({
id: tbl.id,
collapsed: tbl.classList.contains('collapsed')
});
});
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(layout));
console.log('Layout saved:', layout);
}
// 入口
restoreLayout();
</script>
從「Table 拖拽」到「組件化儀表盤」,核心需求不變:用戶自定義佈局 + 狀態持久化。理解原始碼的 DOM 結構與狀態序列化邏輯,有助於在現代框架中設計更優雅的 Portal/Dashboard 系統。關鍵在於:唯一 ID 標識模塊、順序數組表示佈局、布爾值表示摺疊、存儲介質解耦。