逐行来回切换两个文件很容易漏掉修改,差异工具需要让文件选择、差异导航和修改摘要保持一致。

界面可以先稳定下来,但真正决定功能是否成立的是文件条目、差异块、搜索索引、选中ID和保存状态。使用稳定差异ID组织左右文本和当前选中项,并让窗口变化只影响排列方式。

这次实现先收住三条主线:

  • 读取并规范化文本,明确数据从哪里开始;
  • 建立稳定ID,让主要操作真正改变状态;
  • 按窗口宽度重排,补齐设备形态或退出路径。

两份文本先统一换行规则

页面重建不可怕,无法恢复对象才麻烦。围绕文件条目、差异块、搜索索引、选中ID和保存状态建立模型,就是为了避开筛选差异后保留旧索引,会让右侧跳到不相关的代码块。

export interface WorkspaceItem {
  id: string
  title: string
  detail: string
  keywords: string[]
  updatedAt: number
}

export interface WorkspaceState {
  query: string
  selectedId: string
  draft: string
  saved: boolean
}

export class FeatureController {
  private items: WorkspaceItem[] = [
    { id: 'a', title: '双文件差异工作台', detail: '当前工作区', keywords: ['workspace', 'current'], updatedAt: 3 },
    { id: 'b', title: '交互说明', detail: '实现记录', keywords: ['design', 'arkui'], updatedAt: 2 },
    { id: 'c', title: '导出结果', detail: '最近生成', keywords: ['output', 'result'], updatedAt: 1 }

差异块生成稳定锚点

交互层只发送动作意图,建立稳定ID所需的边界与返回值由控制器给出。

  ]
  private state: WorkspaceState = { query: '', selectedId: 'a', draft: '', saved: true }

  search(query: string): WorkspaceItem[] {
    this.state.query = query
    const keyword: string = query.trim().toLowerCase()
    if (keyword.length === 0) {
      return this.items.map((item: WorkspaceItem) => ({ id: item.id, title: item.title, detail: item.detail, keywords: item.keywords, updatedAt: item.updatedAt } as WorkspaceItem))
    }
    return this.items.filter((item: WorkspaceItem) => {
      const indexText: string = (item.title + ' ' + item.detail + ' ' + item.keywords.join(' ')).toLowerCase()
      return indexText.includes(keyword)
    })
  }

  select(id: string): WorkspaceItem | undefined {
    const target: WorkspaceItem | undefined = this.items.find((item: WorkspaceItem) => item.id === id)
    if (target) {
      this.state.selectedId = target.id
    }

建立稳定ID完成后,控制器把结果写回文件条目、差异块、搜索索引、选中ID和保存状态,视图只接收本次动作产生的新状态。

搜索只过滤导航不改结果

页面把模型转换成清楚的层级:左侧文件导航,中间双文件内容,右侧显示差异属性与统计。

import { FeatureController } from '../features/FeatureController'

interface NavItem { icon: string;
    label: string;
    count: string }

@Entry
@Component
struct Index {
  private featureController: FeatureController = new FeatureController()
  private nav: NavItem[] = [
    { icon: '◫', label: '工作区', count: '08' }, { icon: '⌘', label: '最近项目', count: '12' },
    { icon: '◇', label: '资源文件', count: '24' }, { icon: '✓', label: '已完成', count: '06' }
  ]

  build() {
    Column() {
      Row({ space: 12 }) {
        Stack() { Rect().width(34).height(34).radius(11).fill('#4669E8');
    Text('⌘').fontSize(16).fontColor(Color.White) }
        Text('Window Diff').fontSize(19).fontWeight(FontWeight.Bold).fontColor('#EAF0FF')
        Text('DESKTOP TOOL').fontSize(12).fontColor('#8D99B3').margin({ left: 8 })
        Blank()
        Row({ space: 8 }) { Text('⌕').fontSize(18).fontColor('#A9B4CA');
    Text('搜索命令或文件').fontSize(14).fontColor('#7F8AA2') }.width(230).padding({ left: 14, right: 14, top: 9, bottom: 9 }).backgroundColor('#222B3F').borderRadius(12)
        Text('● 已保存').fontSize(13).fontColor('#65D6B0').margin({ left: 10 })
      }.width('100%').height(62).padding({ left: 20, right: 20 }).backgroundColor('#151B2A')

      Row() {
        Column({ space: 8 }) {
          Text('WORKSPACE').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#6F7A91').width('100%').margin({ bottom: 8 })
          ForEach(this.nav, (item: NavItem, index: number) => {
            Row({ space: 11 }) { Text(item.icon).fontSize(16).fontColor(index === 0 ? Color.White : '#8390A8');
    Text(item.label).fontSize(15).fontColor(index === 0 ? Color.White : '#A7B0C1').layoutWeight(1);
    Text(item.count).fontSize(12).fontColor('#6F7A91') }.width('100%').padding(11).borderRadius(11).backgroundColor(index === 0 ? '#4669E8' : Color.Transparent)
          }, (item: NavItem) => item.label)
          Blank()
          Column({ space: 5 }) { Text('当前任务').fontSize(12).fontColor('#778298');
    Text('Index.ets').fontSize(15).fontWeight(FontWeight.Medium).fontColor('#DDE5F5');
    Text('8 处修改').fontSize(13).fontColor('#63C5FF') }.width('100%').padding(12).borderRadius(12).backgroundColor('#20283A')
        }.width(188).height('100%').padding(16).backgroundColor('#1A2131')

        Column({ space: 14 }) {
          Row() { Column({ space: 3 }) { Text('Index.ets').fontSize(26).fontWeight(FontWeight.Bold).fontColor('#20283A');
    Text('左右文件、差异导航和修改摘要保持同步').fontSize(14).fontColor('#7D8798') }.alignItems(HorizontalAlign.Start);
    Blank();
    Button('预览').backgroundColor('#EDF0F6').fontColor('#475269');
    Button('导出').backgroundColor('#4669E8').margin({ left: 8 }) }.width('100%')
          Row({ space: 14 }) {
            Column({ space: 12 }) {
              Row() { Text('01').fontSize(13).fontColor('#4669E8');
    Text('  INPUT').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#7D8798');
    Blank();
    Text('8 处修改').fontSize(13).fontColor('#7D8798') }.width('100%')
              Column({ space: 9 }) { Rect().width('72%').height(9).radius(5).fill('#CBD4E5');
    Rect().width('88%').height(9).radius(5).fill('#D9E0EC');
    Rect().width('58%').height(9).radius(5).fill('#4669E8').opacity(0.72);
    Rect().width('80%').height(9).radius(5).fill('#D9E0EC') }.width('100%').alignItems(HorizontalAlign.Start)
              Divider().color('#E6E9EF')
              Row({ space: 8 }) { Text('+').fontSize(16).fontColor('#4669E8');
    Text('添加一个工作节点').fontSize(14).fontColor('#69758A') }.width('100%')
              Blank()
              Row({ space: 8 }) { Text('●').fontSize(12).fontColor('#64CDA8');
    Text('结构检查通过').fontSize(13).fontColor('#657086') }.width('100%')
            }.layoutWeight(1).height('100%').padding(18).borderRadius(20).backgroundColor(Color.White).border({ width: 1, color: '#E6EAF0' })

            Column({ space: 12 }) {
              Row() { Text('实时预览').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#273044');
    Blank();

在这里插入图片描述

窗口变窄时改排列不改选中项

这一部分处理完成之外的结果:取消、过期、不可用与可以重试。

    return target
  }

  updateDraft(value: string): void {
    this.state.draft = value
    this.state.saved = false
  }

  save(): boolean {
    if (this.state.draft.trim().length === 0) {
      return false
    }
    this.state.saved = true
    return true
  }

  getState(): WorkspaceState {
    return { query: this.state.query, selectedId: this.state.selectedId, draft: this.state.draft, saved: this.state.saved }
  }
}

离开WindowDiff页面时,当前文件条目、差异块、搜索索引、选中ID和保存状态按会话归属收尾;再次进入后不会叠加旧任务。

在这里插入图片描述

WindowDiff的工作区结构

下面是WindowDiff当前使用的目录。pages负责左侧文件导航,中间双文件内容,右侧显示差异属性与统计,features保存本文展示的业务链路。

WindowDiff/
├─ entry/src/main/ets/
│  ├─ pages/Index.ets                 # 首屏与响应式布局
│  ├─ features/FeatureController.ets # 模型、动作和边界
│  ├─ entryability/EntryAbility.ets   # 页面入口与生命周期
│  └─ adapters/                       # 真机能力或数据源接口
├─ entry/src/main/resources/          # 颜色、文字和媒体资源
└─ build-profile.json5                # 工程构建配置

Index.ets只组合界面,FeatureController.ets处理文件条目、差异块、搜索索引、选中ID和保存状态。如果后面接入真实设备或网络服务,只需增加适配器并把结果转换成控制器认识的状态。

WindowDiff最终把读取并规范化文本、建立稳定ID和按窗口宽度重排连在同一条状态路径中。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐