PythonIDE Docs
中文
简体中文

原生列表与详情

NavigationStack、NavigationLink、搜索、刷新和滑动操作。

演示 List + searchableNavigationLink 详情、swipe_actionsrefreshable 的标准列表流。

#预期效果

运行后会出现原生列表详情页,搜索、分类、导航详情和行级操作都能产生可见反馈。

#适合场景

  • 项目列表、课程目录、素材库、文件浏览等一列数据进入详情的页面。
  • 需要搜索、筛选、下拉刷新、行滑动操作和详情页的标准列表流。

#页面结构

区域结构作用
顶层NavigationStack承载列表标题和详情页返回栈。
筛选区Section + Picker切换分类并展示当前状态。
内容区List + ForEach + NavigationLink稳定渲染动态行并进入详情。
行操作.swipe_actions(...)完成、删除等高频动作。

#完整示例

python
import appui
import haptics

state = appui.State(
    query="",
    filter="全部",
    status="就绪",
    items=[
        {"id": "inbox", "title": "收件箱整理", "subtitle": "今天完成 12 条消息归档", "tag": "工作", "done": False},
        {"id": "clip", "title": "剪贴板素材", "subtitle": "保存常用文案和链接", "tag": "工具", "done": True},
        {
            "id": "video",
            "title": "视频片源检查",
            "subtitle": "确认 HLS 和 MP4 是否可播放",
            "tag": "媒体",
            "done": False,
        },
        {"id": "health", "title": "血压周报", "subtitle": "统计最近 7 天平均值", "tag": "健康", "done": True},
    ],
)


def set_query(value):
    state.query = value


def set_filter(value):
    state.filter = value


def visible_items():
    query = state.query.strip().lower()
    rows = []
    for item in state.items:
        if state.filter != "全部" and item["tag"] != state.filter:
            continue
        text = f"{item['title']} {item['subtitle']} {item['tag']}".lower()
        if query and query not in text:
            continue
        rows.append(item)
    return rows


def toggle_done(item_id):
    rows = []
    for item in state.items:
        copy = dict(item)
        if copy["id"] == item_id:
            copy["done"] = not copy["done"]
        rows.append(copy)
    state.items = rows
    haptics.selection()


def delete_item(item_id):
    state.items = [item for item in state.items if item["id"] != item_id]
    state.status = "已删除"
    haptics.notification("success")


def reload_items():
    state.status = "已刷新"
    haptics.selection()


def detail_view(item):
    def toggle_current():
        toggle_done(item["id"])

    return appui.Form(
        [
            appui.Section(
                [
                    appui.LabeledContent("标题", value=item["title"]),
                    appui.LabeledContent("分类", value=item["tag"]),
                    appui.LabeledContent("状态", value="完成" if item["done"] else "进行中"),
                ],
                header="信息",
            ),
            appui.Section(
                [
                    appui.Text(item["subtitle"]).font("body"),
                    appui.Button("切换完成状态", action=toggle_current),
                ],
                header="操作",
            ),
        ]
    ).navigation_title(item["title"])


def row_view(item):
    def toggle_current():
        toggle_done(item["id"])

    def delete_current():
        delete_item(item["id"])

    status_icon = "checkmark.circle.fill" if item["done"] else "circle"
    status_color = "systemGreen" if item["done"] else "secondaryLabel"
    return appui.NavigationLink(
        destination=detail_view(item),
        label=appui.HStack(
            [
                appui.Image(system_name=status_icon).foreground_color(status_color),
                appui.VStack(
                    [
                        appui.Text(item["title"]).font("headline"),
                        (
                            appui.Text(item["subtitle"])
                            .font("caption")
                            .foreground_color("secondaryLabel")
                        ),
                    ],
                    alignment="leading",
                    spacing=3,
                ),
                appui.Spacer(),
                appui.Text(item["tag"]).font("caption").foreground_color("systemBlue"),
            ],
            spacing=10,
        ),
    ).swipe_actions(
        actions=[
            appui.Button("完成", action=toggle_current),
            appui.Button("删除", role="destructive", action=delete_current),
        ]
    )


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


def body():
    rows = visible_items()
    content = (
        appui.ContentUnavailableView(
            "没有结果",
            system_image="magnifyingglass",
            description="换个关键词或分类再试",
        )
        if not rows
        else appui.ForEach(rows, row_view, key=row_key)
    )
    return appui.NavigationStack(
        appui.List(
            [
                appui.Section(
                    [
                        appui.Picker(
                            "分类",
                            selection=state.filter,
                            options=["全部", "工作", "工具", "媒体", "健康"],
                            on_change=set_filter,
                        ),
                        appui.LabeledContent("状态", value=state.status),
                    ],
                    header="筛选",
                ),
                appui.Section("事项", [content]),
            ]
        )
        .searchable(text=state.query, on_change=set_query)
        .refreshable(reload_items)
        .navigation_title("事项")
    )


appui.run(body, state=state, presentation="sheet")

#关键技巧

  • state.items 只存纯数据(字典列表),不要存入 Text/Button 等视图对象。
  • 搜索用 List(...).searchable(...),不要用自绘 TextField 条顶替系统搜索栏。
  • 详情进路由用 NavigationLink;行级操作放 swipe_actions;下拉刷新用 refreshable

#失败路径

  • 搜索和筛选没有结果时显示 ContentUnavailableView,不要让列表区域空白。
  • 删除或刷新后把结果写回 state.status,用户能看见刚才发生了什么。

#可替换点

当前写法可替换为
静态 state.itemsstorage.get_json(...) 或网络请求结果
haptics.selection()普通状态文本、toast 或 alert
分类 Picker搜索栏、分段控件或多个筛选字段

#相关文档

文档用途
导航与页面结构列表详情和导航栈。
呈现 APIswipe_actionsrefreshable