Mochi0622's picture
initial deploy
e66cfb4
Raw
History Blame Contribute Delete
8.95 kB
"""
components/cards.py
--------------------
所有卡片元件的單一來源。
公開 API:
create_chapter_card(data) → dmc.Card (章節卡片,原 Chapter_card.py)
render_card_from_node(card_node) → dmc.Card (結構化 node 卡片,新版)
generate_custom_markdown_card(...) → dmc.Card (舊版 Markdown 字串卡片,保留供過渡期使用)
遷移建議:
- 新頁面請使用 render_card_from_node(接受 JSON 結構化資料)
- generate_custom_markdown_card 標記為 deprecated,待舊頁面遷移完成後刪除
"""
from __future__ import annotations
from typing import Any
from dash import html, dcc
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from components.render_utils import _inline_math, _block_math, _runs_to_children
try:
import dash_latex
DashLatex = dash_latex.DashLatex
except Exception:
DashLatex = None
# ---------------------------------------------------------------------------
# 1. 章節卡片(原 Chapter_card.py)
# ---------------------------------------------------------------------------
def create_chapter_card(data: dict) -> dmc.Card:
"""
產生章節總覽用的卡片。
data 欄位:
title str 卡片標題
badges List[Tuple[str, str]] [(label, color), ...]
description List[str] 說明文字(最多 4 行)
link str 「單元學習」按鈕連結
test str 「小測驗」按鈕連結
trick str (選用) 「技巧學習」按鈕連結
"""
buttons = [
dmc.Anchor(
href=data["link"],
children=dmc.Button(
"單元學習", variant="light", color="orange", size="sm", radius="xl"
),
)
]
if "trick" in data:
buttons.append(
dmc.Anchor(
href=data["trick"],
children=dmc.Button(
"技巧學習", variant="outline", color="gray", size="sm", radius="xl"
),
)
)
buttons.append(
dmc.Anchor(
href=data["test"],
children=dmc.Button(
"小測驗", variant="filled", color="blue", size="sm", radius="xl"
),
)
)
# 說明文字補足到 4 行,保持卡片高度一致
desc_items = [
dmc.Text(line, size="sm", c="dimmed") for line in data["description"]
]
desc_items += [
dmc.Text(" ", size="sm")
for _ in range(4 - len(data["description"]))
]
return dmc.Card(
children=[
dmc.Text(data["title"], fw=700, size="lg"),
dmc.Group(
gap="xs",
mt="xs",
mb="sm",
children=[
dmc.Badge(label, color=color, radius="md")
for label, color in data["badges"]
],
),
dmc.Stack(gap=0, style={"minHeight": 100}, children=desc_items),
dmc.Flex(
gap="sm",
wrap="wrap",
justify="flex-start",
mt="sm",
children=buttons,
),
],
withBorder=True,
shadow="sm",
radius="md",
style={
"width": {
"base": 280,
"sm": 320,
"md": 350,
"lg": 400,
}
},
)
# ---------------------------------------------------------------------------
# 2. 結構化 node 卡片(原 generate_common_card.py:render_card_from_node)
# ---------------------------------------------------------------------------
def render_card_from_node(card_node: dict) -> dmc.Card:
"""
把 Test2ChMd 產生的卡片 JSON node 渲染成 DMC Card。
card_node 格式:
{
"kind": str, # 卡片種類(例如「定義」「定理」)
"headline": str, # 標題文字
"body": List[block], # 內容 block 清單
}
body block 支援的 type:
paragraph / list / math / url / picture / horizontal_rule
"""
kind = (card_node.get("kind") or "卡片").strip()
headline = (card_node.get("headline") or "").strip()
header = dmc.CardSection(
dmc.Group(
[
dmc.Badge(kind, color="violet", variant="filled"),
dmc.Text(headline, fw=700, style={"whiteSpace": "pre-wrap"}),
],
gap="sm",
),
withBorder=True,
inheritPadding=True,
py=6,
style={
"backgroundColor": dmc.DEFAULT_THEME["colors"]["violet"][7],
"color": "white",
},
)
body_children: list[Any] = []
for b in card_node.get("body", []):
t = (b.get("type") or "").lower()
if t == "paragraph":
body_children.append(
html.Div(_runs_to_children(b.get("runs") or []))
)
elif t == "list":
items = [
dmc.ListItem(_runs_to_children(it.get("runs") or []))
for it in b.get("content", [])
]
body_children.append(
dmc.List(items, listStyleType="none", spacing="xs", withPadding=True)
)
elif t == "math":
body_children.append(_block_math(b.get("latex", "")))
elif t == "url":
body_children.append(
dcc.Markdown(
f"[{b.get('text')}]({b.get('href')})", link_target="_blank"
)
)
elif t == "picture":
body_children.append(
html.Img(
src=b.get("src"),
title=b.get("alt") or "",
style={
"display": "inline-block",
"margin": "6px",
"maxWidth": "100%",
"height": "auto",
},
)
)
elif t == "horizontal_rule":
body_children.append(dmc.Divider(my=12))
return dmc.Card(
[header, dmc.Stack(body_children, gap="xs", px="md", py="sm")],
withBorder=True,
shadow="md",
radius="md",
mb=14,
)
# ---------------------------------------------------------------------------
# 3. 舊版 Markdown 字串卡片(deprecated,保留至頁面遷移完成)
# ---------------------------------------------------------------------------
def generate_custom_markdown_card(
title: str,
paragraphs: list[dict],
card_color: str = "#f8f9fa",
title_bg_color: str = dmc.DEFAULT_THEME["colors"]["green"][7],
latex_list: list[str] | None = None,
) -> dmc.Card:
"""
.. deprecated::
請改用 render_card_from_node。
此函式將在所有舊頁面遷移完成後刪除。
paragraphs 格式:
[{"text": str, "align": "left|center|right", "color": str}, ...]
latex_list 格式:
["latex_str_1", "latex_str_2", ...] (有序清單)
"""
def _render_paragraph(p: dict) -> html.Div:
return html.Div(
dcc.Markdown(p["text"], mathjax=True),
style={
"textAlign": p.get("align", "left"),
"color": p.get("color", "black"),
"marginBottom": "0.5rem",
},
)
content: list[Any] = [_render_paragraph(p) for p in paragraphs]
if latex_list:
# 需要 dash_latex;若未安裝則退回純文字
list_items = []
for latex in latex_list:
if DashLatex:
list_items.append(
dmc.ListItem(DashLatex(f"${latex}$"), mt=10)
)
else:
list_items.append(
dmc.ListItem(dcc.Markdown(f"${latex}$", mathjax=True), mt=10)
)
content.append(
dmc.List(
children=list_items,
listStyleType="ordered",
spacing="sm",
)
)
return dmc.Card(
children=[
dmc.CardSection(
dmc.Group(
children=[
dmc.Text(
title,
fw=500,
style={"color": dmc.DEFAULT_THEME["colors"]["gray"][0]},
)
]
),
withBorder=True,
inheritPadding=True,
py=3,
style={"backgroundColor": title_bg_color},
),
html.Div(children=content, style={"padding": "10px"}),
],
withBorder=True,
shadow="md",
radius="md",
style={"backgroundColor": card_color},
)