PythonIDE Docs
中文
简体中文

示例:紧凑仪表盘

看板卡片、网格布局和真实交互。

适合做首页概览、运营面板、学习进度页。这个样板使用 ScrollView + LazyVGrid,卡片高度稳定,点击卡片后会更新选中状态。

#预期效果

运行后会出现紧凑看板,指标卡、筛选表单和明细导航会随搜索和状态切换更新。

#完整代码

python
import appui

state = appui.State(selected="Revenue")

metrics = [
    {
        "id": "revenue",
        "name": "Revenue",
        "value": "$48.2K",
        "delta": "+12%",
        "color": "systemGreen",
    },
    {"id": "users", "name": "Users", "value": "8,420", "delta": "+5%", "color": "systemBlue"},
    {"id": "errors", "name": "Errors", "value": "14", "delta": "-3%", "color": "systemOrange"},
    {"id": "uptime", "name": "Uptime", "value": "99.98%", "delta": "stable", "color": "systemTeal"},
]


def metric_key(item):
    return item["id"]


def select_metric(name):
    state.selected = name


def metric_card(item):
    def open_metric():
        select_metric(item["name"])

    content = appui.VStack([
        appui.HStack([
            appui.Text(item["name"]).font("caption").foreground_color("secondaryLabel"),
            appui.Spacer(),
            appui.Text(item["delta"]).font("caption").foreground_color(item["color"]),
        ]),
        appui.Text(item["value"]).font("title2").bold(),
    ], alignment="leading", spacing=8)

    return (
        appui.Button(action=open_metric, content=content)
        .button_style("plain")
        .frame(max_width=appui.infinity, min_height=104, alignment="leading")
        .padding(14)
        .background("secondarySystemBackground", corner_radius=8)
    )


def body():
    grid = appui.LazyVGrid(
        columns=[appui.flexible(minimum=140), appui.flexible(minimum=140)],
        spacing=12,
        content=[appui.ForEach(metrics, row_builder=metric_card, key=metric_key)],
    )

    header = appui.VStack([
        appui.Text("Operations").font("largeTitle").bold()
            .frame(max_width=appui.infinity, alignment="leading"),
        appui.Text(f"Selected: {state.selected}")
            .foreground_color("secondaryLabel")
            .frame(max_width=appui.infinity, alignment="leading"),
    ], alignment="leading", spacing=4)

    return appui.NavigationStack(
        appui.ScrollView(
            appui.VStack([header, grid], alignment="leading", spacing=16).padding(16)
        )
        .navigation_title("Dashboard")
    )


appui.run(body, state=state)

#筛选表单与明细导航

运营类仪表盘如果有搜索、筛选和明细页,优先用 List + Section + NavigationLink。空结果用 ContentUnavailableView,不要只留空白页面。

python
import appui

state = appui.State(query="", status="All")

rows = [
    {"id": "revenue", "name": "Revenue", "status": "Good", "value": "$48.2K"},
    {"id": "errors", "name": "Errors", "status": "Needs Review", "value": "14"},
    {"id": "uptime", "name": "Uptime", "status": "Good", "value": "99.98%"},
]


def row_key(row):
    return row["id"]


def set_query(value):
    state.query = value


def set_status(value):
    state.status = value


def filtered_rows():
    query = state.query.strip().lower()
    result = rows
    if state.status != "All":
        result = [row for row in result if row["status"] == state.status]
    if query:
        result = [row for row in result if query in row["name"].lower()]
    return result


def detail_view(row):
    return appui.Form([
        appui.Section("Metric", [
            appui.LabeledContent("Name", value=row["name"]),
            appui.LabeledContent("Status", value=row["status"]),
            appui.LabeledContent("Value", value=row["value"]),
        ])
    ]).navigation_title(row["name"])


def row_view(row):
    return appui.NavigationLink(
        destination=detail_view(row),
        label=appui.LabeledContent(row["name"], value=row["value"]),
    )


def body():
    visible = filtered_rows()
    content = (
        appui.ContentUnavailableView(
            "No Metrics",
            system_image="chart.bar",
            description="Change the search or filter.",
        )
        if not visible
        else appui.ForEach(visible, row_builder=row_view, key=row_key)
    )

    return appui.NavigationStack(
        appui.List([
            appui.Section("Filter", [
                appui.Picker(
                    "Status",
                    selection=state.status,
                    options=["All", "Good", "Needs Review"],
                    on_change=set_status,
                )
                    .picker_style("segmented")
            ]),
            appui.Section("Metrics", [content]),
        ])
        .searchable(text=state.query, on_change=set_query)
        .navigation_title("Dashboard")
    )


appui.run(body, state=state)

#可复用点

  • 首页看板可以用 ScrollView + LazyVGrid,但普通数据列表仍优先用 List
  • 卡片用 Button 包裹,点击后必须更新状态或进入详情。
  • appui.infinity,不要写字符串 ".infinity"
  • 卡片 min_height 固定,避免不同内容导致网格跳动。