【腾讯 CoDesign】面试宝典 https://codesign.qq.com/s/2kY5j3LVeb9ExNd 访问密码:2YDD
【apifox】面试宝典 https://apifox.com/apidoc/shared-31e6a8d7-316a-4c5a-83c1-e5c7a2c343bf
登录 | 首页 |
---|---|
我的 | 打卡 |
---|---|
面试题 | 学习时间 |
---|---|
创建项目,参考华为 Codelabs 优秀案例结合项目自身情况对项目结构进行维护
1)创建项目
2)准备好项目结构
ets
├── common // 通用模块
│ ├── components // - 通用组件
│ ├── constants // - 常量数据
│ ├── images // - 图片资源
│ └── utils // - 工具类
├── entryability // 入口UIAbility
│ └── EntryAbility.ts
├── models // - 数据模型
├── pages // - 页面组件
│ └── Index.ets
└── views // - 页面对应自定义组件
└── Index
1)封装 logger 工具类
1import hilog from '@ohos.hilog'
2
3const DOMAIN = 0xFF00
4const PREFIX = 'INTERVIEW'
5const FORMAT = '%{public}s, %{public}s'
6
7export class Logger {
8 static debug(...args: string[]) {
9 hilog.debug(DOMAIN, PREFIX, FORMAT, args)
10 }
11
12 static info(...args: string[]) {
13 hilog.info(DOMAIN, PREFIX, FORMAT, args)
14 }
15
16 static warn(...args: string[]) {
17 hilog.warn(DOMAIN, PREFIX, FORMAT, args)
18 }
19
20 static error(...args: string[]) {
21 hilog.error(DOMAIN, PREFIX, FORMAT, args)
22 }
23
24 static fatal(...args: string[]) {
25 hilog.fatal(DOMAIN, PREFIX, FORMAT, args)
26 }
27
28 static isLoggable(level: hilog.LogLevel) {
29 return hilog.isLoggable(DOMAIN, PREFIX, level)
30 }
31}
2)修改 entryAbility 的日志打印
1import UIAbility from '@ohos.app.ability.UIAbility';
2import window from '@ohos.window';
3import { Logger } from '../common/utils/Logger';
4
5export default class EntryAbility extends UIAbility {
6 onCreate(want, launchParam) {
7 Logger.info('Ability onCreate')
8 }
9
10 onDestroy() {
11 Logger.info('Ability onDestroy')
12 }
13
14 onWindowStageCreate(windowStage: window.WindowStage) {
15 // Main window is created, set main page for this ability
16 Logger.info('Ability onWindowStageCreate')
17
18 windowStage.loadContent('pages/Index', (err, data) => {
19 if (err.code) {
20 Logger.info('Failed to load the content', JSON.stringify(err) ?? '')
21 return;
22 }
23 Logger.info('Succeeded in loading the content', JSON.stringify(data) ?? '');
24 });
25 }
26
27 onWindowStageDestroy() {
28 // Main window is destroyed, release UI related resources
29 Logger.info( 'Ability onWindowStageDestroy');
30 }
31
32 onForeground() {
33 // Ability has brought to foreground
34 Logger.info('Ability onForeground');
35 }
36
37 onBackground() {
38 // Ability has back to background
39 Logger.info('Ability onBackground');
40 }
41}
1)用户信息持久化,获取用户,设置用户,删除用户以及用户访问页面控制
1export class UserModel {
2 id: string
3 username: string
4 avatar: string
5 refreshToken: string
6 token: string
7 totalTime?: number
8 nickName?: string
9 clockinNumbers?: number
10}
1import router from '@ohos.router'
2import { UserModel } from '../../models/User'
3
4const USER_KEY = 'interview-user'
5const PASS_LIST = ['pages/LoginPage']
6
7export class Auth {
8 static initLocalUser() {
9 PersistentStorage.PersistProp(USER_KEY, '{}')
10 }
11
12 static setUser(user: UserModel) {
13 AppStorage.Set(USER_KEY, JSON.stringify(user))
14 }
15
16 static getUser() {
17 const str = AppStorage.Get<string>(USER_KEY) || '{}'
18 return JSON.parse(str) as UserModel
19 }
20
21 static delUser() {
22 AppStorage.Set(USER_KEY, '{}')
23 }
24
25 // 需要登录页面使用
26 static pushUrl(options: router.RouterOptions) {
27 const user = this.getUser()
28 if (!PASS_LIST.includes(options.url) && !user.token) {
29 return router.pushUrl({
30 url: 'pages/LoginPage',
31 params: {
32 ...options.params,
33 return_url: options.url
34 }
35 })
36 }
37 return router.pushUrl(options)
38 }
39}
2)在 Index 初始化持久化用户
1Auth.initLocalUser()
1import http from '@ohos.net.http'
2import { Auth } from './Auth'
3
4const BASE_URL = 'https://api-harmony-teach.itheima.net/hm/'
5
6const req = http.createHttp()
7
8const request = <T = unknown>(
9 url: string,
10 method: http.RequestMethod = http.RequestMethod.GET,
11 params: object = {}
12) => {
13 let fullUrl = BASE_URL + url
14 const options: http.HttpRequestOptions = {
15 header: {
16 'Content-Type': 'application/json',
17 },
18 readTimeout: 30000,
19 method,
20 }
21
22 // 如果是对象数据拼接URL,如果是其他则携带在 extraData
23 if (method === http.RequestMethod.GET) {
24 const urlParams = Object.keys(params).map(key => `${key}=${params[key]}`)
25 fullUrl += `?${urlParams.join('&')}`
26 } else {
27 options.extraData = params
28 }
29
30 // 携带 token
31 const user = Auth.getUser()
32 if (user.token) {
33 options.header['Authorization'] = `Bearer ${user.token}`
34 }
35
36 return req.request(fullUrl, options)
37
38}
39
40export class Request {
41 static get<T>(url: string, params?: object) {
42 return request<T>(url, http.RequestMethod.GET, params)
43 }
44
45 static post<T>(url: string, params?: object) {
46 return request<T>(url, http.RequestMethod.POST, params)
47 }
48
49 static put<T>(url: string, params?: object) {
50 return request<T>(url, http.RequestMethod.PUT, params)
51 }
52
53 static delete<T>(url: string, params?: object) {
54 return request<T>(url, http.RequestMethod.DELETE, params)
55 }
56}
1return req.request(fullUrl, options)
2 .then(res => {
3 if (res.result) {
4 // 1. 处理响应
5 const response = JSON.parse(res.result as string) as {
6 code: number
7 message: string
8 data: T
9 }
10 if (response.code === 10000) {
11 return response
12 }
13 // 2. 处理 token 失效
14 if (response.code === 401) {
15 Auth.delUser()
16 router.pushUrl({
17 url: 'pages/Login'
18 }, router.RouterMode.Single)
19 }
20 }
21 return Promise.reject(res.result)
22 })
23 .catch(err => {
24 promptAction.showToast({ message: '网络错误' })
25 return Promise.reject(err)
26 })
27 .finally(() => {
28 // 销毁请求释放资源
29 req.destroy()
30 })
1// 请求日志:
2+ Logger.info(`REQUEST→${url}→${method}`, JSON.stringify(params))
3
4 return req.request(fullUrl, options)
5 .then(res => {
6 if (res.result) {
7 // 响应日志:
8+ Logger.info(`RESPONSE→${url}→${method}`, res.result.toString().substring(0, 250))
9 // 1. 处理响应
10 const response = JSON.parse(res.result as string) as {
11 code: number
12 message: string
13 data: T
14 }
15 if (response.code === 10000) {
16 return response
17 }
18 // 2. 处理 token 失效
19 if (response.code === 401) {
20 Auth.delUser()
21 router.pushUrl({
22 url: 'pages/Login'
23 }, router.RouterMode.Single)
24 }
25 }
26 return Promise.reject(res.result)
27 })
28 .catch(err => {
29 promptAction.showToast({ message: '网络错误' })
30 // 错误日志:
31+ Logger.error(`RESPONSE→${url}→${method}`, JSON.stringify(err).substring(0, 250))
32 return Promise.reject(err)
33 })
34 .finally(() => {
35 // 销毁请求释放资源
36 req.destroy()
37 })
一个界面适配多类终端
手机-竖屏
pad-横屏
1import mediaquery from '@ohos.mediaquery'
2class ToolBarItem {
3 defaultIcon: string | Resource
4 activeIcon: string | Resource
5 label: string
6}
7
8@Entry
9@Component
10struct Index {
11 @State
12 activeIndex: number = 0
13
14 @State
15 isLandscape: boolean = false
16
17 listenerScreen = mediaquery.matchMediaSync('(orientation: landscape) and (device-type: tablet)')
18
19 aboutToAppear() {
20 this.listenerScreen.on('change', (mediaQueryResult) => {
21 this.isLandscape = mediaQueryResult.matches
22 })
23 }
24
25 toolBars: ToolBarItem[] = [
26 { defaultIcon: $r('app.media.home'), activeIcon: $r('app.media.home_select'), label: '首页' },
27 { defaultIcon: $r('app.media.project'), activeIcon: $r('app.media.project_select'), label: '项目' },
28 { defaultIcon: $r('app.media.interview'), activeIcon: $r('app.media.interview_select'), label: '面经' },
29 { defaultIcon: $r('app.media.mine'), activeIcon: $r('app.media.mine_select'), label: '我的' }
30 ]
31
32 @Builder
33 TabBarBuilder(item: ToolBarItem, index: number) {
34 Column({ space: 4 }) {
35 Image(this.activeIndex === index ? item.activeIcon : item.defaultIcon)
36 .width(24)
37 Text(item.label)
38 .fontSize(12)
39 .fontColor(this.activeIndex === index ? '#000' : '#aaa')
40 }
41 }
42
43 build() {
44 Tabs({
45 index: this.activeIndex,
46 }) {
47 ForEach(this.toolBars, (item: ToolBarItem, index: number) => {
48 TabContent() {
49 Text(index.toString())
50 }
51 .tabBar(this.TabBarBuilder(item, index))
52 })
53 }
54 .barPosition(this.isLandscape ? BarPosition.Start : BarPosition.End)
55 .vertical(this.isLandscape)
56 .barHeight(this.isLandscape ? 400 : 50)
57 .onChange(index => this.activeIndex = index)
58 }
59}
手机端等比例缩放适配设备
1// 安卓一般 360 设计稿 1080,面试宝典是 375
2const designWidth = 375
3// 书写尺寸 = 设备宽度 / 设计稿宽度 * 测量尺寸
4const useSize = deviceWidth / designWidth * measureSize
1import display from '@ohos.display'
2import deviceInfo from '@ohos.deviceInfo'
3
4const designWidth = 375
5const devicePhysics = display.getDefaultDisplaySync().width
6
7export const vp2vp = (originSize: number) => {
8 // 不是PAD是手机等比例缩放
9 if ( deviceInfo.deviceType !== 'tablet' ) {
10 return px2vp(devicePhysics) / designWidth * originSize
11 }
12 return originSize
13}
1@Builder
2 TabBarBuilder(item: ToolBarItem, index: number) {
3 Column({ space: vp2vp(4) }) {
4 Image(this.activeIndex === index ? item.activeIcon : item.defaultIcon)
5 .width(vp2vp(24))
6 Text(item.label)
7 .fontSize(vp2vp(12))
8 .fontColor(this.activeIndex === index ? '#000' : '#aaa')
9 }
10 }
11
12 build() {
13 Tabs({
14 index: this.activeIndex,
15 }) {
16 ForEach(this.toolBars, (item: ToolBarItem, index: number) => {
17 TabContent() {
18 Text(index.toString())
19 }
20 .tabBar(this.TabBarBuilder(item, index))
21 })
22 }
23 .barPosition(this.isLandscape ? BarPosition.Start : BarPosition.End)
24 .vertical(this.isLandscape)
25 .barHeight(this.isLandscape ? vp2vp(400) : vp2vp(50))
26 .onChange(index => this.activeIndex = index)
27 }
应用名称
主题颜色
1)应用名称
1{
2 "string": [
3 {
4 "name": "module_desc",
5 "value": "模块描述"
6 },
7 {
8 "name": "EntryAbility_desc",
9 "value": "前端面试宝典,包含常见面试题,和企业真实面经"
10 },
11 {
12 "name": "EntryAbility_label",
13 "value": "面试通"
14 },
15 {
16 "name": "reason",
17 "value": "获取通知权限"
18 }
19 ]
20}
2)主题颜色
1{
2 "color": [
3 {
4 "name": "start_window_background",
5 "value": "#FFFFFF"
6 },
7 {
8 "name": "green",
9 "value": "#27AE60"
10 },
11 {
12 "name": "gray",
13 "value": "#979797"
14 },
15 {
16 "name": "black",
17 "value": "#121826"
18 },
19 {
20 "name": "gray_bg",
21 "value": "#F6F7F9"
22 },
23 {
24 "name": "orange",
25 "value": "#F2994A"
26 },
27 {
28 "name": "blue",
29 "value": "#3266EE"
30 },
31 {
32 "name": "blue_bg",
33 "value": "#EDF2FF"
34 }
35 ]
36}
准备首页、项目、面经、我的四个 tabs 页面
使用 页面组件
1)准备组件
1@Component
2export struct Home {
3 build() {
4 Column(){
5 Text('首页')
6 }
7 .width('100%')
8 .height('100%')
9 }
10}
1@Component
2export struct Project {
3 build() {
4 Column(){
5 Text('项目')
6 }
7 .width('100%')
8 .height('100%')
9 }
10}
1@Component
2export struct Interview {
3 build() {
4 Column(){
5 Text('面经')
6 }
7 .width('100%')
8 .height('100%')
9 }
10}
1@Component
2export struct Mine {
3 build() {
4 Column(){
5 Text('我的')
6 }
7 .width('100%')
8 .height('100%')
9 }
10}
2)使用组件
1import { Home } from '../views/Index/Home'
2import { Project } from '../views/Index/Project'
3import { Interview } from '../views/Index/Interview'
4import { Mine } from '../views/Index/Mine'
5
6// ...
7
8 TabContent() {
9 if (index === 0) Home()
10 else if (index === 1) Project()
11 else if (index === 2) Interview()
12 else Mine()
13 }
14 .tabBar(this.TabBarBuilder(item, index))
搜索框组件
打卡组件
骨架屏组件
1)搜索框组件
1import { vp2vp } from '../utils/Basic'
2
3@Component
4export struct IvSearch {
5
6 textAlign: FlexAlign = FlexAlign.Start
7
8 build() {
9 Row() {
10 Image($r('app.media.icon_public_search'))
11 .width(vp2vp(15))
12 .aspectRatio(1)
13 .fillColor($r('app.color.gray'))
14
15 Text('请输入关键词')
16 .fontSize(vp2vp(14))
17 .fontColor($r('app.color.gray'))
18 .margin({ left: vp2vp(3) })
19 }
20 .layoutWeight(1)
21 .height(vp2vp(28))
22 .backgroundColor($r('app.color.gray_bg'))
23 .borderRadius(vp2vp(14))
24 .padding({ left: vp2vp(10), right: vp2vp(10) })
25 .justifyContent(this.textAlign)
26 }
27}
2)打卡组件
1import { vp2vp } from '../utils/Basic'
2
3@Component
4export struct IvClock {
5 @Prop
6 clockCount: number = 0
7
8 build() {
9 Stack({ alignContent: Alignment.End }) {
10 Image(this.clockCount > 0 ? $r('app.media.clocked') : $r('app.media.unclock'))
11 .objectFit(ImageFit.Fill)
12 if (this.clockCount > 0) {
13 Column() {
14 Text('已连续打卡')
15 .fontSize(8)
16 Text() {
17 Span(this.clockCount.toString())
18 .fontWeight(600)
19 .fontSize(12)
20 Span(' 天')
21 .fontSize(10)
22 }
23 }
24 .width('50')
25 } else {
26 Text('打卡')
27 .width('50')
28 .textAlign(TextAlign.Center)
29 .fontSize(vp2vp(18))
30 .fontWeight(500)
31 .fontColor('#333C4F')
32 .margin({ bottom: vp2vp(4) })
33 }
34 }
35 .width(vp2vp(74))
36 .height(vp2vp(28))
37 }
38}
1import { IvClock } from '../../common/components/IvClock'
2import { IvSearch } from '../../common/components/IvSearch'
3
4@Component
5export struct Home {
6 build() {
7 Column() {
8 Row({ space: 15 }) {
9 IvSearch()
10 IvClock()
11 }
12 .padding(15)
13
14 Text('首页')
15 }
16 .width('100%')
17 .height('100%')
18 }
19}
3)骨架屏组件
1linearGradient({
2// 角度,默认 180
3angle?: number | string,
4// 颜色,比例
5colors: Array<[ResourceColor, number]>,
6})
1import { vp2vp } from '../utils/Basic'
2
3@Component
4export struct IvSkeleton {
5 widthValue: number | string = 100
6 heightValue: number | string = 20
7 @State
8 translateValue: string = '-100%'
9
10 build() {
11 Stack() {
12 Text()
13 .backgroundColor($r('app.color.gray_bg'))
14 .height('100%')
15 .width('100%')
16 Text()
17 .linearGradient({
18 angle: 90,
19 colors: [
20 ['rgba(255,255,255,0)', 0],
21 ['rgba(255,255,255,0.5)', 0.5],
22 ['rgba(255,255,255,0)', 1]
23 ]
24 })
25 .height('100%')
26 .width('100%')
27 .translate({
28 x: this.translateValue,
29 })
30 .animation({
31 duration: 1500,
32 iterations: -1
33 })
34 }
35 .height(this.heightValue)
36 .width(this.widthValue)
37 .borderRadius(vp2vp(4))
38 .clip(true)
39 .onAppear(() => {
40 this.translateValue = '100%'
41 })
42 }
43}