一、背景说明

行酒令APP是为了练习鸿蒙开发,熟悉相关组件以及上架审核流程而开发的。本文将分享练习的相关核心代码,老手略过。

功能比较简单,先给几张首页截图。

二、项目及代码

2.1 项目代码结构

2.2 代码

  • 主Ability

import { AbilityConstant, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import EmbeddableUIAbility from '@ohos.app.ability.EmbeddableUIAbility';
import userLocal from '../base/utils/UserLocal';

export default class EntryAbility extends EmbeddableUIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    userLocal.init(this.context)
  }

  onDestroy(): void {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });

    const win = windowStage.getMainWindowSync()
    win.setWindowLayoutFullScreen(true)
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}
  • 首页pages/Index

import userLocal from '../base/utils/UserLocal';
import { APP_NAME, KEY_PRIVACY_SHOW } from '../data/Constants';
import UserPrivacyDialog from '../base/widget/UserPrivacyDialog';
import { common } from '@kit.AbilityKit';
import { TabBar } from './component/TabBar';

/**
 * Created by ZhaoHongBo on 2024/12/16.
 * Copyright (c) 2024 ZHB. All rights reserved.
 */
@Entry
@Component
struct Index {
  context = getContext(this) as common.UIAbilityContext;
  dialogController: CustomDialogController = new CustomDialogController({
    builder: UserPrivacyDialog({
      confirm: () => {
        userLocal.save(KEY_PRIVACY_SHOW, '1')
      },
      cancel: () => {
        this.context.getApplicationContext().killAllProcesses()
      }
    }),
    autoCancel: false
  })

  onPageShow(): void {
    this.showPrivacyDialog()
  }

  onPageHide(): void {
    this.dialogController.close()
  }

  showPrivacyDialog() {
    let showFlag = userLocal.get(KEY_PRIVACY_SHOW, '0')
    if (showFlag == '0') {
      this.dialogController.open()
    }
  }

  build() {
    Navigation() {
      TabBar()
    }.title(APP_NAME).titleMode(NavigationTitleMode.Full)
  }
}
  • 显示H5页面的自定义WebView

import { AtomicServiceWeb, AtomicServiceWebController, router } from '@kit.ArkUI'

@Entry
@Component
struct HBWebPage {
  @State params: object = router.getParams()
  @State url: string | Resource = ''
  @State titleStr: string = ''
  @State controller: AtomicServiceWebController = new AtomicServiceWebController()
  navPathStack: NavPathStack = new NavPathStack();

  aboutToAppear(): void {
    let urlPath: string = this.params['url']
    if (urlPath.startsWith('http')) {
      this.url = urlPath
    } else {
      this.url = $rawfile(urlPath)
    }
  }

  onBackPress(): boolean | void {
    if (this.controller.accessBackward()) {
      this.controller.backward()
    } else {
      router.back()
    }
    return true
  }

  build() {
    NavDestination() {
      AtomicServiceWeb({
        src: this.url,
        navPathStack: this.navPathStack,
        controller: this.controller,
        onControllerAttached: () => {
          this.controller.setCustomUserAgent('')
        }
      })
    }
    .onReady((context: NavDestinationContext) => {
      this.navPathStack = context.pathStack
    })
    .title(this.titleStr)
    .onClick(event => {
      if (event.target) {
        this.onBackPress()
      }
    })
    .margin({ top: 48 })
  }
}
  • 首页

import fileHelper from '../../base/utils/FileHelper'
import { common } from '@kit.AbilityKit'
import { GameBean, GameRootBean } from '../../data/bean/GameRootBean'
import { HTabBarTitle } from './HTabBarTitle'
import router from '@ohos.router'

/**
 * Created by ZhaoHongBo(zzf_soft@163.com) on 2024/12/23.
 * Copyright (c) 2024 ZHB. All rights reserved.
 */
@Component
export struct HomeView {
  context = getContext() as common.UIAbilityContext
  @State data1: GameRootBean = new GameRootBean()
  @State data2: GameRootBean = new GameRootBean()
  @State data3: GameRootBean = new GameRootBean()
  @State selectedIndex: number = 0
  tabController: TabsController = new TabsController()

  aboutToAppear(): void {
    this.data1 = JSON.parse(fileHelper.getRawFile(this.context, 'data/directive1.json')) as GameRootBean
    this.data2 = JSON.parse(fileHelper.getRawFile(this.context, 'data/directive2.json')) as GameRootBean
    this.data3 = JSON.parse(fileHelper.getRawFile(this.context, 'data/directive3.json')) as GameRootBean
  }

  build() {
    Stack() {
      Image($r('app.media.bg_game_wan')).opacity(0.1)
      Tabs({ controller: this.tabController }) {
        TabContent() {
          this.buildTxt1()
        }.tabBar(this.tabBarCustom('顺口溜', 0))

        TabContent() {
          this.buildTxt2()
        }.tabBar(this.tabBarCustom('划拳', 1))

        TabContent() {
          this.buildTxt3()
        }.tabBar(this.tabBarCustom('哥俩好', 2))

        TabContent() {
          this.buildDev()
        }.tabBar(this.tabBarCustom('开发者', 3))
      }.barBackgroundColor(Color.White).onChange(index => {
        this.selectedIndex = index
      }).onAppear(() => {
        this.tabController.changeIndex(3)
      })
    }
  }

  @Builder
  tabBarCustom(title: string, targetIndex: number) {
    HTabBarTitle({ title, targetIndex, currentTabIndex: this.selectedIndex })
  }

  @Builder
  buildTxt(item: GameBean) {
    Stack() {
      Stack() {
        Text(item.content.replaceAll(',', '\n')
          .replaceAll('。', '\n')
          .replaceAll(';', '\n')
          .replaceAll(';', '\n')
          .replaceAll('、', '\n'))
          .fontColor(Color.Black)
          .fontWeight(FontWeight.Bold)
          .fontSize(28)
          .lineHeight(50)
          .letterSpacing(12)
      }
      .backgroundColor(Color.White)
      .opacity(0.8)
      .borderRadius(8)
      .padding(16)
      .width('100%')
      .height('90%')
    }.width('90%').height('90%')
  }

  @Builder
  buildTxt1() {
    Swiper() {
      ForEach(this.data1.data, (item: GameBean, index) => {
        this.buildTxt(item)
      })
    }.vertical(true).height('100%')
  }

  @Builder
  buildTxt2() {
    Swiper() {
      ForEach(this.data2.data, (item: GameBean, index) => {
        this.buildTxt(item)
      })
    }.vertical(true).height('100%')
  }

  @Builder
  buildTxt3() {
    Swiper() {
      ForEach(this.data3.data, (item: GameBean, index) => {
        this.buildTxt(item)
      })
    }.vertical(true).height('100%')
  }

  @Builder
  buildDev() {
    Column() {
      Button('打开地图').onClick(() => {
        router.pushUrl({
          url: 'pages/MapPage'
        })
      })
    }
  }
}
  • 关于

import { SettingItemView } from '../../base/widget/SettingItemView'
import { APP_NAME, URL_PROTOCOL_PRI, URL_PROTOCOL_USE } from '../../data/Constants'
import { router } from '@kit.ArkUI'

/**
 * Created by ZhaoHongBo(zzf_soft@163.com) on 2024/12/23.
 * Copyright (c) 2024 ZHB. All rights reserved.
 */
@Component
export struct AboutView {
  build() {
    RelativeContainer() {
      Column() {
        Image($r('app.media.startIcon'))
          .width(120)
          .height(120)
          .id('login_btn')
          .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.7 })

        Text(APP_NAME)
          .height(48)
          .width('100%')
          .textAlign(TextAlign.Center)
          .fontWeight(FontWeight.Bold)
          .fontSize(18)
          .margin({ bottom: 8 })

        SettingItemView({
          attributes: {
            title: '当前版本',
            tips: 'v1.0.0'
          }
        }).margin({ top: 24 })

        SettingItemView({
          attributes: {
            title: '隐私政策',
            showArrow: true
          }
        }).margin({ top: 12 }).onClick(() => {
          router.pushUrl({
            url: 'pages/HBWebPage',
            params: {
              url: URL_PROTOCOL_PRI
            }
          })
        })

        SettingItemView({
          attributes: {
            title: '用户协议',
            showArrow: true
          }
        }).margin({ top: 12 }).onClick(() => {
          router.pushUrl({
            url: 'pages/HBWebPage',
            params: {
              url: URL_PROTOCOL_USE
            }
          })
        })

        SettingItemView({
          attributes: {
            title: '联系邮箱',
            tips: 'zzf_soft@163.com',
            showArrow: true
          }
        }).margin({ top: 12 })
      }.alignRules({
        top: { anchor: '__container__', align: VerticalAlign.Top },
        bottom: { anchor: 'copyright', align: VerticalAlign.Top }
      })

      Column() {
        Text($r('app.string.copyright')).fontSize(14).fontWeight(FontWeight.Medium).alignSelf(ItemAlign.Center)
        Text($r('app.string.icp_info')).fontSize(13).fontWeight(FontWeight.Regular)
      }.id('copyright').alignRules({
        bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
      }).align(Alignment.Center).width('100%')
    }.height('85%')
  }
}
  • 知识库

import { JSON } from '@kit.ArkTS'
import { GameBean, GameRootBean } from '../../data/bean/GameRootBean'
import { common } from '@kit.AbilityKit'
import fileHelper from '../../base/utils/FileHelper'
import { router } from '@kit.ArkUI'

/**
 * Created by ZhaoHongBo(zzf_soft@163.com) on 2024/12/23.
 * Copyright (c) 2024 ZHB. All rights reserved.
 */
@Component
export struct KnowledgeView {
  context = getContext() as common.UIAbilityContext
  @State data: GameRootBean = new GameRootBean()

  aboutToAppear(): void {
    this.data = JSON.parse(fileHelper.getRawFile(this.context, 'data/game.json')) as GameRootBean
  }

  build() {
    Stack({ alignContent: Alignment.BottomEnd }) {
      List() {
        ForEach(this.data.data, (item: GameBean, index: number) => {
          ListItem() {
            Stack() {
              Column({ space: 8 }) {
                Text(`${item.id}`)
                  .fontSize(21)
                  .padding({ top: 4, bottom: 4, left: 16 })
                  .fontWeight(FontWeight.Bold)
                  .backgroundColor($r('app.color.list_title_head_color'))
                  .borderRadius({ topLeft: 4, topRight: 4 })
                  .width('100%')
                  .textAlign(TextAlign.Start)
                Text(item.content).padding(8).fontSize(16).fontStyle(FontStyle.Italic)
              }
            }.width('100%')
          }
          .borderWidth(3)
          .borderColor($r('app.color.default_theme_color11'))
          .borderRadius(8)
          .margin(8)
        })
      }.margin({ left: 16, right: 16 }).scrollBar(BarState.Off)

      Image($r('app.media.ic_random_packages'))
        .width(48)
        .height(48)
        .borderColor($r('app.color.hb_theme_color_main'))
        .borderWidth(2)
        .borderRadius(100)
        .useEffect(true).margin(16)
        .onClick(() => {
          router.pushUrl({
            url: 'pages/GamePage'
          })
        })
    }
  }
}
  • TabBar

import { AboutView } from './AboutView'
import { HomeView } from './HomeView'
import { KnowledgeView } from './KnowledgeView'

interface BuilderParams {
  index: number //标签索引
  label: string //标签名称
  normalIcon: Resource //未选中状态图标
  selectIcon: Resource //选中状态图标
}

@Component
export struct TabBar {
  controller: TabsController = new TabsController() //tabs控制器
  @State current: number = 0 //当前tab选中项的索引

  @Builder
  tabBuilder($$: BuilderParams) {
    Column() {
      //图标
      Image(this.current === $$.index ? $$.selectIcon : $$.normalIcon).height(24).width(24).objectFit(ImageFit.Contain)
      //文字
      Text($$.label)
        .fontSize('12fp')
        .fontColor($r(this.current === $$.index ? 'app.color.hb_theme_color_main' : 'app.color.hb_theme_color_default'))
        .fontWeight(this.current === $$.index ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 3 })
    }.width('100%').onClick(() => {
      this.current = $$.index
      this.controller.changeIndex(this.current) //切换到当前页
    })
  }

  build() {
    Column() {
      Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
        TabContent() {
          HomeView()
        }.tabBar(this.tabBuilder({
          index: 0,
          label: '顺口溜',
          normalIcon: $r('app.media.bottom_tab_home_nor'),
          selectIcon: $r('app.media.bottom_tab_home_sel')
        }))

        TabContent() {
          KnowledgeView()
        }.tabBar(this.tabBuilder({
          index: 1,
          label: '小游戏',
          normalIcon: $r('app.media.bottom_tab_game_nor'),
          selectIcon: $r('app.media.bottom_tab_game_sel')
        }))

        TabContent() {
          AboutView()
        }.tabBar(this.tabBuilder({
          index: 2,
          label: '关于',
          normalIcon: $r('app.media.bottom_tab_my_nor'),
          selectIcon: $r('app.media.bottom_tab_my_sel')
        }))
      }
      .width('100%')
      .margin({ bottom: 12 })
      .barMode(BarMode.Fixed)
      .scrollable(true)
      .divider({
        color: '#dedede',
        strokeWidth: 1
      })
      .barBackgroundColor(Color.White)
      .onChange((index: number) => {
        this.current = index
      })
    }.width('100%')
  }
}
  • 通用loading

import hilog from '@ohos.hilog'

@Preview
@CustomDialog
export struct LoadingDialog {
  @Prop loadingTips: string
  @State rotateAngle: number = 0

  controller: CustomDialogController

  private tag: string = 'LoadingDialog'
  aboutToAppear() {
    hilog.debug(0xFFFF, this.tag, "Loading展示》》》》》")
  }

  build() {
    Stack() {
      Column() {
        Image($r('app.media.hb_loading_icon'))
          .height(50)
          .width(50)
          .rotate({ angle: this.rotateAngle })
          .animation({
            duration: 500,
            iterations: -1,
            curve: Curve.Friction
          })
          .onAppear(() => {
            this.rotateAngle = 360
          })
        Text(this.loadingTips)
          .fontSize('15fp')
          .margin({ top: 10 })
      }
      .justifyContent(FlexAlign.Center)
      .height(150)
      .width(150)
      .border({ radius: 5 })
    }
  }
}
  • 设置项

@Entry
@Component
export struct SettingItemView {
  @State attributes: SettingItemAttributes = new SettingItemAttributes()

  build() {
    Column() {
      Row() {
        Text(this.attributes.title).fontSize(16).margin({ left: 2 })
        Blank()
        Text(this.attributes.tips).fontSize(16).margin({ left: 8 })
        Image($r('app.media.hb_arrow_right'))
          .width(21)
          .height(21)
          .visibility(this.attributes.showArrow ? Visibility.Visible : Visibility.None)
      }
      .width('90%')
      .padding({
        left: 16,
        right: 16,
        top: 12,
        bottom: 12
      })
      .margin({ left: 8, right: 8 })
      .borderRadius(8)
      .borderStyle(BorderStyle.Solid)
      .borderColor($r('app.color.hb_item_board_color'))
      .borderWidth(1)
    }.width('100%')
  }
}

class SettingItemAttributes {
  title: string = '当前版本'
  tips?: string = 'v1.0.0'
  showArrow?: boolean = true
}
  • 隐私协议弹框

import { LengthMetrics, router } from '@kit.ArkUI'
import { URL_PROTOCOL_PRI, URL_PROTOCOL_USE } from '../../data/Constants'

/**
 * Created by ZhaoHongBo on 2024/12/9.
 * Copyright (c) 2024 ZHB. All rights reserved.
 */
@CustomDialog
export default struct UserPrivacyDialog {
  controller: CustomDialogController
  confirm: Function = () => {
  }
  cancel: Function = () => {
  }

  build() {
    Column({ space: 10 }) {
      Text() {
        Span('我已阅读并同意')
        Span($r('app.string.user_privacy_content2'))
          .fontColor($r('app.color.htitle_font_color'))
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.controller.close()
            router.pushUrl({
              url: 'pages/HBWebPage',
              params: {
                url: URL_PROTOCOL_PRI
              }
            })
          })
        Span($r('app.string.user_privacy_content3'))
        Span($r('app.string.user_privacy_content4'))
          .fontColor($r('app.color.htitle_font_color'))
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.controller.close()
            router.pushUrl({
              url: 'pages/HBWebPage',
              params: {
                url: URL_PROTOCOL_USE
              }
            })
          })
        Span(',点击以下“确定”按钮即表示同意。')
      }.fontColor('#111111').lineSpacing(LengthMetrics.vp(10)).fontSize('18fp').margin({ top: 16, bottom: 8 })

      Button('确定')
        .backgroundColor($r('app.color.hb_theme_color_main'))
        .onClick(() => {
          this.confirm()
          this.controller.close()
        }).width('90%').margin({ bottom: 8 })
    }.width('90%')
    .padding(12)
  }
}
  • 用户协议

import { router } from '@kit.ArkUI'
import { URL_PROTOCOL_PRI, URL_PROTOCOL_USE } from '../../data/Constants'

@CustomDialog
export default struct UserPrivacyDialog {
  controller: CustomDialogController
  confirm: Function = () => {
  }
  cancel: Function = () => {
  }

  build() {
    Column({ space: 10 }) {

      Text($r('app.string.user_privacy_title'))
        .fontSize(20)
        .fontWeight(FontWeight.Bold).margin({ top: 12 })

      Text() {
        Span($r('app.string.user_privacy_content1'))
        Span($r('app.string.user_privacy_content2')).fontColor($r('app.color.hb_default_hint')).onClick(() => {
          this.controller.close()
          router.pushUrl({
            url: 'pages/HBWebPage',
            params: {
              url: URL_PROTOCOL_PRI
            }
          })
        })
        Span($r('app.string.user_privacy_content3'))
        Span($r('app.string.user_privacy_content4')).fontColor($r('app.color.hb_default_hint')).onClick(() => {
          this.controller.close()
          router.pushUrl({
            url: 'pages/HBWebPage',
            params: {
              url: URL_PROTOCOL_USE
            }
          })
        })
        Span($r('app.string.user_privacy_content5'))
      }.width('100%').lineHeight(28)

      Flex({ justifyContent: FlexAlign.SpaceAround }) {
        Button($r('app.string.refuse_label'))
          .backgroundColor($r('app.color.lightest_primary_color'))
          .fontColor($r('app.color.light_gray'))
          .onClick(() => {
            this.cancel()
            this.controller.close()
          }).layoutWeight(1)

        Blank().width(18)
        Button($r('app.string.agree_label'))
          .backgroundColor($r('app.color.primary_color'))
          .fontColor($r('app.color.hb_default_hint'))
          .onClick(() => {
            this.confirm()
            this.controller.close()
          }).layoutWeight(1)
      }.margin({ bottom: 10 })
    }.width('90%')
    .padding(12)
  }
}
  • 网络请求(axios二次封装)

import { promptAction } from '@kit.ArkUI';
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from '@ohos/axios';
import { APP_NAME } from '../../data/Constants';

const baseUrl: string = "https://aihongbo.cn/japanapi/"

export interface HttpResponse<T> {
  result: string,
  data: T,
  message: string
}

export interface APIErrorType {
  result: string
  message: string,
  data: object
}

const instance = axios.create({
  baseURL: baseUrl,
  timeout: 30_000,
  readTimeout: 30_000,
  connectTimeout: 30_000
})

instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  config.headers['auth'] = APP_NAME
  return config
}, (error: AxiosError) => {
  return Promise.reject(error)
})

instance.interceptors.response.use((response: AxiosResponse) => {
  return response
}, (error: AxiosError<APIErrorType>) => {
  if (error.response?.status === 401) {
    // 删除用户信息 去登录页
    // auth.removeUser()
    // router.pushUrl({
    //   url: 'pages/LoginPage'
    // })
    promptAction.showToast({ message: '请求资源出错' })
  } else {
    promptAction.showToast({ message: `${error.response?.data || error.response?.status || '请求出错'}` })
  }
  return Promise.reject(error)
})

export type ResponseType<T> = AxiosResponse<T>

export class HNet {
  static get<T>(url: string, params?: AxiosRequestConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      instance.get<T, ResponseType<T>>(url, params).then(response => {
        let result: string = String(response.data['result'])
        if (result === '200' || result === '101' || result === '1' || result === '111') {
          resolve(response.data)
        } else {
          let apiErr: APIErrorType = {
            result: result,
            message: response.data['message'],
            data: response
          }
          reject(apiErr)
        }
      }).catch((error: AxiosError) => {
        let apiErr: APIErrorType = {
          result: '000',
          message: error.message,
          data: error.toJSON()
        }
        reject(apiErr)
      })
    })
  }

  static post<T, D>(url: string, data?: D): Promise<T> {
    return new Promise((resolve, reject) => {
      instance.post<null, ResponseType<T>, D>(url, data).then(response => {
        resolve(response.data)
      }).catch((error: AxiosError) => {
        reject(error)
      })
    })
  }

  static delete<T>(url: string, data?: AxiosRequestConfig): Promise<ResponseType<T>> {
    return instance.delete<null, ResponseType<T>>(url, data)
  }

  static put<T>(url: string, data?: object): Promise<ResponseType<T>> {
    return instance.put<null, ResponseType<T>>(url, data)
  }
}

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐