面试宝典APP-我的模块

个人中心

1. 我的-页面结构

views/Index/Mine.ets
1import { IvClock } from '../../common/components/IvClock'
2import { vp2vp } from '../../common/utils/Basic'
3
4@Preview
5@Component
6export struct Mine {
7  @Builder
8  NavBuilder(icon: Resource, title: string,) {
9    Column({ space: vp2vp(8) }) {
10      Image(icon)
11        .width(vp2vp(30))
12        .height(vp2vp(30))
13        .objectFit(ImageFit.Fill)
14      Text(title)
15        .fontColor($r('app.color.gray'))
16        .fontSize(vp2vp(13))
17    }
18  }
19
20  @Builder
21  CellBuilder(title: string, cb?: () => void) {
22    Row() {
23      Text(title)
24        .fontSize(vp2vp(15))
25        .fontColor($r('app.color.black'))
26      Image($r('app.media.icon_my_arrow'))
27        .fillColor('#C3C3C5')
28        .width(vp2vp(18))
29        .height(vp2vp(18))
30    }
31    .width('100%')
32    .height(vp2vp(40))
33    .justifyContent(FlexAlign.SpaceBetween)
34    .padding({ left: vp2vp(15), right: vp2vp(15) })
35    .onClick(()=>{
36      cb && cb()
37    })
38  }
39
40  build() {
41    Column({ space: vp2vp(15) }) {
42      // 头部
43      Row() {
44        Image('/common/images/avatar.png')
45          .alt('/common/images/avatar.png')
46          .width(vp2vp(55))
47          .aspectRatio(1)
48          .borderRadius(vp2vp(30))
49          .border({ width: 0.5, color: '#e4e4e4' })
50        Column({ space: vp2vp(5) }) {
51          Text('黑马程序员')
52            .fontSize(vp2vp(18))
53            .fontWeight(FontWeight.Bold)
54          Row() {
55            Text('编辑资料')
56              .fontSize(vp2vp(12))
57              .fontColor($r('app.color.gray'))
58            Image($r('app.media.icon_edit'))
59              .width(vp2vp(12))
60              .aspectRatio(1)
61              .fillColor($r('app.color.gray'))
62          }
63        }
64        .padding({ left: vp2vp(12) })
65        .alignItems(HorizontalAlign.Start)
66        .layoutWeight(1)
67
68        IvClock({ clockCount: 0 })
69      }
70      .height(vp2vp(80))
71
72      // 导航
73      GridRow({ columns: 4 }) {
74        GridCol() {
75          this.NavBuilder($r('app.media.icon_my_history'), '历史记录')
76        }
77
78        GridCol() {
79          this.NavBuilder($r('app.media.icon_my_favo'), '我的收藏')
80        }
81
82        GridCol() {
83          this.NavBuilder($r('app.media.icon_my_zan'), '我的点赞')
84        }
85
86        GridCol() {
87          Column() {
88            this.NavBuilder($r('app.media.icon_my_time'), '累计学时')
89            Row() {
90              Text('10分钟')
91                .fontColor('#C3C3C5')
92                .fontSize(vp2vp(11))
93              Image($r('app.media.icon_my_arrow'))
94                .fillColor('#C3C3C5')
95                .width(vp2vp(13))
96                .height(vp2vp(13))
97            }
98            .margin({ top: vp2vp(3) })
99          }
100        }
101      }
102      .backgroundColor('#fff')
103      .padding({ top: vp2vp(15), bottom: vp2vp(15) })
104      .borderRadius(vp2vp(8))
105
106      Row() {
107        Text()
108          .width(vp2vp(3))
109          .height(vp2vp(12))
110          .backgroundColor($r('app.color.green'))
111        Text('前端常用词')
112          .fontSize(vp2vp(15))
113          .fontColor($r('app.color.black'))
114          .layoutWeight(1)
115          .padding({ left: vp2vp(12) })
116        Image($r('app.media.icon_my_new'))
117          .width(vp2vp(53))
118          .height(vp2vp(22))
119          .margin({ right: vp2vp(10) })
120      }
121      .backgroundColor('#fff')
122      .padding({ top: vp2vp(15), bottom: vp2vp(15) })
123      .borderRadius(vp2vp(8))
124
125      List() {
126        ListItem() {
127          this.CellBuilder('推荐分享')
128        }
129
130        ListItem() {
131          this.CellBuilder('意见反馈')
132        }
133
134        ListItem() {
135          this.CellBuilder('关于我们')
136        }
137
138        if (this.user.token) {
139          ListItem() {
140            this.CellBuilder('退出登录', () => {
141              // 退出登录
142            })
143          }
144        }
145      }
146      .padding({ top: vp2vp(10), bottom: vp2vp(10) })
147      .borderRadius(vp2vp(8))
148      .backgroundColor('#fff')
149      .divider({ strokeWidth: 0.5, color: $r('app.color.gray_bg') })
150
151    }
152    .padding(vp2vp(15))
153    .width('100%')
154    .height('100%')
155    .backgroundColor($r('app.color.gray_bg'))
156  }
157}

2. 我的-页面逻辑

1)在 Index 初始化持久化用户

pages/Index.ets
1Auth.initLocalUser()

2)获取 AppStorage 用户数据

views/Index/Mine.ets
1@StorageProp(USER_KEY)
2  @Watch('updateUser')
3  userJson: string = '{}'
4  @State
5  user: UserModel = JSON.parse(this.userJson)
6
7  updateUser() {
8    this.user = JSON.parse(this.userJson)
9  }

3)进行展示

views/Index/Mine.ets
1// 头部
2      Row() {
3        Image(this.user.avatar || '/common/images/avatar.png')
4          .alt('/common/images/avatar.png')
5          .width(vp2vp(55))
6          .aspectRatio(1)
7          .borderRadius(vp2vp(30))
8          .border({ width: 0.5, color: '#e4e4e4' })
9        Column({ space: vp2vp(5) }) {
10          if (this.user.token) {
11            Text(this.user.nickName || this.user.username)
12              .fontSize(vp2vp(18))
13              .fontWeight(FontWeight.Bold)
14            Row() {
15              Text('编辑资料')
16                .fontSize(vp2vp(12))
17                .fontColor($r('app.color.gray'))
18              Image($r('app.media.icon_edit'))
19                .width(vp2vp(12))
20                .aspectRatio(1)
21                .fillColor($r('app.color.gray'))
22            }
23          } else {
24            Text('点击登录')
25              .fontSize(vp2vp(18))
26              .fontWeight(FontWeight.Bold)
27              .onClick(() => {
28                router.pushUrl({
29                  url: 'pages/LoginPage'
30                })
31              })
32          }
33        }
34        .padding({ left: vp2vp(12) })
35        .alignItems(HorizontalAlign.Start)
36        .layoutWeight(1)
37
38        IvClock({ clockCount: this.user.clockinNumbers || 0  })
39      }
40      .height(vp2vp(80))
views/Index/Mine.ets
1Column() {
2            this.NavBuilder($r('app.media.icon_my_time'), '累计学时')
3            Row() {
4              Text(formatTime(this.user.totalTime))
5                .fontColor('#C3C3C5')
6                .fontSize(vp2vp(11))
7              Image($r('app.media.icon_my_arrow'))
8                .fillColor('#C3C3C5')
9                .width(vp2vp(13))
10                .height(vp2vp(13))
11            }
12            .margin({ top: vp2vp(3) })
13          }
views/Index/Mine.ets
1if (this.user.token) {
2          this.CellBuilder('退出登录')
3        }

4)Basic 封装时间转换函数

common/utils/Basic.ets
1export const formatTime = (time: number = 0, hasUnit: boolean = true) => {
2  if (time < 3600) {
3    return String(Math.floor(time / 60)) + (hasUnit ? ' 分钟' : '')
4  } else {
5    return String(Math.round(time / 3600 * 10) / 10) + (hasUnit ? ' 小时' : '')
6  }
7}
views/Index/Mine.ets
1Text(formatTime(this.user.totalTime))
2                .fontColor('#C3C3C5')
3                .fontSize(vp2vp(11))

3. 登录-页面结构

pages/LoginPage.ets
1import { vp2vp } from '../common/utils/Basic'
2
3@Extend(TextInput) function customStyle() {
4  .height(vp2vp(44))
5  .borderRadius(vp2vp(2))
6  .backgroundColor('#ffffff')
7  .border({ width: { bottom: 0.5 }, color: '#e4e4e4' })
8  .padding({ left: 0 })
9  .placeholderColor('#C3C3C5')
10  .caretColor('#fa711d')
11}
12
13@Entry
14@Component
15struct LoginPage {
16  @State
17  username: string = 'hmheima'
18  @State
19  password: string = 'Hmheima%123'
20  @State
21  isAgree: boolean = false
22  @State
23  loading: boolean = false
24
25  build() {
26    Navigation() {
27      Scroll() {
28        Column() {
29          Column() {
30            Image($r('app.media.icon'))
31              .width(vp2vp(120))
32              .aspectRatio(1)
33            Text('面试宝典')
34              .fontSize(vp2vp(28))
35              .margin({ bottom: vp2vp(15) })
36            Text('搞定企业面试真题,就用面试宝典')
37              .fontSize(vp2vp(14))
38              .fontColor($r('app.color.gray'))
39          }
40
41          Column({ space: vp2vp(15) }) {
42            TextInput({ placeholder: '请输入用户名', text: this.username })
43              .customStyle()
44              .onChange(val => this.username = val)
45            TextInput({ placeholder: '请输入密码', text: this.password, })
46              .type(InputType.Password)
47              .showPasswordIcon(false)
48              .customStyle()
49              .onChange(val => this.password = val)
50            Row() {
51              Checkbox()
52                .selectedColor('#fa711d')
53                .width(vp2vp(14))
54                .aspectRatio(1)
55                .select(this.isAgree)
56                .onChange((val) => {
57                  this.isAgree = val
58                })
59              Row({ space: vp2vp(4) }) {
60                Text('已阅读并同意')
61                  .fontSize(vp2vp(14))
62                  .fontColor($r('app.color.gray'))
63                Text('用户协议')
64                  .fontSize(vp2vp(14))
65                Text('和')
66                  .fontSize(vp2vp(14))
67                  .fontColor($r('app.color.gray'))
68                Text('隐私政策')
69                  .fontSize(vp2vp(14))
70              }
71            }
72            .width('100%')
73
74            Button({ type: ButtonType.Normal   }) {
75              Row() {
76                if (this.loading) {
77                  LoadingProgress()
78                    .color('#ffffff')
79                    .width(vp2vp(24))
80                    .height(vp2vp(24))
81                    .margin({ right: vp2vp(10) })
82                }
83                Text('立即登录').fontColor('#ffffff')
84              }
85            }
86            .width('100%')
87            .backgroundColor('transparent')
88            .stateEffect(false)
89            .borderRadius(vp2vp(4))
90            .height(vp2vp(44))
91            .linearGradient({
92              direction: GradientDirection.Right,
93              colors: [['#fc9c1c', 0], ['#fa711d', 1]]
94            })
95          }
96          .padding(vp2vp(30))
97
98          Column() {
99            Text('其他登录方式')
100              .fontSize(vp2vp(14))
101              .fontColor($r('app.color.gray'))
102          }
103          .padding({ top: vp2vp(70), bottom: vp2vp(100) })
104        }
105      }
106    }
107    .titleMode(NavigationTitleMode.Mini)
108    .mode(NavigationMode.Stack)
109  }
110}

4. 登录-页面逻辑

1)控制按钮,禁用|启用状态,点击事件

pages/Index.ets
1Button({ type: ButtonType.Normal }) {
2  Row() {
3    if (this.loading) {
4      LoadingProgress()
5        .color('#ffffff')
6        .width(vp2vp(24))
7        .height(vp2vp(24))
8        .margin({ right: vp2vp(10) })
9    }
10    Text('立即登录')
11      .fontColor('#ffffff')
12  }
13}
14.width('100%')
15.backgroundColor('transparent')
16.borderRadius(vp2vp(4))
17.height(vp2vp(44))
18.stateEffect(false)
19.linearGradient({
20  direction: GradientDirection.Right,
21  colors: [['#fc9c1c', 0], ['#fa711d', 1]]
22})
23.enabled(!this.loading)
24.onClick(() => {
25  this.login()
26})

2)实现登录,非空校验、登录跳转

pages/LoginPage.ets
1login() {
2  if (!this.username) {
3    return promptAction.showToast({ message: '请输入用户名' })
4  }
5  if (!this.password) {
6    return promptAction.showToast({ message: '请输入密码' })
7  }
8  if (!this.isAgree) {
9    return promptAction.showToast({ message: '请勾选已阅读并同意' })
10  }
11
12  this.loading = true
13  Request.post<UserModel>('login', {
14    username: this.username,
15    password: this.password
16  }).then(res => {
17    Auth.setUser(res.data)
18
19    const params = router.getParams()
20    if (params && params['return_url']) {
21      const url = params['return_url']
22      delete params['return_url']
23      router.replaceUrl({ url, params })
24    } else {
25      router.back()
26    }
27    this.loading = false
28    return promptAction.showToast({ message: '登录成功' })
29  }).catch(e => {
30    this.loading = false
31    return promptAction.showToast({ message: '登录失败' })
32  })
33
34}

5. 我的-退出登录

1)退出登录

pages/Mine.ets
1if (this.user.token) {
2  this.CellBuilder('退出登录', async () => {
3    const ok = await promptAction.showDialog({
4      title: '温馨提示',
5      message: '您确认要退出面试宝典APP吗?',
6      buttons: [
7        { text: '取消', color: '#c3c4c5' },
8        { text: '确认', color: '#333333', }
9      ]
10    })
11    if (ok.index === 1) {
12      Auth.delUser()
13    }
14  })
15}

2)退出和登录,使用 emitter 通知 Home 更新页面

基本语法:

1on(event: InnerEvent, callback: Callback<EventData>): void
2
3emit(event: InnerEvent, data?: EventData): void
4
5interface InnerEvent {
6  eventId: number
7}
  • Home 注册事件
views/Index/Home.ets
1aboutToAppear() {
2  this.getQuestionTypeList()
3
4  emitter.on(LOGIN_EVENT, () => this.getQuestionTypeList())
5}
  • 登录触发事件
pages/LoginPage.ets
1Auth.setUser(res.data)
2// 通知 Home 更新页面
3emitter.emit(LOGIN_EVENT)
  • 退出触发事件
views/Index/Mine.ets
1if (ok.index === 1) {
2  Auth.delUser()
3  // 通知 Home 更新页面
4  emitter.emit(LOGIN_EVENT)
5}

打卡日历

1. 实现打卡

1)首页打开数量显示

views/Index/Home.ets
1@StorageProp(USER_KEY)
2  @Watch('updateUser')
3  userJson: string = '{}'
4  @State
5  user: UserModel = JSON.parse(this.userJson)
6
7  updateUser() {
8    this.user = JSON.parse(this.userJson)
9  }
views/Index/Home.ets
1Row({ space: vp2vp(10) }) {
2        IvSearch()
3+        IvClock({ clockCount: this.user.clockinNumbers || 0 })
4      }

2)打卡功能实现

common/components/IvClock.ets
1.onClick(() => {
2  const user = Auth.getUser()
3  if (user.token) {
4    if (user.clockinNumbers > 0) {
5      // 跳转打卡日历页面
6      return router.pushUrl({ url: 'pages/ClockPage' })
7    } else {
8      // 进行打卡
9      Request.post<{ clockinNumbers: number }>('clockin').then(res => {
10        Auth.setUser({ ...user, clockinNumbers: res.data.clockinNumbers })
11        promptAction.showToast({ message: '打卡成功' })
12      })
13    }
14  } else {
15    router.pushUrl({ url: 'pages/LoginPage' })
16  }
17})

3)更新打卡信息(用户信息)重新登录系统的时候

views/Index/Mine.ets
1// 获取用户信息
2aboutToAppear(){
3  if (this.user.token) {
4    Request.get<UserModel>('userInfo')
5      .then(res => {
6        const { avatar, nickName, clockinNumbers, totalTime } = res.data
7        Auth.setUser({ ...this.user, avatar, nickName, clockinNumbers, totalTime })
8      })
9  }
10}

2. 打卡日历-页面结构

pages/ClockPage.ets
1import { vp2vp } from '../common/utils/Basic'
2
3@Entry
4@Component
5struct ClockPage {
6  @Builder
7  dayBuilder(params: {
8    day: number,
9    text: string
10  }) {
11    Column() {
12      Row() {
13        Text(params.day.toString())
14          .fontSize(vp2vp(40))
15          .fontWeight(FontWeight.Bold)
16        Text('天')
17          .fontSize(vp2vp(10))
18          .fontColor($r('app.color.gray'))
19          .margin({ bottom: vp2vp(8), left: vp2vp(10) })
20      }
21      .alignItems(VerticalAlign.Bottom)
22
23      Text(params.text)
24        .fontSize(vp2vp(10))
25        .fontColor($r('app.color.gray'))
26    }.margin({ right: vp2vp(36) })
27  }
28
29  build() {
30    Column() {
31      Navigation() {
32        Column() {
33          Row() {
34            Text('今日已打卡')
35              .fontSize(vp2vp(20))
36              .margin({ right: vp2vp(5) })
37            Image($r('app.media.icon_clock_card'))
38              .width(vp2vp(20))
39              .aspectRatio(1)
40              .objectFit(ImageFit.Fill)
41          }
42          .width('100%')
43
44          Row() {
45            this.dayBuilder({ day: 10, text: '累计打卡' })
46            this.dayBuilder({ day: 10, text: '连续打卡' })
47          }
48          .padding({ top: vp2vp(10), bottom: vp2vp(25) })
49          .width('100%')
50          .justifyContent(FlexAlign.Start)
51
52          Row() {
53
54          }
55          .height(300)
56          .margin({ bottom: vp2vp(50) })
57
58          Image('/common/images/clock_btn.png')
59            .width(vp2vp(145))
60            .height(vp2vp(38))
61            .objectFit(ImageFit.Fill)
62
63        }
64        .padding(vp2vp(15))
65        .layoutWeight(1)
66      }
67      .title('每日打卡')
68      .titleMode(NavigationTitleMode.Mini)
69      .mode(NavigationMode.Stack)
70      .backgroundImage('/common/images/clock_bg.png')
71      .backgroundImageSize(ImageSize.Contain)
72      .backgroundImagePosition(Alignment.Top)
73    }.backgroundColor($r('app.color.gray_bg'))
74  }
75}

3. 打卡日历-第三方库

1)方式1:使用 ohpm 命令行,参考:链接

2)方式2:

entry/oh-package.json5
1"dependencies": {
2    "dayjs": "latest"
3  }

3)查找支持的第三方包 链接

4. 打卡日历-共享日历本地库

1)创建本地包

  • 创建模块
  • 包名称 miniCalendar

2)修改日历文件,名称 MainPage.ets 改成 MiniCalendar.ets

miniCalendar/src/main/ets/utils/index.ets
1// 设计稿宽度
2import display from '@ohos.display'
3import deviceInfo from '@ohos.deviceInfo'
4const designWidth = 375
5// 物理像素
6const devicePhysics = display.getDefaultDisplaySync().width
7
8export const vp2vp = (originSize: number) => {
9  // useSize =  deviceWidth / designWidth * measureSize
10  // 只有 手机 才需要
11  if (deviceInfo.deviceType !== 'tablet') {
12    return px2vp(devicePhysics) / designWidth * originSize
13  }
14  return originSize
15}
miniCalendar/src/main/ets/components/MiniCalendar.ets
1import dayjs from 'dayjs'
2import { vp2vp } from '../utils'
3
4const img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAABECAYAAAA4E5OyAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAF5SURBVHgB7drLUcMwFIXhGzvOMKwoJXSAO0gJ0BkdECoQJaQT2II1MTo8NiZsmLm6R+R8mzjJIuN/Yo0tyUxERERERERE/rmVOUopXa3Xm4dyeFN+ap/z6904ji9GrDNHfT/s7CMGzLsSJyGSEXMNMs+2PPktexTXIMfjdF9eDouPqaO4jiHwNY6kcrhdfHXI+W1kG1Pcg0BLUaoEgVaiVAsCLUSpGgTYo1QPAsxRQoIAa5SwIMAYJTQIsEUJDwJMUSiCAEsUmiDAEIUqCERHoQsCkVFcH///CieME7cTUwddN9yaI8og31Yn/r993z+bI8oguGSGYZPKjNuPS2aaLh7NEeWg+luMnC/L1XR9PoNqdAygujGLjgE0t+4MMYDi4Y4lBoQ//jPFgNAJIrYYEDaFyBgDQiaZWWNA9WUI5hhQdaGKPQZUW8psIQZUWexuJQa47yBqKQa47yBqKQbU3kFEHQNq7CDaf76bn9hjiIiIiIiIiMhZeAeOPE1Nm7TnTQAAAABJRU5ErkJggg=='
5
6class DayItem {
7  public date: number
8  public month: number
9  public year: number
10  public isSelected?: boolean
11}
12
13@Component
14export struct MiniCalendar {
15  // 内部属性
16  private weeks: string[] = ['日', '一', '二', '三', '四', '五', '六']
17  private selectedText: string = '已打卡'
18  private format: string = 'YYYY-MM-DD'
19  // 当前时间
20  @Prop
21  @Watch('onCurrentDateUpdate')
22  currentDate: string
23  // 选中时间
24  @Link
25  @Watch('onCurrentDateUpdate')
26  selectedDays: string[]
27  @State
28  days: DayItem[] = []
29  onClickDate: (date: string) => void
30  onChangeMonth: (date: string) => void
31
32  onCurrentDateUpdate() {
33    this.days = this.getDays(this.currentDate)
34  }
35
36  aboutToAppear() {
37    this.days = this.getDays(this.currentDate)
38  }
39
40  getDays(originDate?: string) {
41    const date = originDate ? dayjs(originDate) : dayjs()
42    const days: DayItem[] = []
43    const len = 42
44
45    // 当前月
46    const currDays = date.daysInMonth()
47    for (let index = 1; index <= currDays; index++) {
48      days.push({
49        date: index,
50        month: date.month() + 1,
51        year: date.year(),
52        isSelected: this.selectedDays.some(item => date.date(index).isSame(item))
53      })
54    }
55    // 上个月
56    const prevMonth = date.date(0)
57    const prevMonthLastDate = prevMonth.date()
58    const prevDays = prevMonth.day()
59    if (prevDays < 6) {
60      for (let index = 0; index <= prevDays; index++) {
61        days.unshift({
62          date: prevMonthLastDate - index,
63          month: prevMonth.month() + 1,
64          year: prevMonth.year()
65        })
66      }
67    }
68    // 下个月
69    const nextMonth = date.date(currDays + 1)
70    const start = days.length
71    for (let index = 1; index <= len - start; index++) {
72      days.push({
73        date: index,
74        month: nextMonth.month() + 1,
75        year: nextMonth.year()
76      })
77    }
78
79    return days
80  }
81
82  isTheMonth(month: number) {
83    return dayjs(this.currentDate).month() === month - 1
84  }
85
86  @Styles
87  btnStyle() {
88    .width(vp2vp(20))
89    .height(vp2vp(20))
90    .backgroundColor('#f6f7f9')
91    .borderRadius(vp2vp(4))
92  }
93
94  build() {
95    Column() {
96      Row() {
97        Column() {
98          Image(img)
99            .width(vp2vp(14))
100            .aspectRatio(1)
101            .rotate({ angle: 180 })
102        }
103        .btnStyle()
104        .justifyContent(FlexAlign.Center)
105        .onClick(() => {
106          const date = dayjs(this.currentDate).subtract(1, 'month')
107          this.currentDate = date.format(this.format)
108          this.onChangeMonth && this.onChangeMonth(date.format('YYYY-MM'))
109        })
110
111        Text(dayjs(this.currentDate).format('YYYY年MM月'))
112          .fontColor('#6B7897')
113          .margin({ right: vp2vp(20), left: vp2vp(20) })
114          .width(vp2vp(110))
115          .textAlign(TextAlign.Center)
116
117        Column() {
118          Image(img)
119            .width(vp2vp(14))
120            .aspectRatio(1)
121        }
122        .btnStyle()
123        .justifyContent(FlexAlign.Center)
124        .onClick(() => {
125          const date = dayjs(this.currentDate).add(1, 'month')
126          this.currentDate = date.format(this.format)
127          this.onChangeMonth && this.onChangeMonth(date.format('YYYY-MM'))
128        })
129      }
130      .padding(15)
131
132      GridRow({ columns: 7 }) {
133        ForEach(this.weeks, (item) => {
134          GridCol() {
135            Column() {
136              Text(item)
137                .fontColor('#6E7B8A')
138            }
139          }.height(vp2vp(40))
140        })
141        ForEach(this.days, (item: DayItem) => {
142          GridCol() {
143            Column() {
144              if (item.isSelected) {
145                Text(item.date.toString())
146                  .fontColor('#fff')
147                  .width(vp2vp(32))
148                  .height(vp2vp(32))
149                  .borderRadius(vp2vp(16))
150                  .backgroundColor('#FFC531')
151                  .textAlign(TextAlign.Center)
152                  .fontSize(vp2vp(14))
153                Text(this.selectedText)
154                  .fontColor('#FFC531')
155                  .fontSize(vp2vp(10))
156                  .margin({ top: vp2vp(2) })
157
158              } else {
159                Text(item.date.toString())
160                  .width(vp2vp(32))
161                  .height(vp2vp(32))
162                  .fontSize(vp2vp(14))
163                  .textAlign(TextAlign.Center)
164                  .fontColor(this.isTheMonth(item.month) ? '#6E7B8A' : '#E1E4E7')
165              }
166            }.onClick(() => {
167              const date = `${item.year}-${item.month.toString().padStart(2, '0')}-${item.date.toString()
168                .padStart(2, '0')}`
169              this.onClickDate && this.onClickDate(date)
170            })
171          }.height(vp2vp(48))
172        })
173      }
174      .padding({ top: vp2vp(15) })
175      .border({ width: { top: 0.5 }, color: '#f6f7f9' })
176    }
177    .width('100%')
178    .backgroundColor('#fff')
179    .borderRadius(vp2vp(8))
180  }
181}

3)导出组件

index.ets
1export { MiniCalendar } from './src/main/ets/components/MiniCalendar'
2export { vp2vp } from './src/main/ets/utils'

4)使用组件

entry/oh-package.json5
1"dependencies": {
2    "@ohos/miniCalendar": "file:../miniCalendar"
3  }
1import { MiniCalendar } from '@ohos/miniCalendar'
1MiniCalendar({
2              currentDate: '2023-12-9',
3              selectedDays: $selectedDays,
4              onChangeMonth: (date: string) => {
5                promptAction.showToast({ message: date })
6              },
7              selectedText: '已签到'
8            })

3. 打卡日历-显示签到

1)数据模型

models/ClockModel.ets
1export class ClockModel {
2  flag?: boolean
3  clockinNumbers?: number
4  totalClockinNumber?: number
5  clockins?: ClockInModel[]
6}
7
8export class ClockInModel {
9  id: string
10  createdAt: string
11}

2)获取数据

pages/ClockPage.ets
1@State
2  clockData: ClockModel = {
3    totalClockinNumber: 0,
4    clockinNumbers: 0
5  }
6
7  aboutToAppear() {
8    this.getData()
9  }
10
11  async getData(date?: {
12    year: string,
13    month: string
14  }) {
15    const res = await Request.get<ClockModel>('clockinInfo', date || {})
16    this.clockData = res.data
17    this.selectedDays = res.data.clockins.map(item => item.createdAt)
18  }

3)渲染页面

pages/ClockPage.ets
1Row() {
2            this.dayBuilder({ day: this.clockData.totalClockinNumber, text: '累计打卡' })
3            this.dayBuilder({ day: this.clockData.clockinNumbers, text: '连续打卡' })
4          }

4)切换月份查询打卡数据

pages/ClockPage.ets
1MiniCalendar({
2              currentDate: '2023-12-9',
3              selectedDays: $selectedDays,
4              onChangeMonth: (date: string) => {
5                const [year, month] = date.split('-')
6                this.getData({ year, month })
7              }
8            })

编辑资料

1. 资料页面

views/Index/Mine.ets
1Row() {
2              Text('编辑资料')
3                .fontSize(vp2vp(12))
4                .fontColor($r('app.color.gray'))
5              Image($r('app.media.icon_edit'))
6                .width(vp2vp(12))
7                .aspectRatio(1)
8                .fillColor($r('app.color.gray'))
9            }
10            .onClick(() => {
11              router.pushUrl({
12                url: 'pages/ProfilePage'
13              })
14            })
pages/ProfilePage.ets
1import { USER_KEY } from '../common/utils/Auth'
2import { vp2vp } from '../common/utils/Basic'
3import { UserModel } from '../models/UserModel'
4
5@Entry
6@Component
7struct ProfilePage {
8  @StorageProp(USER_KEY)
9  @Watch('updateUser')
10  userJson: string = '{}'
11  @State
12  user: UserModel = JSON.parse(this.userJson)
13
14  updateUser() {
15    this.user = JSON.parse(this.userJson)
16  }
17
18  pickerAvatar() {
19    // TODO
20  }
21
22  updateNickName() {
23    // TODO
24  }
25
26  build() {
27    Navigation() {
28      List() {
29        ListItem() {
30          Row() {
31            Text('头像')
32            Image(this.user.avatar)
33              .alt('/common/images/avatar.png')
34              .width(vp2vp(40))
35              .aspectRatio(1)
36              .borderRadius(vp2vp(20))
37              .border({ width: 0.5, color: '#e4e4e4' })
38              .onClick(() => {
39                this.pickerAvatar()
40              })
41          }
42          .width('100%')
43          .height(vp2vp(60))
44          .justifyContent(FlexAlign.SpaceBetween)
45        }
46
47        ListItem() {
48          Row() {
49            Text('昵称')
50            TextInput({ text: this.user.nickName })
51              .textAlign(TextAlign.End)
52              .layoutWeight(1)
53              .padding(0)
54              .height(vp2vp(60))
55              .backgroundColor(Color.White)
56              .borderRadius(0)
57              .onChange((value) => this.user.nickName = value)
58              .onSubmit(() => {
59                this.updateNickName()
60              })
61          }
62          .width('100%')
63          .justifyContent(FlexAlign.SpaceBetween)
64        }
65      }
66      .width('100%')
67      .height('100%')
68      .padding({ left: vp2vp(30), right: vp2vp(30), top: vp2vp(15), bottom: vp2vp(15) })
69      .divider({ strokeWidth: 0.5, color: '#f5f5f5' })
70    }
71    .title('完善个人信息')
72    .titleMode(NavigationTitleMode.Mini)
73    .mode(NavigationMode.Stack)
74  }
75}

2. 修改昵称

1)实现更新

1updateNickName() {
2    Request.post('userInfo/profile', {
3      nickName: this.user.nickName
4    }).then(res => {
5      promptAction.showToast({ message: '更新昵称成功' })
6      Auth.setUser(this.user)
7    })
8  }

2)自定义弹窗

common/components/IvLoadingDialog.ets
1import { vp2vp } from '../utils/Basic'
2
3@CustomDialog
4export struct IvLoadingDialog {
5  message: string = ''
6  controller: CustomDialogController
7
8  build() {
9    Column() {
10      LoadingProgress()
11        .width(vp2vp(48))
12        .aspectRatio(1)
13        .color('#fff')
14      if (this.message) {
15        Text(this.message)
16          .fontSize(vp2vp(14))
17          .fontColor('#fff')
18      }
19    }
20    .width(vp2vp(120))
21    .aspectRatio(1)
22    .backgroundColor('rgba(0,0,0,0.5)')
23    .borderRadius(vp2vp(16))
24    .justifyContent(FlexAlign.Center)
25  }
26}

3)初始化加载框

pages/ProfilePage.ets
1dialog: CustomDialogController = new CustomDialogController({
2    builder: IvLoadingDialog({ message: '更新中...' }),
3    customStyle: true,
4
5  })
6
7  updateNickName() {
8  this.dialog.open()
9  Request.post('userInfo/profile', {
10    nickName: this.user.nickName
11  }).then(res => {
12    Auth.setUser(this.user)
13    this.dialog.close()
14  })
15}

3. 修改头像

1)选择文件

pages/ProfilePage.ets
1URI: string = null
2
3pickerAvatar() {
4  const photoSelectOptions = new picker.PhotoSelectOptions()
5  photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
6  photoSelectOptions.maxSelectNumber = 1
7
8  const photoViewPicker = new picker.PhotoViewPicker()
9  photoViewPicker.select(photoSelectOptions).then(result => {
10    // 1. 得到文件路径
11    this.URI = result.photoUris[0]
12    this.uploadAvatar()
13  })
14}

2)上传图片

pages/ProfilePage.ets
1uploadAvatar() {
2  this.dialog.open()
3
4  const context = getContext(this)
5  const fileType = 'jpg'
6  const fileName = Date.now() + '.' + fileType
7  const copyFilePath = context.cacheDir + '/' + fileName
8
9  const file = fs.openSync(this.URI, fs.OpenMode.READ_ONLY)
10  fs.copyFileSync(file.fd, copyFilePath)
11
12  const config: request.UploadConfig = {
13    url: BaseURL + 'userInfo/avatar',
14    method: 'POST',
15    header: {
16      'Accept': '*/*',
17      'Authorization': `Bearer ${this.user.token}`,
18      'Content-Type': 'multipart/form-data'
19    },
20    files: [
21      { name: 'file', uri: `internal://cache/` + fileName, type: fileType, filename: fileName }
22    ],
23    data: []
24  }
25
26  request.uploadFile(context, config, (err, data) => {
27    if (err) return Logger.error('UPLOAD', err.message)
28    data.on('progress', (size) => {
29      Logger.info(size.toString())
30    })
31    data.on('complete', () => {
32      this.getUserInfo()
33    })
34  })
35}

3)更新数据

pages/ProfilePage.ets
1getUserInfo () {
2  Request.get<{ avatar: string }>('userInfo').then(res => {
3    this.user.avatar = res.data.avatar
4    Auth.setUser(this.user)
5    this.dialog.close()
6  })
7}