ui-components.mdc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. ---
  2. description: UI组件开发模式与最佳实践
  3. globs: ["**/pages/**/*.ets", "**/view/**/*.ets", "**/dialog/**/*.ets"]
  4. alwaysApply: false
  5. ---
  6. # UI组件开发模式与最佳实践
  7. ## 页面组件结构
  8. ### 基本页面模板
  9. ```typescript
  10. import { CommonConstants } from '../common/constants/CommonConstants';
  11. import Logger from '../common/util/Logger';
  12. @Entry
  13. @Component
  14. struct PageName {
  15. // 状态变量
  16. @State isLoading: boolean = false;
  17. @State dataList: Array<ItemType> = [];
  18. // 上下文
  19. private context = getContext(this);
  20. // 生命周期
  21. aboutToAppear() {
  22. this.initData();
  23. }
  24. aboutToDisappear() {
  25. this.cleanup();
  26. }
  27. // 初始化数据
  28. private initData() {
  29. // 初始化逻辑
  30. }
  31. // 清理资源
  32. private cleanup() {
  33. // 清理逻辑
  34. }
  35. // 构建方法
  36. build() {
  37. Column() {
  38. // 页面内容
  39. }
  40. .width('100%')
  41. .height('100%')
  42. .backgroundColor($r('app.color.backgroundPrimary'))
  43. }
  44. }
  45. ```
  46. ### 导航栏组件
  47. ```typescript
  48. @Builder
  49. NavigationArea(title: string, showBack: boolean = true) {
  50. Row() {
  51. if (showBack) {
  52. Image($r('app.media.ic_back'))
  53. .width(24)
  54. .height(24)
  55. .margin({ left: 16 })
  56. .onClick(() => {
  57. router.back();
  58. })
  59. }
  60. Text(title)
  61. .fontSize(18)
  62. .fontWeight(FontWeight.Medium)
  63. .fontColor($r('app.color.fontPrimary'))
  64. .layoutWeight(1)
  65. .textAlign(TextAlign.Center)
  66. .margin({ right: showBack ? 40 : 16 })
  67. }
  68. .width('100%')
  69. .height(56)
  70. .backgroundColor($r('app.color.backgroundPrimary'))
  71. }
  72. ```
  73. ## 列表组件
  74. ### 基本列表组件
  75. ```typescript
  76. @Component
  77. struct MusicListItem {
  78. @Prop musicItem: MusicItem;
  79. @Prop isPlaying: boolean = false;
  80. private onItemClick?: (item: MusicItem) => void;
  81. build() {
  82. Row() {
  83. Image(this.musicItem.cover || $r('app.media.default_music_icon'))
  84. .width(50)
  85. .height(50)
  86. .borderRadius(8)
  87. .objectFit(ImageFit.Cover)
  88. .margin({ right: 12 })
  89. Column() {
  90. Text(this.musicItem.title)
  91. .fontSize(16)
  92. .fontColor($r('app.color.fontPrimary'))
  93. .maxLines(1)
  94. .textOverflow({ overflow: TextOverflow.Ellipsis })
  95. .width('100%')
  96. Text(this.musicItem.artist)
  97. .fontSize(14)
  98. .fontColor($r('app.color.fontSecondary'))
  99. .maxLines(1)
  100. .textOverflow({ overflow: TextOverflow.Ellipsis })
  101. .width('100%')
  102. .margin({ top: 4 })
  103. }
  104. .layoutWeight(1)
  105. .alignItems(HorizontalAlign.Start)
  106. if (this.isPlaying) {
  107. Image($r('app.media.ic_playing'))
  108. .width(24)
  109. .height(24)
  110. .margin({ left: 12 })
  111. }
  112. }
  113. .width('100%')
  114. .height(70)
  115. .padding({ horizontal: 16, vertical: 10 })
  116. .onClick(() => {
  117. if (this.onItemClick) {
  118. this.onItemClick(this.musicItem);
  119. }
  120. })
  121. }
  122. }
  123. ```
  124. ### 高性能列表
  125. ```typescript
  126. @Component
  127. struct MusicList {
  128. @State musicList: MusicItem[] = [];
  129. @State currentPlayingId: string = '';
  130. build() {
  131. List({ space: 1 }) {
  132. LazyForEach(new MusicDataSource(this.musicList), (item: MusicItem, index: number) => {
  133. ListItem() {
  134. MusicListItem({
  135. musicItem: item,
  136. isPlaying: item.id === this.currentPlayingId,
  137. onItemClick: (musicItem: MusicItem) => {
  138. this.playMusic(musicItem);
  139. }
  140. })
  141. }
  142. }, (item: MusicItem) => item.id)
  143. }
  144. .width('100%')
  145. .layoutWeight(1)
  146. .divider({ strokeWidth: 1, color: $r('app.color.compDivider') })
  147. }
  148. private playMusic(musicItem: MusicItem) {
  149. this.currentPlayingId = musicItem.id;
  150. // 播放音乐逻辑
  151. }
  152. }
  153. // 数据源类
  154. class MusicDataSource implements IDataSource {
  155. private listeners: DataChangeListener[] = [];
  156. private data: MusicItem[] = [];
  157. constructor(data: MusicItem[]) {
  158. this.data = data;
  159. }
  160. totalCount(): number {
  161. return this.data.length;
  162. }
  163. getData(index: number): MusicItem {
  164. return this.data[index];
  165. }
  166. registerDataChangeListener(listener: DataChangeListener): void {
  167. if (this.listeners.indexOf(listener) < 0) {
  168. this.listeners.push(listener);
  169. }
  170. }
  171. unregisterDataChangeListener(listener: DataChangeListener): void {
  172. const pos = this.listeners.indexOf(listener);
  173. if (pos >= 0) {
  174. this.listeners.splice(pos, 1);
  175. }
  176. }
  177. notifyDataReload(): void {
  178. this.listeners.forEach(listener => {
  179. listener.onDataReloaded();
  180. });
  181. }
  182. }
  183. ```
  184. ## 播放控制组件
  185. ### 播放控制栏
  186. ```typescript
  187. @Component
  188. struct PlayerControlBar {
  189. @Prop isPlaying: boolean = false;
  190. @Prop currentTime: number = 0;
  191. @Prop duration: number = 0;
  192. private onPlayPause?: () => void;
  193. private onPrevious?: () => void;
  194. private onNext?: () => void;
  195. private onSeek?: (position: number) => void;
  196. build() {
  197. Column() {
  198. // 进度条
  199. Row() {
  200. Text(this.formatTime(this.currentTime))
  201. .fontSize(12)
  202. .fontColor($r('app.color.fontSecondary'))
  203. Slider({
  204. value: this.currentTime,
  205. min: 0,
  206. max: this.duration || 1,
  207. style: SliderStyle.InSet
  208. })
  209. .layoutWeight(1)
  210. .margin({ horizontal: 12 })
  211. .trackColor($r('app.color.compBackgroundTertiary'))
  212. .selectedColor($r('app.color.brand'))
  213. .blockColor($r('app.color.brand'))
  214. .onChange((value: number) => {
  215. if (this.onSeek) {
  216. this.onSeek(value);
  217. }
  218. })
  219. Text(this.formatTime(this.duration))
  220. .fontSize(12)
  221. .fontColor($r('app.color.fontSecondary'))
  222. }
  223. .width('100%')
  224. .margin({ bottom: 20 })
  225. // 控制按钮
  226. Row() {
  227. Button() {
  228. Image($r('app.media.ic_previous'))
  229. .width(28)
  230. .height(28)
  231. .fillColor($r('app.color.iconPrimary'))
  232. }
  233. .type(ButtonType.Circle)
  234. .backgroundColor(Color.Transparent)
  235. .width(48)
  236. .height(48)
  237. .onClick(() => {
  238. if (this.onPrevious) {
  239. this.onPrevious();
  240. }
  241. })
  242. Button() {
  243. Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
  244. .width(36)
  245. .height(36)
  246. .fillColor($r('app.color.iconOnPrimary'))
  247. }
  248. .type(ButtonType.Circle)
  249. .backgroundColor($r('app.color.brand'))
  250. .width(64)
  251. .height(64)
  252. .margin({ horizontal: 20 })
  253. .onClick(() => {
  254. if (this.onPlayPause) {
  255. this.onPlayPause();
  256. }
  257. })
  258. Button() {
  259. Image($r('app.media.ic_next'))
  260. .width(28)
  261. .height(28)
  262. .fillColor($r('app.color.iconPrimary'))
  263. }
  264. .type(ButtonType.Circle)
  265. .backgroundColor(Color.Transparent)
  266. .width(48)
  267. .height(48)
  268. .onClick(() => {
  269. if (this.onNext) {
  270. this.onNext();
  271. }
  272. })
  273. }
  274. .width('100%')
  275. .justifyContent(FlexAlign.Center)
  276. }
  277. .width('100%')
  278. .padding({ horizontal: 20, vertical: 16 })
  279. }
  280. private formatTime(time: number): string {
  281. if (isNaN(time) || time < 0) return '00:00';
  282. const minutes = Math.floor(time / 60);
  283. const seconds = Math.floor(time % 60);
  284. return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  285. }
  286. }
  287. ```
  288. ## 对话框组件
  289. ### 自定义对话框
  290. ```typescript
  291. @Component
  292. struct CustomDialog {
  293. @Prop title: string = '';
  294. @Prop content: string = '';
  295. @Prop confirmText: string = '确定';
  296. @Prop cancelText: string = '取消';
  297. @Prop showCancel: boolean = true;
  298. private onConfirm?: () => void;
  299. private onCancel?: () => void;
  300. build() {
  301. Column() {
  302. // 标题
  303. Text(this.title)
  304. .fontSize(18)
  305. .fontWeight(FontWeight.Medium)
  306. .fontColor($r('app.color.fontPrimary'))
  307. .margin({ top: 24, bottom: 12 })
  308. .padding({ horizontal: 20 })
  309. // 内容
  310. Text(this.content)
  311. .fontSize(14)
  312. .fontColor($r('app.color.fontSecondary'))
  313. .textAlign(TextAlign.Center)
  314. .padding({ horizontal: 20 })
  315. .margin({ bottom: 24 })
  316. // 按钮
  317. Row() {
  318. if (this.showCancel) {
  319. Button(this.cancelText)
  320. .fontSize(16)
  321. .fontColor($r('app.color.fontPrimary'))
  322. .backgroundColor(Color.Transparent)
  323. .layoutWeight(1)
  324. .onClick(() => {
  325. if (this.onCancel) {
  326. this.onCancel();
  327. }
  328. })
  329. }
  330. Button(this.confirmText)
  331. .fontSize(16)
  332. .fontColor($r('app.color.brand'))
  333. .backgroundColor(Color.Transparent)
  334. .layoutWeight(1)
  335. .onClick(() => {
  336. if (this.onConfirm) {
  337. this.onConfirm();
  338. }
  339. })
  340. }
  341. .width('100%')
  342. .height(48)
  343. }
  344. .backgroundColor($r('app.color.backgroundPrimary'))
  345. .borderRadius(12)
  346. .width('80%')
  347. }
  348. }
  349. ```
  350. ## 主题适配
  351. ### 主题感知组件
  352. ```typescript
  353. @Component
  354. struct ThemeAwareButton {
  355. @Prop text: string = '';
  356. @Prop type: 'primary' | 'secondary' = 'primary';
  357. private onClick?: () => void;
  358. build() {
  359. Button(this.text)
  360. .fontSize(16)
  361. .fontColor(this.type === 'primary' ?
  362. $r('app.color.fontOnPrimary') :
  363. $r('app.color.fontPrimary'))
  364. .backgroundColor(this.type === 'primary' ?
  365. $r('app.color.brand') :
  366. $r('app.color.compBackgroundSecondary'))
  367. .borderRadius(8)
  368. .padding({ horizontal: 20, vertical: 10 })
  369. .onClick(() => {
  370. if (this.onClick) {
  371. this.onClick();
  372. }
  373. })
  374. }
  375. }
  376. ```
  377. ## 动画效果
  378. ### 页面转场动画
  379. ```typescript
  380. // 页面跳转带动画
  381. router.pushUrl({
  382. url: 'pages/DetailPage',
  383. params: { id: this.itemId }
  384. }).then(() => {
  385. // 页面跳转成功
  386. }).catch((err: Error) => {
  387. Logger.error(`页面跳转失败: ${err.message}`);
  388. });
  389. // 在目标页面中
  390. @Entry
  391. @Component
  392. struct DetailPage {
  393. // 页面转场动画
  394. pageTransition() {
  395. PageTransitionEnter({ duration: 300, curve: Curve.EaseInOut })
  396. .slide(SlideEffect.Right)
  397. PageTransitionExit({ duration: 300, curve: Curve.EaseInOut })
  398. .slide(SlideEffect.Left)
  399. }
  400. build() {
  401. // 页面内容
  402. }
  403. }
  404. ```
  405. ### 状态变化动画
  406. ```typescript
  407. @Component
  408. struct AnimatedButton {
  409. @State isPressed: boolean = false;
  410. build() {
  411. Button('点击我')
  412. .scale({ x: this.isPressed ? 0.95 : 1, y: this.isPressed ? 0.95 : 1 })
  413. .animation({
  414. duration: 100,
  415. curve: Curve.EaseInOut
  416. })
  417. .onTouch((event: TouchEvent) => {
  418. if (event.type === TouchType.Down) {
  419. this.isPressed = true;
  420. } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
  421. this.isPressed = false;
  422. }
  423. })
  424. }
  425. }
  426. ```
  427. ## 响应式布局
  428. ### 断点适配
  429. ```typescript
  430. @Component
  431. struct ResponsiveLayout {
  432. @State currentBreakpoint: string = 'sm';
  433. aboutToAppear() {
  434. // 监听窗口大小变化
  435. window.getLastWindow(getContext(this))
  436. .then((windowClass) => {
  437. windowClass.on('windowSizeChange', (windowSize) => {
  438. this.updateBreakpoint(windowSize.width);
  439. });
  440. this.updateBreakpoint(windowClass.getWindowProperties().windowRect.width);
  441. });
  442. }
  443. private updateBreakpoint(width: number) {
  444. if (width < 600) {
  445. this.currentBreakpoint = 'sm';
  446. } else if (width < 840) {
  447. this.currentBreakpoint = 'md';
  448. } else {
  449. this.currentBreakpoint = 'lg';
  450. }
  451. }
  452. build() {
  453. if (this.currentBreakpoint === 'sm') {
  454. // 小屏幕布局
  455. this.buildSmallLayout();
  456. } else if (this.currentBreakpoint === 'md') {
  457. // 中等屏幕布局
  458. this.buildMediumLayout();
  459. } else {
  460. // 大屏幕布局
  461. this.buildLargeLayout();
  462. }
  463. }
  464. @Builder
  465. buildSmallLayout() {
  466. Column() {
  467. // 小屏幕布局内容
  468. }
  469. }
  470. @Builder
  471. buildMediumLayout() {
  472. Row() {
  473. // 中等屏幕布局内容
  474. }
  475. }
  476. @Builder
  477. buildLargeLayout() {
  478. Grid() {
  479. // 大屏幕布局内容
  480. }
  481. }
  482. }
  483. ```