面试宝典APP-首页模块

首页模块

1. 静态结构

1)轮播图

1Row() {
2  Swiper() {
3    Flex() {
4      Image('/common/images/banner_ai.png')
5        .objectFit(ImageFit.Fill)
6    }
7    .padding({ left: vp2vp(15), right: vp2vp(15) })
8
9    Flex() {
10      Image('/common/images/banner_pj.png')
11        .objectFit(ImageFit.Fill)
12    }
13    .padding({ left: vp2vp(15), right: vp2vp(15) })
14
15    Flex() {
16      Image('/common/images/banner_qa.png')
17        .objectFit(ImageFit.Fill)
18    }
19    .padding({ left: vp2vp(15), right: vp2vp(15) })
20  }
21  .autoPlay(true)
22  .indicator(false)
23}
24.aspectRatio(2.8)

2)骨架效果

1@Builder
2  SkeletonBuilder() {
3    Column() {
4      Row({ space: vp2vp(15) }) {
5        IvSkeleton()
6        IvSkeleton({ widthValue: vp2vp(60) })
7        IvSkeleton({ widthValue: vp2vp(80) })
8      }
9      .width('100%')
10      .margin({ bottom: vp2vp(15) })
11
12      List({ space: vp2vp(15) }){
13        ForEach([1, 2, 3, 4, 5, 6, 7], () => {
14          ListItem(){
15            Column({ space: vp2vp(15) }){
16              Row({ space: vp2vp(15) }){
17                IvSkeleton({ widthValue: vp2vp(30) })
18                IvSkeleton({ widthValue: '100%' }).layoutWeight(1)
19              }
20              Row({ space: vp2vp(15) }){
21                IvSkeleton({ widthValue: vp2vp(50) })
22                IvSkeleton({ widthValue: vp2vp(50) })
23                IvSkeleton({ widthValue: vp2vp(50) })
24              }
25              .width('100%')
26            }
27            .padding({ top: vp2vp(10), bottom: vp2vp(10) })
28          }
29        })
30      }
31      .divider({ strokeWidth: 0.5, color: $r('app.color.gray_bg') })
32    }
33    .width('100%')
34    .padding(vp2vp(15))
35    .layoutWeight(1)
36  }
1@State
2  loading: boolean = false
1if (this.loading) {
2    this.SkeletonBuilder()
3  } else {
4    // Tabs
5  }

2. 分类 Tabs 动态展示

1)数据类型

models/QuestionTypeModel.ets
1export class TagModel {
2  tagName: string
3  nameColor: string
4  borderColor: string
5}
6
7export class QuestionTypeModel {
8  public id: number
9  public name: string
10  public icon?: string
11  public describeInfo?: string
12  public displayNewestFlag?: 0 | 1
13  public tags?: TagModel[]
14}

3) 获取数据

1@State
2  questionTypeList: QuestionTypeModel[] = []
3  @State
4  activeIndex: number = 0
5
6  aboutToAppear() {
7    this.loading = true
8    Request.get<QuestionTypeModel[]>('question/type')
9      .then(res => {
10        this.questionTypeList = res.data
11      })
12      .finally(() => {
13        this.loading = false
14      })
15  }

4)渲染 tabs

1if (this.loading) {
2        this.SkeletonBuilder()
3      } else {
4        Tabs() {
5          ForEach(this.questionTypeList, (item: QuestionTypeModel, index) => {
6            TabContent() {
7              // 列表组件
8            }
9            .tabBar(this.TabItemBuilder(item, index))
10            .height('100%')
11          })
12        }
13        .layoutWeight(1)
14        .barMode(BarMode.Scrollable)
15        .onChange(i => this.activeIndex = i)
16      }

5)自定义构建函数

1@Builder
2  TabItemBuilder(q: QuestionTypeModel, index: number) {
3    Row() {
4      Stack({ alignContent: Alignment.Bottom }) {
5        Text(q.name)
6          .fontSize(vp2vp(15))
7          .height(vp2vp(44))
8          .fontColor(this.activeIndex === index ? $r('app.color.black') : $r('app.color.gray'))
9        Text()
10          .width(this.activeIndex === index ? vp2vp(20) : 0)
11          .height(vp2vp(2))
12          .backgroundColor($r('app.color.black'))
13          .animation({ duration: this.activeIndex === index ? 300 : 0 })
14      }
15      .padding({ left: vp2vp(index === 0 ? 15 : 0) })
16
17      if (q.displayNewestFlag === 1) {
18        Image($r('app.media.new'))
19          .width(vp2vp(34))
20          .height(vp2vp(14))
21          .padding({ left: vp2vp(5) })
22      }
23    }
24    .padding({ right: vp2vp(15) })
25  }

3. 试题列表组件抽取

1)标签组件

common/components/IvTag.ets
1import { vp2vp } from '../utils/Basic'
2
3@Component
4export struct IvTag {
5  text: string
6  difficulty: number = 1
7
8  getTag() {
9    if (this.difficulty === 3 || this.difficulty === 4) {
10      return { text: '一般', color: $r('app.color.blue') }
11    } else if (this.difficulty === 5) {
12      return { text: '困难', color: $r('app.color.orange') }
13    }
14    return { text: this.text ? this.text : '简单', color: $r('app.color.green') }
15  }
16
17  build() {
18    Text(this.getTag().text)
19      .fontColor(this.getTag().color)
20      .fontSize(vp2vp(10))
21      .width(vp2vp(34))
22      .height(vp2vp(18))
23      .backgroundColor($r('app.color.gray_bg'))
24      .borderRadius(vp2vp(2))
25      .textAlign(TextAlign.Center)
26      .margin({ right: vp2vp(7) })
27  }
28}

2)试题项组件

common/components/IvQuestionItem.ets
1import { Auth } from '../utils/Auth'
2import { vp2vp } from '../utils/Basic'
3import { IvTag } from './IvTag'
4
5@Component
6export struct IvQuestionItem {
7  @Builder
8  SplitBuilder() {
9    Text('|')
10      .width(vp2vp(30))
11      .textAlign(TextAlign.Center)
12      .fontColor('#C3C3C5')
13      .fontSize(vp2vp(12))
14  }
15
16  @Builder
17  TextBuilder(text: string) {
18    Text(text)
19      .fontColor('#C3C3C5')
20      .fontSize(vp2vp(12))
21  }
22
23  build() {
24    Column() {
25      Row() {
26        IvTag({ difficulty: 4 })
27        Text('闭包你是怎么理解的?')
28          .fontSize(vp2vp(15))
29          .layoutWeight(1)
30          .textOverflow({ overflow: TextOverflow.Ellipsis })
31          .maxLines(1)
32      }.width('100%')
33
34      Row() {
35        this.TextBuilder(`点赞 100`)
36        this.SplitBuilder()
37        this.TextBuilder(`浏览 200`)
38        this.SplitBuilder()
39        this.TextBuilder(`已看过`)
40      }
41      .width('100%')
42      .margin({ top: vp2vp(10) })
43    }
44    .height(vp2vp(80))
45    .justifyContent(FlexAlign.Center)
46    .width('100%')
47    .onClick(() => {
48      Auth.pushUrl({
49        url: 'pages/QuestionDetail',
50      })
51    })
52  }
53}

3)试题列表组件

common/components/IvQuestionList.ets
1import { vp2vp } from '../utils/Basic'
2import { IvQuestionItem } from './IvQuestionItem'
3
4@Component
5export struct IvQuestionList {
6  build() {
7    List() {
8      ForEach([1, 2, 3, 4, 5, 6, 7, 8], () => {
9        ListItem() {
10          IvQuestionItem()
11        }
12      })
13    }
14    .divider({
15      strokeWidth: 0.5,
16      color: $r('app.color.gray_bg')
17    })
18    .padding({ left: vp2vp(15), right: vp2vp(15) })
19    .height('100%')
20    .width('100%')
21  }
22}

4)首页使用组件

views/Index/Home.ets
1ForEach(this.questionTypeList, (item: QuestionTypeModel, index) => {
2            TabContent() {
3              // 列表组件
4              IvQuestionList()
5            }
6            .tabBar(this.TabItemBuilder(item, index))
7            .height('100%')
8          })

4. 试题列表加载

1)请求参数类型

models/QuestionItemModel.ets
1export class QueryQuestionListParams {
2  type: number
3  page: number
4  questionBankType: number
5  sort?: number
6  keyword?: string
7}
8
9@Observed
10export class QuestionItemModel {
11  public id: string
12  public stem: string
13  public difficulty: number
14  public likeCount: number
15  public views: number
16  public readFlag?: 0 | 1
17}

2)试题列表组件

common/components/IvQuestionList.ets
1import promptAction from '@ohos.promptAction'
2import { QueryQuestionListParams, QuestionItemModel } from '../../models/QuestionItemModel'
3import { vp2vp } from '../utils/Basic'
4import { Request } from '../utils/Request'
5import { IvQuestionItem } from './IvQuestionItem'
6
7@Component
8export struct IvQuestionList {
9  @Prop
10  questionType: number
11  @Prop
12  @Watch('initQuestionList')
13  activeIndex: number
14  @Prop
15  selfIndex: number
16
17  params: QueryQuestionListParams = {
18    questionBankType: 10,
19    type: this.questionType,
20    page: 1,
21    sort: 0,
22  }
23  @State
24  questionList: QuestionItemModel[] = []
25
26  aboutToAppear() {
27    this.initQuestionList()
28  }
29
30  initQuestionList () {
31    if (this.activeIndex === this.selfIndex && this.questionList.length === 0) {
32      this.getQuestionList(this.params)
33    }
34  }
35
36  getQuestionList() {
37    return Request.get<{
38      total: number,
39      pageTotal: number,
40      rows: QuestionItemModel[]
41    }>('question/list', this.params)
42      .then(res => {
43        this.questionList = res.data.rows
44      })
45      .catch(e => {
46        promptAction.showToast({ message: JSON.stringify(e) })
47      })
48  }
49
50  build() {
51    List() {
52      ForEach(this.questionList, (item: QuestionItemModel) => {
53        ListItem() {
54          IvQuestionItem({ item })
55        }
56      },
57        ({id, likeCount, readFlag, views}: QuestionItemModel) => {
58          return JSON.stringify({ id, likeCount, readFlag, views })
59        }
60      )
61    }
62    .divider({
63      strokeWidth: 0.5,
64      color: $r('app.color.gray_bg')
65    })
66    .padding({ left: vp2vp(15), right: vp2vp(15) })
67    .height('100%')
68    .width('100%')
69  }
70}

3)试题选项组件

common/components/IvQuestionList.ets
1import { QuestionItemModel } from '../../models/QuestionItemModel'
2import { Auth } from '../utils/Auth'
3import { vp2vp } from '../utils/Basic'
4import { IvTag } from './IvTag'
5
6@Component
7export struct IvQuestionItem {
8  @ObjectLink
9  item: QuestionItemModel
10
11  @Builder
12  SplitBuilder() {
13    Text('|')
14      .width(vp2vp(30))
15      .textAlign(TextAlign.Center)
16      .fontColor('#C3C3C5')
17      .fontSize(vp2vp(12))
18  }
19
20  @Builder
21  TextBuilder(text: string) {
22    Text(text)
23      .fontColor('#C3C3C5')
24      .fontSize(vp2vp(12))
25  }
26
27  build() {
28    Column() {
29      Row() {
30        IvTag({ difficulty: this.item.difficulty })
31        Text(this.item.stem)
32          .fontSize(vp2vp(15))
33          .layoutWeight(1)
34          .textOverflow({ overflow: TextOverflow.Ellipsis })
35          .maxLines(1)
36      }.width('100%')
37
38      Row() {
39        this.TextBuilder(`点赞 ${this.item.likeCount}`)
40        this.SplitBuilder()
41        this.TextBuilder(`浏览 ${this.item.views}`)
42        this.SplitBuilder()
43        if (this.item.readFlag === 1) {
44          this.TextBuilder(`已看过`)
45        }
46      }
47      .width('100%')
48      .margin({ top: vp2vp(10) })
49    }
50    .height(vp2vp(80))
51    .justifyContent(FlexAlign.Center)
52    .width('100%')
53    .onClick(() => {
54      Auth.pushUrl({
55        url: 'pages/QuestionDetail',
56        params: this.item
57      })
58    })
59  }
60}

5. 加载更多

1)加载和完成状态

1@State
2  finished: boolean = false
3  @State
4  loading: boolean = false

2)判断是否可以加载更多

common/components/IvQuestionList.ets
1getQuestionList() {
2+    if (this.loading || this.finished)  return
common/components/IvQuestionList.ets
1if ( this.params.page < res.data.pageTotal ) {
2          this.params.page ++
3        } else {
4          this.finished = true
5        }
6        this.loading = false

3)监听滚动到最下方

common/components/IvQuestionList.ets
1.onReachEnd(() => {
2      this.getQuestionList()
3    })

4)加上加载UI结构

common/components/IvQuestionList.ets
1@Builder
2  LoadingBuilder () {
3    if (this.finished) {
4      Row() {
5        Text('没有更多了~')
6          .fontColor($r('app.color.gray'))
7          .fontSize(vp2vp(14))
8      }
9      .width('100%')
10      .height(vp2vp(50))
11      .justifyContent(FlexAlign.Center)
12    } else {
13      if (this.loading) {
14        Row() {
15          LoadingProgress()
16            .width(vp2vp(24))
17            .margin({ right: vp2vp(5) })
18          Text('加载中...')
19            .fontColor($r('app.color.gray'))
20            .fontSize(vp2vp(14))
21        }
22        .width('100%')
23        .height(vp2vp(50))
24        .justifyContent(FlexAlign.Center)
25      }
26    }
27  }

6. LazyForEach 性能优化

1)实现数据源

models/BasicDataSource.ets
1export class BasicDataSource implements IDataSource {
2  private listeners: DataChangeListener[] = [];
3
4  public totalCount(): number {
5    return 0;
6  }
7
8  public getData(index: number): any {
9
10  }
11
12  registerDataChangeListener(listener: DataChangeListener): void {
13    if (this.listeners.indexOf(listener) < 0) {
14      console.info('add listener');
15      this.listeners.push(listener);
16    }
17  }
18
19  unregisterDataChangeListener(listener: DataChangeListener): void {
20    const pos = this.listeners.indexOf(listener);
21    if (pos >= 0) {
22      console.info('remove listener');
23      this.listeners.splice(pos, 1);
24    }
25  }
26
27  notifyDataReload(): void {
28    this.listeners.forEach(listener => {
29      listener.onDataReloaded();
30    })
31  }
32
33  notifyDataAdd(index: number): void {
34    this.listeners.forEach(listener => {
35      listener.onDataAdd(index);
36    })
37  }
38
39  notifyDataChange(index: number): void {
40    this.listeners.forEach(listener => {
41      listener.onDataChange(index);
42    })
43  }
44
45  notifyDataDelete(index: number): void {
46    this.listeners.forEach(listener => {
47      listener.onDataDelete(index);
48    })
49  }
50}

2)继承数据源进行扩展

common/components/IvQuestionList.ets
1class QuestionListDataSource extends BasicDataSource {
2
3  private questionList: QuestionItemModel[] = []
4
5  totalCount () {
6    return this.questionList.length
7  }
8
9  getData(index: number): QuestionItemModel  {
10    return this.questionList[index]
11  }
12
13  public addData(index: number, data: QuestionItemModel): void {
14    this.questionList.splice(index, 0, data);
15    this.notifyDataAdd(index);
16  }
17
18  public pushData(data: QuestionItemModel): void {
19    this.questionList.push(data);
20    this.notifyDataAdd(this.questionList.length - 1);
21  }
22
23}

3)改用数据源渲染列表

common/components/IvQuestionList.ets
1questionListDataSource = new QuestionListDataSource()
common/components/IvQuestionList.ets
1initQuestionList () {
2+    if (this.activeIndex === this.selfIndex && this.questionListDataSource.totalCount() === 0) {
3      this.getQuestionList()
4    }
5  }
common/components/IvQuestionList.ets
1}>('question/list', this.params)
2      .then(res => {
3
4+        res.data.rows.forEach(item => {
5+          this.questionListDataSource.pushData(item)
6+        })
common/components/IvQuestionList.ets
1+      LazyForEach(this.questionListDataSource, (item: QuestionItemModel) => {
2        ListItem() {
3          IvQuestionItem({ item })
4        }
5      },