从手机单栏到平板分栏:任务清单的筛选、选中态与不可变更新

任务清单最容易停在"能显示几行文字"的阶段。真正开始操作以后,新增任务要进入列表,筛选条件要立即生效,勾选状态要同步顶部计数,宽屏详情还要始终指向当前任务。只要其中一个状态单独维护,页面很快就会出现列表和详情不一致的问题。

这一版从单一数据源开始。每个 TaskItem 同时保存标题、详情、完成状态和标签,页面只记录当前选择、筛选条件与输入内容,不再额外维护一份"已完成列表"。

typescript 复制代码
interface TaskItem {
  id: number
  title: string
  detail: string
  done: boolean
  tag: string
}

@State selectedId: number = 1
@State filter: string = '全部'
@State newTask: string = ''
@State tasks: TaskItem[] = [/* 初始任务 */]

筛选结果通过 visibleTasks() 实时派生。选择"待处理"时过滤 done === false,选择"已完成"时过滤完成项,其他情况返回完整数组。这样勾选任务以后不需要手动维护第二份数组,当前筛选下的列表会随 tasks 更新。

typescript 复制代码
private visibleTasks(): TaskItem[] {
  if (this.filter === '待处理') {
    return this.tasks.filter((item: TaskItem) => !item.done)
  }
  if (this.filter === '已完成') {
    return this.tasks.filter((item: TaskItem) => item.done)
  }
  return this.tasks
}

状态切换没有直接写 item.done = !item.done。任务数组属于 @State,通过 map() 返回新对象和新数组,ArkUI 能够明确观察到引用变化;未命中的元素保持原对象,只有目标任务被替换。

typescript 复制代码
private toggleTask(id: number): void {
  this.tasks = this.tasks.map((item: TaskItem) => {
    if (item.id === id) {
      return {
        id: item.id,
        title: item.title,
        detail: item.detail,
        done: !item.done, // 只替换被操作的任务
        tag: item.tag
      }
    }
    return item
  })
}

直接修改数组元素在简单页面里有时也能看到变化,但随着列表、计数和详情同时依赖这份数据,刷新边界会越来越难判断。现在顶部完成数量直接对 tasks 过滤,详情按钮也调用同一个 toggleTask(),两处行为保持一致。

新增流程多处理了几个容易漏掉的状态。输入先 trim(),空字符串停在错误提示;成功后把新任务放到列表顶部,选中它,清空输入框,并把筛选恢复成"全部"。如果新增时仍停留在"已完成",新建的未完成任务会被过滤掉,用户会误以为按钮没有生效,因此筛选复位不是装饰动作。

typescript 复制代码
private addTask(): void {
  const title: string = this.newTask.trim()
  if (title.length === 0) {
    this.tip = '任务内容不能为空'
    return
  }

  const task: TaskItem = {
    id: Date.now(),
    title: title,
    detail: '刚刚添加,稍后补充详情',
    done: false,
    tag: '临时'
  }
  this.tasks = [task, ...this.tasks]
  this.selectedId = task.id
  this.newTask = ''
  this.filter = '全部'
  this.tip = `已添加:${title}`
}

手机宽度下只展示任务列表,减少详情区域对纵向空间的占用。页面宽度达到700以后,左侧列表占46%,右侧详情使用剩余空间。这里没有创建两套页面,TaskList()DetailPanel() 都读取同一组状态。

typescript 复制代码
.onAreaChange((_oldValue: Area, newValue: Area) => {
  this.wideMode = Number(newValue.width) >= 700
})

if (this.wideMode) {
  Row({ space: 18 }) {
    Column() { this.TaskList() }.width('46%').height('100%')
    Column() { this.DetailPanel() }.layoutWeight(1).height('100%')
  }
} else {
  this.TaskList()
}

初始运行时,列表里同时存在已完成和待处理任务,顶部计数从同一数组计算。勾选框、标签和选中背景给出了三种不同状态,后续操作不需要跳转页面。

切到"待处理"以后,已经完成的条目从当前视图移除,但数据没有删除;选中某条任务时,背景变化由 selectedId 控制。

当前版本的数据只存在组件内存中,应用重启后会回到初始任务;详情文本也还没有编辑入口。继续完善时,应先接入持久化,再为新增任务增加详情和标签输入。分栏本身已经与数据层解耦,这些字段扩展不需要重写手机和平板两套状态逻辑。

选中任务与筛选结果不是同一份状态

selectedId 保存的是全量任务中的选择,visibleTasks() 返回的是当前筛选结果。二者职责不同:切到"已完成"时,被选中的待处理任务可能暂时不在列表里,但原数据没有消失。宽屏详情仍可按ID从完整数组查找。

typescript 复制代码
private selectedTask(): TaskItem {
  const result: TaskItem[] = this.tasks.filter(
    (item: TaskItem) => item.id === this.selectedId
  )
  return result.length > 0 ? result[0] : this.tasks[0]
}

当前回退到 tasks[0],前提是数组至少有一条记录。加入删除功能后,最后一条任务被删除会让这里返回 undefined,详情区域继续访问标题就会出错。删除能力进入工程前,需要先定义空任务状态,并在删除当前项后选择相邻任务。

详情面板本身没有复制任务内容,所有文字和按钮都通过 selectedTask() 读取:

typescript 复制代码
@Builder
DetailPanel() {
  Column({ space: 18 }) {
    Text('任务详情')
    Text(this.selectedTask().title)
      .fontSize(26)
      .fontWeight(FontWeight.Bold)
    Text(this.selectedTask().detail)
      .fontSize(16)
      .lineHeight(25)
    Text(`标签 · ${this.selectedTask().tag}`)
    Button(this.selectedTask().done ? '重新打开' : '标记完成')
      .onClick(() => {
        this.toggleTask(this.selectedId)
      })
  }
}

列表勾选和详情按钮最终调用同一个 toggleTask(),避免两种入口产生不同结果。顶部的完成数也直接过滤任务数组,不需要在两个按钮里各自加减计数。

宽屏断点为什么放在页面容器上

断点判断绑定最外层容器的 onAreaChange,读取的是页面实际布局宽度,而不是假设设备型号。窗口缩放、分屏或横竖屏变化都能重新计算 wideMode

typescript 复制代码
.onAreaChange((_oldValue: Area, newValue: Area) => {
  this.wideMode = Number(newValue.width) >= 700
})

700是当前页面根据列表最小宽度和详情阅读宽度选择的经验断点,不是系统规定值。左栏设为46%,右栏用 layoutWeight(1) 吸收剩余空间,18像素间距只出现于宽屏分栏。

窄屏时详情面板不渲染,因此点击任务只改变选中背景,没有单独详情页。若手机也需要编辑详情,应增加页面跳转或覆盖层,而不是强行把分栏压缩进小屏。宽屏与窄屏可以共享数据状态,但交互路径不必完全相同。

数据落盘前先处理未完成的输入

当前任务只存在内存中。接入本地存储时,保存对象应包含 tasks,筛选条件和 wideMode 属于临时UI状态,不需要落盘。selectedId 是否恢复可以根据体验决定,但恢复前必须确认对应任务仍存在。

新增任务使用 Date.now() 生成ID,手动点击场景足够;批量导入或并发创建时需要更可靠的唯一标识。标签和详情目前使用默认值,完整表单应在构造 TaskItem 前一次完成校验,避免先插入记录、再逐字段补写造成中间状态。

相关推荐
m0_749690232 小时前
【寻迹校园 HarmonyOS NEXT 实战 28】不交换手机号也能交接:固定校内交接点的隐私设计
华为·harmonyos·arkts·产品设计·隐私设计·安全交接
Magic-ZYJ2 小时前
HarmonyOS Stage 模型实战:UIAbility 生命周期如何驱动页面安全状态
安全·华为·harmonyos·鸿蒙·移动端开发·独立开发者·心晴手记
贾伟康2 小时前
【中国方言题库|09】HarmonyOS ArkTS 方言搜索实战:实现词语检索和无结果反馈
harmonyos·arkts·状态管理·arkui·本地搜索
梦想不只是梦与想3 小时前
鸿蒙 AppGallery Connect:查看应用信息(三)
harmonyos·appgallery·client id·app id·developer id
贾伟康3 小时前
【中国方言题库|03】HarmonyOS ArkTS 四川话分库实战:复用题库组件并保持地区参数清晰
harmonyos·arkts·arkui·路由传参·组件复用
贾伟康3 小时前
【中国方言题库|10】HarmonyOS ArkTS 语音播放实战:管理读音播放与页面生命周期
生命周期·harmonyos·arkts·语音合成·texttospeech
YM52e14 小时前
鸿蒙ArkTS实战项目 - 门店陈列巡检台:巡检卡片与多列切换实现
学习·华为·harmonyos
lilian23315 小时前
Harmony os 技术实战|拼豆制图27:用单字符编码承载 50 张 70×70 图纸
前端·数据库·华为·harmonyos
HwJack2021 小时前
UIAbility 生命周期全链路:从冷启动到热启动的实战笔记
笔记·华为·harmonyos