鸿蒙ArkTS项目实战 - 门店陈列巡检台:完整代码与运行效果
·



一、完整源码
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' }); })
四、运行核对
- 完整源码里最重要的是
PatrolWaterfallAlgorithm与StorePatrolPage的配合关系。 - 路由与首页入口更新后,要实际点开页面验证三列和间距按钮是否生效。
更多推荐




所有评论(0)