Spaces:
Sleeping
Sleeping
File size: 5,949 Bytes
e66cfb4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | """
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"},
)
|