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 }
1)数据类型
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 }
1)标签组件
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)试题项组件
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)试题列表组件
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)首页使用组件
1ForEach(this.questionTypeList, (item: QuestionTypeModel, index) => {
2 TabContent() {
3 // 列表组件
4 IvQuestionList()
5 }
6 .tabBar(this.TabItemBuilder(item, index))
7 .height('100%')
8 })
1)请求参数类型
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)试题列表组件
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)试题选项组件
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}
1)加载和完成状态
1@State
2 finished: boolean = false
3 @State
4 loading: boolean = false
2)判断是否可以加载更多
1getQuestionList() {
2+ if (this.loading || this.finished) return
1if ( this.params.page < res.data.pageTotal ) {
2 this.params.page ++
3 } else {
4 this.finished = true
5 }
6 this.loading = false
3)监听滚动到最下方
1.onReachEnd(() => {
2 this.getQuestionList()
3 })
4)加上加载UI结构
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 }
1)实现数据源
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)继承数据源进行扩展
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)改用数据源渲染列表
1questionListDataSource = new QuestionListDataSource()
1initQuestionList () {
2+ if (this.activeIndex === this.selfIndex && this.questionListDataSource.totalCount() === 0) {
3 this.getQuestionList()
4 }
5 }
1}>('question/list', this.params)
2 .then(res => {
3
4+ res.data.rows.forEach(item => {
5+ this.questionListDataSource.pushData(item)
6+ })
1+ LazyForEach(this.questionListDataSource, (item: QuestionItemModel) => {
2 ListItem() {
3 IvQuestionItem({ item })
4 }
5 },