Spaces:
Sleeping
Sleeping
| """ | |
| layout/page/page_layout.py | |
| --------------------------- | |
| 頁面主體 layout 組裝。 | |
| 只負責把各子模組的產出組合成最終的 dmc.Container, | |
| 不含資料邏輯,也不直接定義 callback。 | |
| 公開 API: | |
| make_test_page(json_source, *, top_offset, show_src) → dmc.Container | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| import traceback | |
| from dash import html, dcc | |
| import dash_mantine_components as dmc | |
| from ..renderer.renderer import render_doc | |
| try: | |
| from ..renderer.renderer import install_callbacks_once | |
| except Exception: | |
| install_callbacks_once = None | |
| from ..toc import make_toc | |
| from .loader import load_json, build_toc_titles | |
| from .sections import ( | |
| split_blocks_into_sections, | |
| get_section_blocks, | |
| make_section_control, | |
| ) | |
| from .page_callbacks import register_page_callbacks | |
| # --------------------------------------------------------------------------- | |
| # 回到頂端按鈕樣式(獨立常數,方便日後調整) | |
| # --------------------------------------------------------------------------- | |
| _BACK_TO_TOP_STYLE: dict[str, Any] = { | |
| "position": "fixed", | |
| "right": "24px", | |
| "bottom": "24px", | |
| "width": "46px", | |
| "height": "46px", | |
| "borderRadius": "999px", | |
| "border": "none", | |
| "textDecoration": "none", | |
| "display": "flex", | |
| "alignItems": "center", | |
| "justifyContent": "center", | |
| "background": "rgba(0, 0, 0, 0.35)", | |
| "color": "white", | |
| "fontSize": "22px", | |
| "cursor": "pointer", | |
| "zIndex": 999, | |
| "backdropFilter": "blur(4px)", | |
| "boxShadow": "0 4px 12px rgba(0,0,0,0.18)", | |
| } | |
| # TOC 欄固定定位樣式 | |
| def _toc_fixed_style(top_offset: int) -> dict[str, Any]: | |
| return { | |
| "position": "sticky", | |
| "top": f"{top_offset + 12}px", | |
| "maxHeight": f"calc(100vh - {top_offset + 32}px)", | |
| "overflowY": "auto", | |
| "overflowX": "hidden", | |
| "paddingRight": "4px", | |
| "alignSelf": "flex-start", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # 主要組裝函式 | |
| # --------------------------------------------------------------------------- | |
| def make_test_page( | |
| json_source: Any, | |
| *, | |
| top_offset: int = 90, | |
| show_src: bool = True, | |
| ) -> dmc.Container: | |
| """ | |
| 組裝完整的頁面 Container。 | |
| json_source:可傳入檔案路徑(str/Path)、已解析的 dict 或 list。 | |
| top_offset: 頁首高度(px),用於 TOC 固定定位與 scroll offset。 | |
| show_src: 保留參數(尚未使用),預留供未來顯示原始 JSON 用。 | |
| """ | |
| # 1. 確保 dashTest callbacks 已掛載 | |
| if callable(install_callbacks_once): | |
| try: | |
| install_callbacks_once() | |
| except Exception: | |
| pass | |
| # 2. 確保頁面 callbacks 已掛載 | |
| register_page_callbacks() | |
| # 3. 載入資料 | |
| data = load_json(json_source) | |
| blocks = data.get("blocks", []) | |
| sections = data.get("sections", []) or [] | |
| raw_toc = data.get("toc", []) or [] | |
| # 4. 若 JSON 未提供 sections,自動按 H2 切割 | |
| if not sections: | |
| sections = split_blocks_into_sections(blocks) | |
| # 5. 決定初始 section | |
| default_section_id = sections[0].get("id") if sections else None | |
| current_blocks = get_section_blocks(sections, default_section_id) if sections else blocks | |
| # 6. 渲染初始內容 | |
| try: | |
| content = render_doc(current_blocks) | |
| except Exception as e: | |
| content = dmc.Alert( | |
| f"render_doc 發生錯誤:{e}\n\n{traceback.format_exc(limit=2)}", | |
| title="渲染失敗", | |
| color="red", | |
| variant="filled", | |
| ) | |
| # 7. Section 控制列 | |
| section_control = make_section_control(sections, default_section_id) | |
| # 8. 左欄:Store + 控制列 + 內容 | |
| left_children: list[Any] = [ | |
| dcc.Store(id="page-sections-store", data=sections), | |
| dcc.Store(id="page-raw-toc-store", data=raw_toc), | |
| dcc.Store(id="page-active-section-store", data=default_section_id), | |
| ] | |
| if section_control is not None: | |
| left_children.append(section_control) | |
| left_children.append(html.Div(content, id="section-content-wrapper")) | |
| left_area = dmc.Stack(left_children, gap="md", style={"minWidth": 0}) | |
| # 9. TOC(僅當有標題時才產生) | |
| toc_titles = build_toc_titles(current_blocks, raw_toc) | |
| has_toc = bool(toc_titles) | |
| toc_component = ( | |
| make_toc( | |
| titles=toc_titles, | |
| top_offset=top_offset, | |
| col_span={"xs": 0, "sm": 0, "md": 3, "lg": 3, "xl": 3}, | |
| visible_from="md", | |
| ) | |
| if has_toc | |
| else None | |
| ) | |
| # 10. Grid 欄位配置 | |
| main_span = {"base": 12, "md": 9 if has_toc else 12} | |
| toc_span = {"base": 12, "md": 3} if has_toc else {"base": 12, "md": 0} | |
| toc_style = _toc_fixed_style(top_offset) if has_toc else {} | |
| cols = [ | |
| dmc.GridCol(left_area, id="main-content-col", span=main_span), | |
| dmc.GridCol( | |
| html.Div( | |
| id="section-toc-wrapper", | |
| children=toc_component, | |
| style=toc_style, | |
| ), | |
| id="toc-col", | |
| span=toc_span, | |
| className="toc-col-wrapper", | |
| style={"display": "block" if has_toc else "none"}, | |
| ), | |
| ] | |
| # compat:空 Grid 讓 Dash 的 pattern-matching callback 不出錯 | |
| grid = dmc.Grid(cols, gutter="xl") | |
| compat = dmc.Grid(id="Grid", style={"display": "none"}) | |
| return dmc.Container( | |
| [ | |
| html.Div(id="page-top-anchor"), | |
| dmc.Stack([grid, compat], gap="md"), | |
| html.A( | |
| "↑", | |
| href="#page-top-anchor", | |
| title="回到最上方", | |
| id="back-to-top-btn", | |
| style=_BACK_TO_TOP_STYLE, | |
| ), | |
| ], | |
| fluid=True, | |
| style={"overflow": "visible"}, | |
| ) | |