--- description: UI组件开发模式与最佳实践 globs: ["**/pages/**/*.ets", "**/view/**/*.ets", "**/dialog/**/*.ets"] alwaysApply: false --- # UI组件开发模式与最佳实践 ## 页面组件结构 ### 基本页面模板 ```typescript import { CommonConstants } from '../common/constants/CommonConstants'; import Logger from '../common/util/Logger'; @Entry @Component struct PageName { // 状态变量 @State isLoading: boolean = false; @State dataList: Array = []; // 上下文 private context = getContext(this); // 生命周期 aboutToAppear() { this.initData(); } aboutToDisappear() { this.cleanup(); } // 初始化数据 private initData() { // 初始化逻辑 } // 清理资源 private cleanup() { // 清理逻辑 } // 构建方法 build() { Column() { // 页面内容 } .width('100%') .height('100%') .backgroundColor($r('app.color.backgroundPrimary')) } } ``` ### 导航栏组件 ```typescript @Builder NavigationArea(title: string, showBack: boolean = true) { Row() { if (showBack) { Image($r('app.media.ic_back')) .width(24) .height(24) .margin({ left: 16 }) .onClick(() => { router.back(); }) } Text(title) .fontSize(18) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.fontPrimary')) .layoutWeight(1) .textAlign(TextAlign.Center) .margin({ right: showBack ? 40 : 16 }) } .width('100%') .height(56) .backgroundColor($r('app.color.backgroundPrimary')) } ``` ## 列表组件 ### 基本列表组件 ```typescript @Component struct MusicListItem { @Prop musicItem: MusicItem; @Prop isPlaying: boolean = false; private onItemClick?: (item: MusicItem) => void; build() { Row() { Image(this.musicItem.cover || $r('app.media.default_music_icon')) .width(50) .height(50) .borderRadius(8) .objectFit(ImageFit.Cover) .margin({ right: 12 }) Column() { Text(this.musicItem.title) .fontSize(16) .fontColor($r('app.color.fontPrimary')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') Text(this.musicItem.artist) .fontSize(14) .fontColor($r('app.color.fontSecondary')) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') .margin({ top: 4 }) } .layoutWeight(1) .alignItems(HorizontalAlign.Start) if (this.isPlaying) { Image($r('app.media.ic_playing')) .width(24) .height(24) .margin({ left: 12 }) } } .width('100%') .height(70) .padding({ horizontal: 16, vertical: 10 }) .onClick(() => { if (this.onItemClick) { this.onItemClick(this.musicItem); } }) } } ``` ### 高性能列表 ```typescript @Component struct MusicList { @State musicList: MusicItem[] = []; @State currentPlayingId: string = ''; build() { List({ space: 1 }) { LazyForEach(new MusicDataSource(this.musicList), (item: MusicItem, index: number) => { ListItem() { MusicListItem({ musicItem: item, isPlaying: item.id === this.currentPlayingId, onItemClick: (musicItem: MusicItem) => { this.playMusic(musicItem); } }) } }, (item: MusicItem) => item.id) } .width('100%') .layoutWeight(1) .divider({ strokeWidth: 1, color: $r('app.color.compDivider') }) } private playMusic(musicItem: MusicItem) { this.currentPlayingId = musicItem.id; // 播放音乐逻辑 } } // 数据源类 class MusicDataSource implements IDataSource { private listeners: DataChangeListener[] = []; private data: MusicItem[] = []; constructor(data: MusicItem[]) { this.data = data; } totalCount(): number { return this.data.length; } getData(index: number): MusicItem { return this.data[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { this.listeners.splice(pos, 1); } } notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded(); }); } } ``` ## 播放控制组件 ### 播放控制栏 ```typescript @Component struct PlayerControlBar { @Prop isPlaying: boolean = false; @Prop currentTime: number = 0; @Prop duration: number = 0; private onPlayPause?: () => void; private onPrevious?: () => void; private onNext?: () => void; private onSeek?: (position: number) => void; build() { Column() { // 进度条 Row() { Text(this.formatTime(this.currentTime)) .fontSize(12) .fontColor($r('app.color.fontSecondary')) Slider({ value: this.currentTime, min: 0, max: this.duration || 1, style: SliderStyle.InSet }) .layoutWeight(1) .margin({ horizontal: 12 }) .trackColor($r('app.color.compBackgroundTertiary')) .selectedColor($r('app.color.brand')) .blockColor($r('app.color.brand')) .onChange((value: number) => { if (this.onSeek) { this.onSeek(value); } }) Text(this.formatTime(this.duration)) .fontSize(12) .fontColor($r('app.color.fontSecondary')) } .width('100%') .margin({ bottom: 20 }) // 控制按钮 Row() { Button() { Image($r('app.media.ic_previous')) .width(28) .height(28) .fillColor($r('app.color.iconPrimary')) } .type(ButtonType.Circle) .backgroundColor(Color.Transparent) .width(48) .height(48) .onClick(() => { if (this.onPrevious) { this.onPrevious(); } }) Button() { Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play')) .width(36) .height(36) .fillColor($r('app.color.iconOnPrimary')) } .type(ButtonType.Circle) .backgroundColor($r('app.color.brand')) .width(64) .height(64) .margin({ horizontal: 20 }) .onClick(() => { if (this.onPlayPause) { this.onPlayPause(); } }) Button() { Image($r('app.media.ic_next')) .width(28) .height(28) .fillColor($r('app.color.iconPrimary')) } .type(ButtonType.Circle) .backgroundColor(Color.Transparent) .width(48) .height(48) .onClick(() => { if (this.onNext) { this.onNext(); } }) } .width('100%') .justifyContent(FlexAlign.Center) } .width('100%') .padding({ horizontal: 20, vertical: 16 }) } private formatTime(time: number): string { if (isNaN(time) || time < 0) return '00:00'; const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } } ``` ## 对话框组件 ### 自定义对话框 ```typescript @Component struct CustomDialog { @Prop title: string = ''; @Prop content: string = ''; @Prop confirmText: string = '确定'; @Prop cancelText: string = '取消'; @Prop showCancel: boolean = true; private onConfirm?: () => void; private onCancel?: () => void; build() { Column() { // 标题 Text(this.title) .fontSize(18) .fontWeight(FontWeight.Medium) .fontColor($r('app.color.fontPrimary')) .margin({ top: 24, bottom: 12 }) .padding({ horizontal: 20 }) // 内容 Text(this.content) .fontSize(14) .fontColor($r('app.color.fontSecondary')) .textAlign(TextAlign.Center) .padding({ horizontal: 20 }) .margin({ bottom: 24 }) // 按钮 Row() { if (this.showCancel) { Button(this.cancelText) .fontSize(16) .fontColor($r('app.color.fontPrimary')) .backgroundColor(Color.Transparent) .layoutWeight(1) .onClick(() => { if (this.onCancel) { this.onCancel(); } }) } Button(this.confirmText) .fontSize(16) .fontColor($r('app.color.brand')) .backgroundColor(Color.Transparent) .layoutWeight(1) .onClick(() => { if (this.onConfirm) { this.onConfirm(); } }) } .width('100%') .height(48) } .backgroundColor($r('app.color.backgroundPrimary')) .borderRadius(12) .width('80%') } } ``` ## 主题适配 ### 主题感知组件 ```typescript @Component struct ThemeAwareButton { @Prop text: string = ''; @Prop type: 'primary' | 'secondary' = 'primary'; private onClick?: () => void; build() { Button(this.text) .fontSize(16) .fontColor(this.type === 'primary' ? $r('app.color.fontOnPrimary') : $r('app.color.fontPrimary')) .backgroundColor(this.type === 'primary' ? $r('app.color.brand') : $r('app.color.compBackgroundSecondary')) .borderRadius(8) .padding({ horizontal: 20, vertical: 10 }) .onClick(() => { if (this.onClick) { this.onClick(); } }) } } ``` ## 动画效果 ### 页面转场动画 ```typescript // 页面跳转带动画 router.pushUrl({ url: 'pages/DetailPage', params: { id: this.itemId } }).then(() => { // 页面跳转成功 }).catch((err: Error) => { Logger.error(`页面跳转失败: ${err.message}`); }); // 在目标页面中 @Entry @Component struct DetailPage { // 页面转场动画 pageTransition() { PageTransitionEnter({ duration: 300, curve: Curve.EaseInOut }) .slide(SlideEffect.Right) PageTransitionExit({ duration: 300, curve: Curve.EaseInOut }) .slide(SlideEffect.Left) } build() { // 页面内容 } } ``` ### 状态变化动画 ```typescript @Component struct AnimatedButton { @State isPressed: boolean = false; build() { Button('点击我') .scale({ x: this.isPressed ? 0.95 : 1, y: this.isPressed ? 0.95 : 1 }) .animation({ duration: 100, curve: Curve.EaseInOut }) .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) { this.isPressed = true; } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) { this.isPressed = false; } }) } } ``` ## 响应式布局 ### 断点适配 ```typescript @Component struct ResponsiveLayout { @State currentBreakpoint: string = 'sm'; aboutToAppear() { // 监听窗口大小变化 window.getLastWindow(getContext(this)) .then((windowClass) => { windowClass.on('windowSizeChange', (windowSize) => { this.updateBreakpoint(windowSize.width); }); this.updateBreakpoint(windowClass.getWindowProperties().windowRect.width); }); } private updateBreakpoint(width: number) { if (width < 600) { this.currentBreakpoint = 'sm'; } else if (width < 840) { this.currentBreakpoint = 'md'; } else { this.currentBreakpoint = 'lg'; } } build() { if (this.currentBreakpoint === 'sm') { // 小屏幕布局 this.buildSmallLayout(); } else if (this.currentBreakpoint === 'md') { // 中等屏幕布局 this.buildMediumLayout(); } else { // 大屏幕布局 this.buildLargeLayout(); } } @Builder buildSmallLayout() { Column() { // 小屏幕布局内容 } } @Builder buildMediumLayout() { Row() { // 中等屏幕布局内容 } } @Builder buildLargeLayout() { Grid() { // 大屏幕布局内容 } } } ```