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}
1)在 Index 初始化持久化用户
1Auth.initLocalUser()
2)获取 AppStorage 用户数据
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)进行展示
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))
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 }
1if (this.user.token) {
2 this.CellBuilder('退出登录')
3 }
4)Basic 封装时间转换函数
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}
1Text(formatTime(this.user.totalTime))
2 .fontColor('#C3C3C5')
3 .fontSize(vp2vp(11))
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}
1)控制按钮,禁用|启用状态,点击事件
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)实现登录,非空校验、登录跳转
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}
1)退出登录
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}
1aboutToAppear() {
2 this.getQuestionTypeList()
3
4 emitter.on(LOGIN_EVENT, () => this.getQuestionTypeList())
5}
1Auth.setUser(res.data)
2// 通知 Home 更新页面
3emitter.emit(LOGIN_EVENT)
1if (ok.index === 1) {
2 Auth.delUser()
3 // 通知 Home 更新页面
4 emitter.emit(LOGIN_EVENT)
5}
1)首页打开数量显示
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 }
1Row({ space: vp2vp(10) }) {
2 IvSearch()
3+ IvClock({ clockCount: this.user.clockinNumbers || 0 })
4 }
2)打卡功能实现
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)更新打卡信息(用户信息)重新登录系统的时候
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}
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}
1)方式1:使用 ohpm 命令行,参考:链接
2)方式2:
1"dependencies": {
2 "dayjs": "latest"
3 }
3)查找支持的第三方包 链接
1)创建本地包
2)修改日历文件,名称 MainPage.ets
改成 MiniCalendar.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}
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)导出组件
1export { MiniCalendar } from './src/main/ets/components/MiniCalendar'
2export { vp2vp } from './src/main/ets/utils'
4)使用组件
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 })
1)数据模型
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)获取数据
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)渲染页面
1Row() {
2 this.dayBuilder({ day: this.clockData.totalClockinNumber, text: '累计打卡' })
3 this.dayBuilder({ day: this.clockData.clockinNumbers, text: '连续打卡' })
4 }
4)切换月份查询打卡数据
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 })
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 })
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}
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)自定义弹窗
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)初始化加载框
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}
1)选择文件
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)上传图片
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)更新数据
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}