在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

一、完整源码

import {
  CustomLayoutAlgorithm,
  DynamicLayout,
  DynamicLayoutAttribute,
  FrameNode,
  LayoutConstraint,
  Position,
  curves
} from '@kit.ArkUI';

@ObservedV2
class PatrolWaterfallAlgorithm extends CustomLayoutAlgorithm {
  @Trace columnCount: number = 2;
  @Trace rowGap: number = 12;
  @Trace columnGap: number = 12;

  onMeasure(self: FrameNode, constraint: LayoutConstraint): void {
    const availableWidth = constraint.maxSize.width;
    const childCount = self.getChildrenCount();
    if (childCount <= 0) {
      self.setMeasuredSize({ width: availableWidth, height: 0 });
      return;
    }
    const itemWidth = (availableWidth - this.columnGap * (this.columnCount - 1)) / this.columnCount;
    const columns: number[] = new Array(this.columnCount).fill(0);
    for (let index = 0; index < childCount; index++) {
      const child = self.getChild(index);
      if (!child) {
        continue;
      }
      child.measure({
        maxSize: { width: itemWidth, height: constraint.maxSize.height },
        minSize: { width: itemWidth, height: 0 },
        percentReference: constraint.percentReference
      });
      const targetColumn = this.findShortestColumn(columns);
      columns[targetColumn] += child.getMeasuredSize().height + this.rowGap;
    }
    const tallest = Math.max(...columns) - this.rowGap;
    self.setMeasuredSize({ width: availableWidth, height: Math.max(tallest, 0) });
  }

  onLayout(self: FrameNode, _: Position): void {
    const width = self.getMeasuredSize().width;
    const itemWidth = (width - this.columnGap * (this.columnCount - 1)) / this.columnCount;
    const topOffsets: number[] = new Array(this.columnCount).fill(0);
    for (let index = 0; index < self.getChildrenCount(); index++) {
      const child = self.getChild(index);
      if (!child) {
        continue;
      }
      const targetColumn = this.findShortestColumn(topOffsets);
      child.layout({
        x: targetColumn * (itemWidth + this.columnGap),
        y: topOffsets[targetColumn]
      });
      topOffsets[targetColumn] += child.getMeasuredSize().height + this.rowGap;
    }
  }

  private findShortestColumn(columns: number[]): number {
    let target = 0;
    let min = columns[0];
    for (let index = 1; index < columns.length; index++) {
      if (columns[index] < min) {
        min = columns[index];
        target = index;
      }
    }
    return target;
  }
}

@ComponentV2
struct PatrolEntranceCard {
  @Local score: number = 88;
  @Local note: string = '入口立牌和动线标识已经完成第一次核对。';

  build() {
    Column({ space: 10 }) {
      Text('入口动线').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#0F172A')
      Text('核对门头、迎宾立牌、地贴和临停提示。')
        .fontSize(12).fontColor('#64748B').lineHeight(18)
      Row() {
        Text('完成度').fontSize(12).fontColor('#64748B')
        Blank()
        Text(`${Math.round(this.score)}`).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#DC2626')
      }.width('100%')
      Slider({ value: this.score, min: 0, max: 100, step: 1 })
        .selectedColor('#EF4444')
        .trackColor('#FEE2E2')
        .blockColor('#DC2626')
        .onChange((value: number) => this.score = value)
      TextArea({ text: this.note, placeholder: '入口说明' })
        .height(94)
        .backgroundColor('#F8FAFC')
        .borderRadius(10)
        .onChange((value: string) => this.note = value)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
  }
}

@ComponentV2
struct PatrolWindowCard {
  @Local mainColor: string = '秋季橙';
  @Local ready: boolean = true;
  @Local remark: string = '主橱窗需要补一条价格说明卡。';

  build() {
    Column({ space: 10 }) {
      Text('主橱窗').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#0F172A')
      TextInput({ text: this.mainColor, placeholder: '主题配色' })
        .backgroundColor('#F8FAFC')
        .borderRadius(10)
        .onChange((value: string) => this.mainColor = value)
      Row() {
        Text('灯箱状态').fontSize(12).fontColor('#64748B')
        Blank()
        Toggle({ type: ToggleType.Switch, isOn: this.ready })
          .selectedColor('#2563EB')
          .onChange((value: boolean) => this.ready = value)
      }.width('100%')
      Text(this.ready ? '主橱窗灯光和背板都已正常点亮。' : '橱窗仍需补测电源与背景灯带。')
        .fontSize(12).fontColor(this.ready ? '#2563EB' : '#B45309').lineHeight(18)
      TextArea({ text: this.remark, placeholder: '补充说明' })
        .height(78)
        .backgroundColor('#F8FAFC')
        .borderRadius(10)
        .onChange((value: string) => this.remark = value)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
  }
}

@ComponentV2
struct PatrolShelfCard {
  @Local gapCount: number = 4;
  @Local note: string = '饮品区第三层与饰品区右侧还有陈列空档。';

  build() {
    Column({ space: 10 }) {
      Text('货架空档').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#0F172A')
      Text('优先处理正对主通道的空档,避免断层观感。')
        .fontSize(12).fontColor('#64748B').lineHeight(18)
      Row() {
        Text('空档数量').fontSize(12).fontColor('#64748B')
        Blank()
        Text(`${Math.round(this.gapCount)}`).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#7C3AED')
      }.width('100%')
      Slider({ value: this.gapCount, min: 0, max: 12, step: 1 })
        .selectedColor('#8B5CF6')
        .trackColor('#F3E8FF')
        .blockColor('#7C3AED')
        .onChange((value: number) => this.gapCount = value)
      TextArea({ text: this.note, placeholder: '缺口分布' })
        .height(104)
        .backgroundColor('#F8FAFC')
        .borderRadius(10)
        .onChange((value: string) => this.note = value)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
  }
}

@ComponentV2
struct PatrolCashierCard {
  @Local queueReady: boolean = false;
  @Local counterOwner: string = '陈值台';
  @Local note: string = '收银口边台需增加一盒小票纸和一块湿巾。';

  build() {
    Column({ space: 10 }) {
      Text('收银区').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#0F172A')
      Row() {
        Text('排队栏').fontSize(12).fontColor('#64748B')
        Blank()
        Toggle({ type: ToggleType.Switch, isOn: this.queueReady })
          .selectedColor('#0F766E')
          .onChange((value: boolean) => this.queueReady = value)
      }.width('100%')
      TextInput({ text: this.counterOwner, placeholder: '值台负责人' })
        .backgroundColor('#F8FAFC')
        .borderRadius(10)
        .onChange((value: string) => this.counterOwner = value)
      TextArea({ text: this.note, placeholder: '口岸说明' })
        .height(86)
        .backgroundColor('#F8FAFC')
        .borderRadius(10)
        .onChange((value: string) => this.note = value)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
  }
}

@ComponentV2
struct PatrolPromoCard {
  @Local badgeCount: number = 24;
  @Local fastFix: string = '第二活动台价签方向不一致,需要统一右上角朝向。';

  build() {
    Column({ space: 10 }) {
      Text('促销台').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#0F172A')
      Text('重点检查标签、样卡和赠品说明是否同版。')
        .fontSize(12).fontColor('#64748B').lineHeight(18)
      Row() {
        Text('价签余量').fontSize(12).fontColor('#64748B')
        Blank()
        Text(`${Math.round(this.badgeCount)}`).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#F97316')
      }.width('100%')
      Slider({ value: this.badgeCount, min: 0, max: 60, step: 1 })
        .selectedColor('#F97316')
        .trackColor('#FFEDD5')
        .blockColor('#EA580C')
        .onChange((value: number) => this.badgeCount = value)
      TextArea({ text: this.fastFix, placeholder: '当场处理项' })
        .height(98)
        .backgroundColor('#F8FAFC')
        .borderRadius(10)
        .onChange((value: string) => this.fastFix = value)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
  }
}

@ComponentV2
struct PatrolStorageCard {
  @Local backroomReady: boolean = true;
  @Local count: number = 12;
  @Local note: string = '后仓临时补货位已留出两层,今晚可直接入库。';

  build() {
    Column({ space: 10 }) {
      Text('后仓补货').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#0F172A')
      Row() {
        Text('临时货位').fontSize(12).fontColor('#64748B')
        Blank()
        Toggle({ type: ToggleType.Switch, isOn: this.backroomReady })
          .selectedColor('#16A34A')
          .onChange((value: boolean) => this.backroomReady = value)
      }.width('100%')
      Row() {
        Text('待补货箱数').fontSize(12).fontColor('#64748B')
        Blank()
        Text(`${Math.round(this.count)}`).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#16A34A')
      }.width('100%')
      Slider({ value: this.count, min: 0, max: 30, step: 1 })
        .selectedColor('#22C55E')
        .trackColor('#DCFCE7')
        .blockColor('#16A34A')
        .onChange((value: number) => this.count = value)
      TextArea({ text: this.note, placeholder: '补货说明' })
        .height(74)
        .backgroundColor('#F8FAFC')
        .borderRadius(10)
        .onChange((value: string) => this.note = value)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(18)
  }
}

@Entry
@ComponentV2
struct StorePatrolPage {
  @Local layoutAlgorithm: PatrolWaterfallAlgorithm = new PatrolWaterfallAlgorithm();

  private switchColumns(count: number): void {
    this.getUIContext()?.animateTo({ curve: curves.springMotion(0.42, 0.86) }, () => {
      this.layoutAlgorithm.columnCount = count;
    });
  }

  private switchGap(rowGap: number, columnGap: number): void {
    this.getUIContext()?.animateTo({ curve: curves.springMotion(0.42, 0.86) }, () => {
      this.layoutAlgorithm.rowGap = rowGap;
      this.layoutAlgorithm.columnGap = columnGap;
    });
  }

  build() {
    Column() {
      Column({ space: 4 }) {
        Text('🧭 59 门店陈列巡检台').fontSize(22).fontWeight(FontWeight.Bold)
        Text('使用 API 24 的自定义 DynamicLayout 算法,把不同高度的巡检卡片排成瀑布流。')
          .fontSize(12).fontColor('#64748B').lineHeight(18)
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 16, right: 16, top: 14, bottom: 10 })

      Row({ space: 8 }) {
        Button('双列').height(36).backgroundColor(this.layoutAlgorithm.columnCount === 2 ? '#111827' : '#E5E7EB')
          .fontColor(this.layoutAlgorithm.columnCount === 2 ? '#FFFFFF' : '#334155')
          .onClick(() => this.switchColumns(2))
        Button('三列').height(36).backgroundColor(this.layoutAlgorithm.columnCount === 3 ? '#111827' : '#E5E7EB')
          .fontColor(this.layoutAlgorithm.columnCount === 3 ? '#FFFFFF' : '#334155')
          .onClick(() => this.switchColumns(3))
        Button('紧凑间距').height(36).backgroundColor('#FEE2E2').fontColor('#991B1B')
          .onClick(() => this.switchGap(8, 8))
        Button('舒展间距').height(36).backgroundColor('#DBEAFE').fontColor('#1D4ED8')
          .onClick(() => this.switchGap(14, 14))
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 10 })

      Scroll() {
        DynamicLayout(this.layoutAlgorithm) {
          PatrolEntranceCard()
          PatrolWindowCard()
          PatrolShelfCard()
          PatrolCashierCard()
          PatrolPromoCard()
          PatrolStorageCard()
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 24 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F7F8FA')
  }
}

二、路由接入

"pages/api24_series/StorePatrolPage",

三、首页入口

Button('🧭 59 门店陈列巡检台') .fontSize(14) .width('80%') .onClick(() => { this.getUIContext().getRouter().pushUrl({ url: 'pages/api24_series/StorePatrolPage' }); })

四、运行核对

  1. 完整源码里最重要的是 PatrolWaterfallAlgorithmStorePatrolPage 的配合关系。
  2. 路由与首页入口更新后,要实际点开页面验证三列和间距按钮是否生效。
Logo

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

更多推荐