/** * 第 5 章配套代码 · Todo List(observableList + 派生属性 综合演示) * * 文件位置:shared/src/commonMain/kotlin/com/example/kuikly/pages/TodoListPage.kt * * 本示例展示: * - observable 单值(input/filter/nextId) * - observableList 列表响应式 * - 派生属性(filteredTodos / activeCount) * - data class.copy() 替换 item 触发刷新 * - 跨字段联动:改 input → 加按钮文案变化 */ package com.example.kuikly.pages import com.tencent.kuikly.core.annotations.Page import com.tencent.kuikly.core.base.Border import com.tencent.kuikly.core.base.BorderStyle import com.tencent.kuikly.core.base.Color import com.tencent.kuikly.core.base.ViewBuilder import com.tencent.kuikly.core.pager.Pager import com.tencent.kuikly.core.reactive.collection.observableListOf import com.tencent.kuikly.core.reactive.handler.observable import com.tencent.kuikly.core.views.Input import com.tencent.kuikly.core.views.Text import com.tencent.kuikly.core.views.View data class TodoItem(val id: Int, val title: String, val done: Boolean) @Page("TodoListPage") internal class TodoListPage : Pager() { // ───── 响应式状态 ───── private val todos = observableListOf() private var input by observable("") private var filter by observable("all") // all / active / done private var nextId = 1 // 普通字段(不需要驱动 UI) // ───── 派生属性(自动响应)───── private val filteredTodos: List get() = when (filter) { "active" -> todos.filter { !it.done } "done" -> todos.filter { it.done } else -> todos } private val activeCount: Int get() = todos.count { !it.done } private val canAdd: Boolean get() = input.trim().isNotEmpty() override fun created() { super.created() // 演示用:预置 3 条 todos.add(TodoItem(nextId++, "看 Kuikly 第 5 章", true)) todos.add(TodoItem(nextId++, "敲一遍 Todo 示例", false)) todos.add(TodoItem(nextId++, "回顾响应式原理", false)) } override fun body(): ViewBuilder { val ctx = this return { attr { backgroundColor(Color(0xFFF5F5F7L)) paddingTop(40f) paddingHorizontal(16f) } // ─── 标题 ─────────────── Text { attr { text("我的 Todo") fontSize(22f) fontWeightBold() color(Color.BLACK) marginBottom(16f) } } // ─── 输入区 ───────────── View { attr { flexDirectionRow(); marginBottom(16f) } Input { attr { flex(1f); height(44f) placeholder("写下你今天要做的事…") placeholderColor(Color(0xFFBDBDBDL)) paddingHorizontal(12f) backgroundColor(Color.WHITE) borderRadius(8f) marginRight(8f) fontSize(14f) color(Color.BLACK) value(ctx.input) } event { onTextChange { p -> ctx.input = p.text } } } View { attr { width(64f); height(44f); allCenter() backgroundColor( if (ctx.canAdd) Color(0xFF1976D2L) else Color(0xFFBDBDBDL) ) borderRadius(8f) opacity(if (ctx.canAdd) 1f else 0.6f) } event { click { if (!ctx.canAdd) return@click ctx.todos.add(TodoItem(ctx.nextId++, ctx.input.trim(), false)) ctx.input = "" } } Text { attr { text("+") color(Color.WHITE) fontSize(22f) fontWeightBold() } } } } // ─── 筛选 Tab ─────────── View { attr { flexDirectionRow(); marginBottom(12f) } listOf( "全部" to "all", "未完成" to "active", "已完成" to "done", ).forEach { (label, value) -> View { attr { flex(1f); height(36f); allCenter() backgroundColor( if (ctx.filter == value) Color(0xFF1976D2L) else Color.WHITE ) borderRadius(8f) marginHorizontal(2f) } event { click { ctx.filter = value } } Text { attr { text(label) fontSize(13f) color( if (ctx.filter == value) Color.WHITE else Color(0xFF424242L) ) } } } } } // ─── 计数(派生属性自动更新)─ Text { attr { text("还剩 ${ctx.activeCount} 项未完成 · 共 ${ctx.todos.size} 项") fontSize(12f) color(Color(0xFF9E9E9EL)) marginBottom(8f) } } // ─── 列表渲染 ──────────── // 注:第 6 章会教用 List 组件做长列表复用,这里直接 forEach 简单演示 ctx.filteredTodos.forEach { todo -> renderTodoRow(ctx, todo).invoke(this) } } } /** 单行 Todo 抽成 ViewBuilder,便于复用 + 阅读 */ private fun renderTodoRow(ctx: TodoListPage, todo: TodoItem): ViewBuilder = { View { attr { flexDirectionRow() alignItemsCenter() padding(12f) marginBottom(8f) backgroundColor(Color.WHITE) borderRadius(8f) } // ─── 完成态圆圈 ─── View { attr { size(20f, 20f); allCenter() borderRadius(10f) border( Border(1.5f, BorderStyle.SOLID, Color(0xFF1976D2L)) ) backgroundColor( if (todo.done) Color(0xFF1976D2L) else Color.WHITE ) marginRight(12f) } event { click { // ★ 关键:data class .copy() 替换整个 item val idx = ctx.todos.indexOfFirst { it.id == todo.id } if (idx >= 0) { ctx.todos[idx] = todo.copy(done = !todo.done) } } } if (todo.done) { Text { attr { text("✓") color(Color.WHITE) fontSize(13f) fontWeightBold() } } } } // ─── 标题 ─── Text { attr { flex(1f) text(todo.title) fontSize(15f) color( if (todo.done) Color(0xFFBDBDBDL) else Color.BLACK ) if (todo.done) textDecorationLineThrough() } } // ─── 删除 ─── Text { attr { text("✕") color(Color(0xFFFF5252L)) fontSize(18f) paddingHorizontal(8f) } event { click { ctx.todos.removeAll { it.id == todo.id } } } } } } }