一、文档说明

本文介绍了一个基于HarmonyOS ArkUI框架开发的微信仿制应用项目。该项目实现了从登录到主界面的完整流程,包含聊天、通讯录、发现和个人中心等核心功能模块。项目特色包括组件化架构、详细代码注释和初学者友好设计。

二、项目概述

文档详细讲解了项目结构、启动流程和各功能模块的实现,如预登录页面、登录验证、主界面Tabs布局、聊天消息发送与接收等。同时提供了开发环境配置指南、学习路径建议和功能状态说明。该项目适合编程初学者学习移动应用开发,掌握HarmonyOS开发基础,理解组件化思想和UI设计原则。完整源码已开源,可作为HarmonyOS应用开发的实践参考。

三、项目特色

完整功能流程:从登录到各个功能模块的完整体验。

组件化架构:代码结构清晰,便于维护和扩展。

详细注释:每个关键代码都有详细说明。

小白友好:用最简单的语言解释复杂概念。

3.1项目结构详解
3.1.1pages页面文件——应用的不同界面

3.1.2components组件文件——可复用的UI模块

3.1.3viewmodels数据模型——数据结构定义

3.2应用启动流程
3.2.1第一步:预登录页面 (PreLogin.ets)

预登录页面是用户打开应用后看到的第一个界面,作为应用入口提供登录和注册两个主要功能选项。该页面采用简洁的UI设计,通过背景图片和按钮布局营造微信应用的熟悉感。

其中的布局设计有:

  • Stack布局:采用堆叠布局实现背景图片和按钮的层次关系。
  • ImageFit.Cover:确保背景图片全屏覆盖,保持比例。
  • 按钮布局:使用Row和Column组合实现按钮的横向排列和垂直居中。

注意:默认程序运行后的第一个界面为Index页面,因此,若想打开后的第一个界面是PreLogin需要在/entry/src/main/ets/entryability下的EntryAbility文件中修开首页面的位置,即将windowStage.loadContent('pages/Index, (err) => {}中的Index改为PreLogin即可。

预登录界面展示

3.2.2第二步:登录页面 (Login.ets)
import { promptAction } from "@kit.ArkUI";

@Entry
@Component
export struct Login{
  @State params:object=this.getUIContext().getRouter().getParams()
  @State isLoading:boolean=false;
  @State username:string="";
  @State password:string="";
  @State showError:boolean=false;

  build() {
    Column(){
      Text("微信")
        .fontColor(Color.Gray)
        .fontSize(20)
        .margin({left:-300})
        .align(Alignment.Start)
      Image($r("app.media.Yao"))//引用media文件夹中的图片资源
        .width(160)
        .height(160)
        .borderRadius(10)
        .margin({top:100})
      if(this.isLoading){
        Text("正在进入……")
          .fontColor(Color.Green)
          .fontSize(20)
          .margin({top:30})
        Text("取消")
          .fontColor("#0047AB")
          .margin({top:400})
          .onClick(()=>{
            this.isLoading=false
          })
      }else{
        TextInput({placeholder:"用户名"})
          .width("80%")
          .height(50)
          .margin(10)
          .onChange((value:string)=>{
            this.username=value;
          })
        TextInput({placeholder:"密码"})
          .width("80%")
          .height(50)
          .margin(10)
          .type(InputType.Password)
          .onChange((value:string)=>{
            this.password=value;
          })
        if(this.showError){
          Text("用户名或密码错误!")
            .fontSize(20)
            .fontColor(Color.Red)
            .margin({top:30})
          Text("请重新输入!!!")
            .fontSize(20)
            .fontColor(Color.Red)
            .margin({top:15})
        }
        Button("进入微信")
          .width("60%")
          .height(50)
          .backgroundColor("#07C160")
          .borderRadius(10)
          .fontColor(Color.White)
          .margin({top:30})
          .onClick(()=>{
            this.showError=false;
            if(this.username=="安於"&&this.password=="123456"){
              this.isLoading=true;
              setTimeout(()=>{
                this.isLoading=false;
                //页面跳转
                this.getUIContext().getRouter().pushUrl({
                  url:'pages/Index'
                })
              },2000);
            }else{
              this.showError=true;
            }
          })

        Row(){
          // 扫一扫按钮
          Button('切换账号')
            .width('45%')
            .height(40)
            .fontSize(16)
            .fontColor("#0047AB")
            .backgroundColor(Color.White)
            .borderRadius(20)
            .margin({ top: 20 })
            .onClick(() => {
              promptAction.showToast({
                message:"未实际该功能!!",
                duration:2000
              })
            })
          Button('仅传输文件')
            .width('45%')
            .height(40)
            .fontSize(16)
            .fontColor("#0047AB")
            .backgroundColor(Color.White)
            .borderRadius(20)
            .margin({ top: 20 })
            .onClick(() => {
              promptAction.showToast({
                message:"未实际该功能!!",
                duration:2000
              })
            })
        }


        /*Row(){
          Text("切换账号")
            .fontColor("#0047AB")
            .margin({top:20,right:50})
          Text("仅传输文件")
            .fontColor("#0047AB")
            .margin({top:20})
        }
        .margin({top:10})*/
      }
    }
    .width('100%')
    .height('100%')
  }
}

登录功能

  • 提供用户名和密码输入框,密码输入框采用遮罩显示。
  • 实现表单验证逻辑,当用户名输入"安於"且密码为"123456"时视为登录成功。
  • 登录成功后显示"正在进入……"加载提示,2秒后自动跳转到主页面(pages/Index)。
  • 登录失败时显示红色错误提示信息。

交互体验

  • 登录过程中可通过"取消"按钮中断操作。
  • 提供"切换账号"和"仅传输文件"两个辅助功能按钮,当前仅显示提示信息。
  • 所有按钮和交互元素均采用微信风格设计,包括绿色登录按钮和蓝色文字按钮。

其中身份验证和登录逻辑验证的实现是:

  • 进行用户身份验证。

  • 下述图片代码为登录验证逻辑。

注意:测试账号——用户名"安於",密码"123456"。

登录界面展示

3.2.3第三步:主页面 (Index.ets)

该页面是应用的核心主界面,采用底部标签页(Tabs)布局,构建了完整的微信功能框架。

import { FinderComponent } from '../components/FinderComponent'
import { MeComponent } from '../components/MeComponent'
import { NoteBookComponent } from '../components/NoteBookComponent'
import { WeChatComponent } from '../components/WeChatComponent'
@Entry
@Component
struct Index {
  @State curIndex:number=0
  @Builder buildTabBar(title:string,normalImg:Resource,selectedImg:Resource,index:number){
    Column(){
      Image(this.curIndex==index?selectedImg:normalImg)
        .width(32)
        .height(32)
        .margin({top:8})
      Text(title)
        .fontSize(16)
        .fontColor(this.curIndex==index?Color.Green:Color.Black)
        .fontWeight(500)
        .margin({
          top:2
        })
    }.height(60)
  }
  build() {
    Tabs(){
      TabContent(){
        WeChatComponent()
      }
        .tabBar(this.buildTabBar('微信',$r('app.media.wechat'),$r('app.media.wechat_selected'),0))
      TabContent(){
        NoteBookComponent()
      }
        .tabBar(this.buildTabBar('通信录',$r('app.media.notebook'),$r('app.media.notebook_selected'),1))
      TabContent(){
        FinderComponent()
      }
        .tabBar(this.buildTabBar('发现',$r('app.media.finder'),$r('app.media.finder_selected'),2))
      TabContent(){
        MeComponent()
      }
        .tabBar(this.buildTabBar('我',$r('app.media.user'),$r('app.media.user_selected'),3))
    }
    .width('100%')
    .barPosition(BarPosition.End)
    .backgroundColor('#f8f7f6')
    .onChange((index:number)=>{
      this.curIndex=index
    })
  }
}

整体架构

  • 使用Tabs组件作为容器,底部固定(BarPosition.End)的标签栏设计。
  • 页面背景色设置为浅灰色(#f8f7f6),符合微信的视觉风格。
  • 通过@State curIndex状态变量精确控制当前选中的标签页索引。

页面集成了四个核心功能模块,每个模块均封装为独立的自定义组件

  • 微信(WeChatComponent):聊天消息主界面。
  • 通信录(NoteBookComponent):联系人管理界面。
  • 发现(FinderComponent):朋友圈及扩展功能入口。
  • 我(MeComponent):个人中心与设置页面。

自定义标签栏设计

  • 采用@Builder装饰器创建了可复用的buildTabBar方法,统一管理标签栏样式。
  • 实现了图标和文字的动态切换效果:选中状态:图标高亮,文字显示为绿色;未选中状态:图标默认,文字显示为黑色。

交互逻辑

  • 通过onChange事件监听标签页切换,实时更新curIndex状态。
  • 状态变化驱动UI更新,确保图标和文字颜色的同步切换。
  • 组件化设计使得各功能模块高度解耦,便于独立开发和维护。

主页面界面展示

3.3核心功能模块详解
3.3.1. 微信模块 (WeChatComponent + ChatPage)
  1. 聊天列表页面 (WeChatComponent.ets)

该页面实现了微信的核心聊天列表界面,复刻了微信消息会话的主要功能与交互体验。

import { MessageModel } from "../viewmodels/MessageModel"
import { NavTileBar } from "./NavTitleBar"
import { router } from "@kit.ArkUI"

@Component
export struct WeChatComponent{
  @State messages: MessageModel[] = [
    {
      avatar:$r('app.media.GuiGui'),
      nickName:'a闺蜜٩(*´◐`*)۶',
      message:'但是我们有课社呀!!',
      time:'10:58'
    },
    {
      avatar:$r('app.media.Yao'),
      nickName:'安於(*^ω^*)',
      message:'[聊天记录]',
      time:'11:33'
    },
    {
      avatar: $r('app.media.Mother'),
      nickName: '妈妈',
      message: '[视频时长 56:16]',
      time: '12:36'
    },
    {
      avatar: $r('app.media.Father'),
      nickName: '爸爸',
      message: '今天这边出太阳了',
      time: '15:20'
    },
    {
      avatar: $r('app.media.GrandMa'),
      nickName: '奶奶',
      message: '[视频时长 46:36]',
      time: '19:30'
    },
    {
      avatar: $r('app.media.RoXi'),
      nickName: '荣茜',
      message: '天气这么好,周末出门去公园吧!!!!',
      time: '昨天 18:30'
    },
    {
      avatar: $r('app.media.Qian'),
      nickName: '颜纤儿',
      message: '好滴好滴!!',
      time: '昨天 15:25'
    },
    {
      avatar: $r('app.media.A_ran'),
      nickName: '阿冉',
      message: 'ok ok!!',
      time: '昨天 12:11'
    },
    {
      avatar: $r('app.media.Cao'),
      nickName: '校草',
      message: '[视频号] 山东绳结达人的视频',
      time: '昨天 08:47'
    },
    {
      avatar: $r('app.media.Han'),
      nickName: '怡晗',
      message: '收到!!',
      time: '11月8日'
    },
    {
      avatar: $r('app.media.Yu'),
      nickName: '小鱼',
      message: '哈哈哈哈哈!!',
      time: '11月7日'
    },
    {
      avatar: $r('app.media.Qi'),
      nickName: '伊琦',
      message: '[视频时长 01:36]',
      time: '11月6日'
    }
  ]

  @Builder buildMessage(message: MessageModel, index: number) {
    ListItem() {
      Row() {
        // 聊天内容
        Row() {
          Image(message.avatar)
            .width(40)
            .height(40)
            .borderRadius(5)
            .margin({ left: 10 })

          Column() {
            Text(message.nickName)
              .fontSize(14)
              .fontWeight(500)
            Text(message.message)
              .fontSize(12)
              .fontColor(Color.Grey)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 10 })

          Column() {
            Text(message.time)
              .fontColor(Color.Black)
              .fontSize(12)
              .opacity(0.4)
              .margin({ right: 10 })
          }
          .width(80)
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .justifyContent(FlexAlign.Start)
        .onClick(() => {
          // 点击进入聊天页面
          this.enterChatPage(message)
        })

        // 删除按钮
        Row() {
          Button('删除')
            .width(80)
            .height(60)
            .backgroundColor(Color.Red)
            .fontColor(Color.White)
            .onClick(() => {
              this.deleteMessage(index)
            })
        }
        .width(80)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .height(60)
    }
    .swipeAction({ end: this.buildDeleteButton(index) })
  }

  @Builder buildDeleteButton(index: number) {
    Button('删除')
      .width(80)
      .height(60)
      .backgroundColor(Color.Red)
      .fontColor(Color.White)
      .onClick(() => {
        this.deleteMessage(index)
      })
  }

  // 进入聊天页面
  private enterChatPage(message: MessageModel) {
    try {
      router.pushUrl({
        url: 'pages/ChatWrapperPage',
        params: {
          avatar: message.avatar,
          nickName: message.nickName,
          messages: [],// 可以传入历史消息
        }
      })
      console.log('路由跳转成功')
    } catch (error) {
      console.error('路由跳转失败:', error)
    }
  }

  // 删除消息
  private deleteMessage(index: number) {
    this.messages.splice(index, 1)
  }

  build() {
    Column() {
      NavTileBar({ title: '微信'})
      List() {
        ForEach(this.messages, (message: MessageModel, index: number) => {
          this.buildMessage(message, index)
        })
      }
      .width('100%')
      .layoutWeight(1)
      .divider({
        strokeWidth: 2,
        startMargin: 60
      })
    }


    .width('100%')


    .height('100%')
    .backgroundColor(Color.White)
  }
}

界面布局

  • 顶部采用自定义的NavTileBar组件,显示"微信"标题。
  • 主体部分使用List组件展示消息列表,每条消息间通过细分割线区分。

消息数据模型

  • 通过MessageModel结构化定义消息数据,包含头像、昵称、消息内容和时间等关键字段。
  • 预置了12条模拟数据,涵盖好友聊天、家人对话、视频消息、视频号分享等多种消息类型。

高级交互功能

  • 实现了左滑删除操作,用户可通过swipeAction向左滑动列表项显示红色删除按钮。
  • 删除操作通过splice方法直接从messages数组中移除对应项,实现UI的实时更新。

其中主要的模块有:

  • 在该页面中显示最近的聊天记录(预制的)。

  • 点击任意列表项可通过router.pushUrl方法跳转到对应的聊天详情页(ChatWrapperPage),并传递联系人信息

  • 通过enterChatPage方法进入到ChatWapperPage页面中使用ChatPage接口。

删除界面展示

  1. 聊天页面 (ChatPage.ets)

该页面实现了一个完整的即时聊天界面,为用户提供了与联系人进行实时消息交互的功能。

import { promptAction, router } from '@kit.ArkUI'
// 导出接口供其他文件使用
export interface ChatMessage {
  id: string
  content: string
  time: string
  isSelf: boolean
}

export interface ChatParams {
  avatar: Resource
  nickName: string
  messages?: ChatMessage[]
}

@Component
export struct ChatPage {
  @Prop params: ChatParams = {} as ChatParams
  @State avatar: Resource = $r('app.media.Yao')
  @State nickName: string = ''
  @State inputText: string = ''
  @State messages: ChatMessage[] = [
    {
      id: '1',
      content: '你好!',
      time: '10:30',
      isSelf: false
    },
    {
      id: '2',
      content: '你好,最近怎么样?',
      time: '10:31',
      isSelf: true
    },
    {
      id: '3',
      content: '还不错,你呢?',
      time: '10:32',
      isSelf: false
    }
  ]

  aboutToAppear(): void {
    if (this.params) {
      this.avatar = this.params.avatar || $r('app.media.Yao')
      this.nickName = this.params.nickName || '好友'
    }
  }

  // 发送消息
  private sendMessage() {
    if (this.inputText.trim() === '') {
      return
    }

    const newMessage: ChatMessage = {
      id: Date.now().toString(),
      content: this.inputText,
      time: this.getCurrentTime(),
      isSelf: true
    }

    this.messages.push(newMessage)

    this.inputText = ''

    // 模拟对方回复
    setTimeout(() => {
      const replyMessage: ChatMessage = {
        id: (Date.now() + 1).toString(),
        content: '收到你的消息了!该功能已成功实现!!',
        time: this.getCurrentTime(),
        isSelf: false
      }
      this.messages.push(replyMessage)
    }, 1000)
  }

  // 获取当前时间
  private getCurrentTime(): string {
    const now = new Date()
    const hours = now.getHours().toString().padStart(2, '0')
    const minutes = now.getMinutes().toString().padStart(2, '0')
    return `${hours}:${minutes}`
  }

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Image($r('app.media.arrow_back'))
          .width(24)
          .height(24)
          .margin({ left: 16 })
          .onClick(() => {
            router.back()
          })

        Image(this.avatar)
          .width(32)
          .height(32)
          .borderRadius(16)
          .margin({ left: 12 })

        Text(this.nickName)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .margin({ left: 8 })
          .layoutWeight(1)

        Button() {
          Image($r('app.media.more'))
            .width(24)
            .height(24)
        }
        .width('20%')
        .height(40)
        .fontSize(14)
        .fontColor("#666666")
        .backgroundColor('#f8f8f8')
        .borderRadius(20)
        .margin({right:6})
        .onClick(() => {
          promptAction.showToast({
            message:"未实现该功能!!",
            duration:2000
          })
        })
      }
      .width('100%')
      .height(56)
      .backgroundColor('#f8f8f8')
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(VerticalAlign.Center)

      // 聊天消息列表
      List() {
        ForEach(this.messages, (message: ChatMessage) => {
          ListItem() {
            Row() {
              if (message.isSelf) {
                // 自己的消息
                Column() {
                  Text(message.content)
                    .fontSize(16)
                    .fontColor(Color.White)
                    .backgroundColor('#07C160')
                    .borderRadius(8)
                    .padding(12)
                    .width('80%')
                }
                .alignItems(HorizontalAlign.End)
                .layoutWeight(1)
                .margin({ right: 12 })

                Text(message.time)
                  .fontSize(12)
                  .fontColor(Color.Gray)
                  .margin({ right: 16 })
              } else {
                // 对方的消息
                Text(message.time)
                  .fontSize(12)
                  .fontColor(Color.Gray)
                  .margin({ left: 16 })

                Column() {
                  Text(message.content)
                    .fontSize(16)
                    .fontColor(Color.Black)
                    .backgroundColor(Color.White)
                    .borderRadius(8)
                    .padding(12)
                    .width('80%')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 12 })
              }
            }
            .width('100%')
            .justifyContent(message.isSelf ? FlexAlign.End : FlexAlign.Start)
            .margin({ top: 8, bottom: 8 })
          }
        })
      }
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#f5f5f5')

      // 输入区域
      Row() {


        Button() {
          Image($r('app.media.speech'))
            .width(24)
            .height(24)
        }
        .width(25)
        .height(40)
        .fontSize(14)
        .backgroundColor('#f8f8f8')
        .margin({left: 16})
        .onClick(() => {
          promptAction.showToast({
            message:"未实现该功能!!",
            duration:2000
          })
        })

        TextInput({ placeholder: '请输入消息...' })
          .width(0)
          .layoutWeight(1)
          .height(40)
          .margin({ left: 12, right: 12 })
          .backgroundColor(Color.White)
          .borderRadius(20)
          .onChange((value: string) => {
            this.inputText = value
          })

        Button('发送')
          .width(60)
          .height(36)
          .fontSize(14)
          .fontColor(Color.White)
          .backgroundColor('#07C160')
          .margin({ right: 16 })
          .onClick(() => {
            this.sendMessage()
          })
      }
      .width('100%')
      .height(60)
      .backgroundColor('#f8f8f8')
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f5f5')
  }
}

页面初始化与参数传递

  • 通过@Prop params接收从列表页传递的联系人信息(ChatParams接口),包括头像和昵称。
  • 在aboutToAppear生命周期钩子中初始化页面状态,确保正确显示联系人信息。

顶部导航栏

  • 采用自定义布局,集成了返回按钮、联系人头像、昵称和更多操作按钮。
  • 返回按钮通过router.back()实现页面回退功能。
  • 更多操作按钮当前仅显示提示信息,为后续功能扩展预留接口。

消息列表展示

  • 使用List组件渲染聊天记录,通过ForEach遍历messages数组。
  • 实现了消息气泡的差异化显示:自己的消息:右对齐,绿色背景(#07C160),白色文字;对方消息:左对齐,白色背景,黑色文字。
  • 每条消息均附带时间戳,采用灰色小字体显示。

消息发送与模拟回复

  • 底部输入区域包含语音按钮、文本输入框和发送按钮。
  • 发送按钮触发sendMessage方法,将输入框内容封装为新的消息对象并添加到列表。
  • 实现了自动回复机制:发送消息1秒后,系统会自动添加一条预设的回复消息,模拟真实聊天场景。

辅助功能

  • getCurrentTime方法自动生成当前时间,确保消息时间戳的准确性。
  • 语音按钮和更多选项按钮当前显示"未实现该功能"提示,保持了界面完整性。

其中发送和收到消息的功能模块实现:

  • 发送消息功能

  • 模拟对方回复

聊天页面展示

3.3.2. 通讯录模块 (NoteBookComponent)

该页面实现了一个功能完备的通讯录界面,提供了高效的联系人查找与管理功能,高度还原了微信通讯录的核心体验。

import { ContactGroupModel } from '../viewmodels/ContactGroupModel'
import { ContactModel } from '../viewmodels/ContactModel'
import { NavTileBar } from './NavTitleBar'
import { promptAction, router } from '@kit.ArkUI'
import { contact } from '@kit.ContactsKit'

@Component
export struct NoteBookComponent{

  @State contacts:ContactGroupModel[]=[
    {
      title:'A',
      contacts:[
        {
          avatar:$r('app.media.GuiGui'),
          nickName:'a闺蜜٩(*´◐`*)۶'
        },
        {
          avatar:$r('app.media.Yao'),
          nickName:'安於(*^ω^*)'
        },
        {
          avatar:$r('app.media.A_ran'),
          nickName:'阿冉'
        }
      ]
    },
    {
      title:'B',
      contacts:[
        {
          avatar:$r('app.media.Father'),
          nickName:'爸爸'
        }
      ]
    },
    {
      title:'C',
      contacts:[
        {
          avatar:$r('app.media.ChenGuo'),
          nickName:'陈果'
        }
      ]
    },
    {
      title:'D',
      contacts:[
        {
          avatar:$r('app.media.DiDi'),
          nickName:'弟弟'
        }
      ]
    },
    {
      title:'E',
      contacts:[
        {
          avatar:$r('app.media.ErJiuYe'),
          nickName:'二舅爷'
        }
      ]
    },
    {
      title:'F',
      contacts:[
        {
          avatar:$r('app.media.FengXiang'),
          nickName:'凤祥(∗ᵒ̶̶̷̀ω˂̶́∗)੭₎'
        }
      ]
    },
    {
      title:'J',
      contacts:[
        {
          avatar:$r('app.media.JiaHui'),
          nickName:'佳绘'
        }
      ]
    },
    {
      title:'L',
      contacts:[
        {
          avatar:$r('app.media.LiuSiTong'),
          nickName:'刘思彤'
        },
        {
          avatar:$r('app.media.LuJiaXin'),
          nickName:'吕嘉欣'
        }
      ]
    },
    {
      title:'M',
      contacts:[
        {
          avatar:$r('app.media.Mother'),
          nickName:'妈妈'
        }
      ]
    },
    {
      title:'N',
      contacts:[
        {
          avatar:$r('app.media.GrandMa'),
          nickName:'奶奶'
        }
      ]
    },
    {
      title:'Q',
      contacts:[
        {
          avatar:$r('app.media.Qi'),
          nickName:'伊琦'
        }
      ]
    },
    {
      title:'R',
      contacts:[
        {
          avatar:$r('app.media.RoXi'),
          nickName:'荣茜'
        }
      ]
    },
    {
      title:'S',
      contacts:[
        {
          avatar:$r('app.media.SongLiXiang'),
          nickName:'宋理想'
        }
      ]
    },
    {
      title:'W',
      contacts:[
        {
          avatar:$r('app.media.WenHan'),
          nickName:'雯涵(* ⁰̷̴͈꒨⁰̷̴͈)'
        },
        {
          avatar:$r('app.media.WeiBaoYi'),
          nickName:'魏宝仪'
        }
      ]
    },
    {
      title:'X',
      contacts:[
        {
          avatar:$r('app.media.Cao'),
          nickName:'校草'
        },
        {
          avatar:$r('app.media.Yu'),
          nickName:'小鱼'
        },
        {
          avatar:$r('app.media.XieHuiXin'),
          nickName:'谢慧欣'
        }
      ]
    },
    {
      title:'Y',
      contacts:[
        {
          avatar:$r('app.media.Qian'),
          nickName:'颜纤儿'
        },
        {
          avatar:$r('app.media.Han'),
          nickName:'怡晗'
        },
        {
          avatar:$r('app.media.YiRuoYu'),
          nickName:'尹若羽'
        }
      ]
    }
  ]
  @State letters:string[]=['A','B','C','D','E','F','G','H','I','J','K',
    'L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
  private listScroller:Scroller=new Scroller()

  private enterChatPage(contact: ContactModel) {
    try {
      router.pushUrl({
        url: 'pages/ChatWrapperPage',
        params: {
          avatar: contact.avatar,
          nickName: contact.nickName,
          messages: []
        }
      })
      console.log('从通讯录进入聊天页面成功:', contact.nickName)
    } catch (error) {
      console.error('从通讯录进入聊天页面失败:', error)
    }
  }


  @Builder buildListItem(image:Resource,title:string,   contact?: ContactModel){
    Row(){
      Image(image)
        .width(40)
        .height(40)
        .borderRadius(5)
        .margin({
          left:10
        })
      Text(title)
        .margin({
          left:10
        })
    }.width('100%')
    .height(60)
    .backgroundColor(Color.White)
    .alignItems(VerticalAlign.Center)
    .onClick(() => {
      // 只有联系人项目才添加点击事件
      if (contact) {
        this.enterChatPage(contact)
      }
    })


  }
  @Builder buildListItemGroupHeader(title:string){
    Row(){
      Text(title)
        .fontSize(16)
        .fontWeight(600)
        .margin({
          left:10
        })
    }.width('100%')
    .height(30)
  }
  build() {
    Stack({ alignContent: Alignment.End }) {
      Column() {
        NavTileBar({ title: '通讯录' })
        List() {
          ListItem() {
            this.buildListItem($r('app.media.newfriend'), '新的朋友')
          }

          ListItem() {
            this.buildListItem($r('app.media.onlychatfriend'), '仅聊天的朋友')
          }

          ListItem() {
            this.buildListItem($r('app.media.groupchat'), '群聊')
          }

          ListItem() {
            this.buildListItem($r('app.media.lable'), '标签')
          }

          ListItem() {
            this.buildListItem($r('app.media.gzh'), '公众号')
          }
        }.width('100%')
        .height(300)
        .divider({
          strokeWidth: 2,
          startMargin: 10
        })

        List({ scroller: this.listScroller }) {
          ForEach(this.contacts, (contactGroup: ContactGroupModel) => {
            ListItemGroup({ header: this.buildListItemGroupHeader(contactGroup.title) })
            ForEach(contactGroup.contacts, (contact: ContactModel) => {
              ListItem() {
                this.buildListItem(contact.avatar, contact.nickName, contact)
              }
            })
          })
        }
        .animation({
          curve: Curve.Linear,
          duration: 1000,
          playMode: PlayMode.Normal
        })
        .backgroundColor('#bbbab9')
        .divider({
          strokeWidth: 2,
          startMargin: 10
        })
        .sticky(StickyStyle.Header)
        .width('100%')
        .layoutWeight(1)
      }.width('100%')
      .height('100%')

      AlphabetIndexer({
        arrayValue: this.letters,
        selected: 0
      })
        .itemSize(20)
        .margin({
          top: 100
        })
        .onSelect((index: number) => {
          const letter = this.letters[index]
          for (let i = 0; i < this.contacts.length; i++) {
            const contactGroup = this.contacts[i]
            if (letter === contactGroup.title) {
              this.listScroller.scrollToIndex(i)
            }
          }
        })

    }.height('100%')
    .width('100%')
  }
}

整体布局结构

  • 页面采用Stack布局,主体为Column,右侧悬浮字母索引器。
  • 顶部使用NavTileBar组件显示"通讯录"标题。
  • 页面分为两个主要区域:上方的功能入口列表和下方的联系人分组列表。

交互与导航
点击任意联系人,通过router.pushUrl方法跳转到聊天详情页,并传递该联系人的头像和昵称参数。

字母索引功能

  • 右侧集成了AlphabetIndexer字母索引器,提供A-Z的快速定位功能
  • 点击索引器字母时,通过onSelect事件和listScroller.scrollToIndex方法,实现联系人列表的快速滚动定位。

联系人分组显示

  • 联系人数据按首字母(A-Z)进行分组存储,每个分组包含标题和联系人列表。
  • 使用ListItemGroup构建分组结构,并通过sticky(StickyStyle.Header)实现分组标题的吸顶效果。
  • 每个联系人项显示头像和昵称,点击可跳转到对应的聊天页面。

通讯录页面展示

3.3.3. 发现模块 (FinderComponent)

该页面构建了微信的"发现"功能入口界面,以列表形式集中展示了各类扩展功能和服务。

import { NavTileBar } from "./NavTitleBar"
import { promptAction } from "@kit.ArkUI"

@Component
export struct FinderComponent{
  @Builder
  buildListItem(icon: Resource, title: string, hasArrow: boolean = true) {
    Row() {
      Image(icon)
        .width(24)
        .height(24)
        .margin({ left: 16, right: 12 })

      Text(title)
        .fontSize(16)
        .fontColor(Color.Black)
        .layoutWeight(1)

      if (hasArrow) {
        Image($r("app.media.arrow_right"))
          .width(16)
          .height(16)
          .margin({ right: 16 })
          .fillColor('#C8C8C8')
      }
    }
    .width('100%')
    .height(50)
    .backgroundColor(Color.White)
    .onClick(() => {
      this.showUnimplementedToast()
    })
  }

  build() {
    Column(){
      NavTileBar({title:'发现'})
      List() {
        // 第一组:社交和内容功能
        ListItem() {
          this.buildListItem($r('app.media.find_moments'), '朋友圈')
        }

        ListItem() {
          this.buildListItem($r('app.media.video'), '视频号')
        }

        ListItem() {
          this.buildListItem($r('app.media.live'), '直播')
        }

        // 第二组:工具功能
        ListItem() {
          this.buildListItem($r('app.media.scan'), '扫一扫')
        }

        ListItem() {
          this.buildListItem($r('app.media.music'), '听一听')
        }

        ListItem() {
          this.buildListItem($r('app.media.look'), '看一看')
        }

        ListItem() {
          this.buildListItem($r('app.media.search'), '搜一搜')
        }

        // 第三组:附近和娱乐
        ListItem() {
          this.buildListItem($r('app.media.nearby'), '附近')
        }

        ListItem() {
          this.buildListItem($r('app.media.game'), '游戏')
        }

        ListItem() {
          this.buildListItem($r('app.media.miniprogram'), '小程序')
        }
      }
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#f8f7f6')
      .divider({
        strokeWidth: 8,
        color: '#f8f7f6',
        startMargin: 0,
        endMargin: 0
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f8f7f6')
  }

  // 显示未实现功能的提示
  private showUnimplementedToast() {
    promptAction.showToast({
      message: `该功能未实现!!`,
      duration: 2000
    })
  }



}

整体布局设计

  • 页面顶部采用NavTileBar组件,固定显示"发现"标题。
  • 主体使用List组件垂直排列所有功能入口,背景色为浅灰色(#f8f7f6)。

功能模块分类
页面将多个功能入口划分为三个逻辑分组,覆盖了社交、内容、工具和娱乐等多个维度:

  • 社交与内容核心:包括"朋友圈"、“视频号"和"直播”,是用户进行社交互动和内容消费的主要入口。
  • 实用工具集:整合了"扫一扫"、“听一听”、"看一看"和"搜一搜"等高频使用的工具功能。
  • 周边与娱乐:提供"附近"、"游戏"和"小程序"等本地化服务和轻应用入口。

交互逻辑

  • 所有功能项均绑定了统一的点击事件,触发showUnimplementedToast方法。
  • 点击后会弹出"该功能未实现!!"的提示信息,持续时间为2秒。
  • 这种设计保持了界面的完整性,为后续功能开发预留了清晰的接口。

  • 显示未实现提示

发现页面展示

3.3.4. 个人中心模块 (MeComponent)

该页面是应用的用户个人中心,集成了个人信息展示、状态管理和各项服务的入口,是用户进行个人设置和查看的核心界面。

import { NavTileBar } from "./NavTitleBar"
import { promptAction, router } from "@kit.ArkUI";

// 定义用户信息接口
interface UserInfo {
  avatar: Resource;
  nickname: string;
  wechatId: string;
  qrCode: Resource;
}

@Component
export struct MeComponent{


  @State userInfo: UserInfo = {
    avatar: $r('app.media.Yao'),
    nickname: '安於',
    wechatId: 'Cmy66666888888',
    qrCode: $r("app.media.qr_icon")
  }

  @Builder
  buildFunctionItem(icon: Resource, title: string, badge?: string) {
    Row() {
      Row() {
        Image(icon)
          .width(24)
          .height(24)
          .margin({ right: 12 })

        Text(title)
          .fontSize(16)
          .fontColor(Color.Black)
          .fontWeight(500)
      }
      .layoutWeight(1)

      Row() {
        if (badge) {
          Text(badge)
            .fontSize(12)
            .fontColor(Color.Red)
            .margin({ right: 8 })
        }

        Image($r('app.media.arrow_right'))
          .width(16)
          .height(16)
          .fillColor('#C8C8C8')
      }
    }
    .width('100%')
    .height(50)
    .padding({ left: 16, right: 16 })
    .backgroundColor(Color.White)
    .onClick(() => {
      this.showUnimplementedToast()
    })
  }

  build() {
    Column(){
      NavTileBar({title:'我'})
      // 用户信息区域
      Column() {
        Row() {
          // 头像
          Image(this.userInfo.avatar)
            .width(64)
            .height(64)
            .borderRadius(5)
            .margin({ left: 16, right: 12 })

          // 用户信息
          Column() {
            Text(this.userInfo.nickname)
              .fontSize(18)
              .fontColor(Color.Black)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 4 })

            Text('微信号: ' + this.userInfo.wechatId)
              .fontSize(14)
              .fontColor('#666666')
              .margin({top:6})
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          // 二维码图标
          Image(this.userInfo.qrCode)
            .width(24)
            .height(24)
            .margin({ right: 16 })
            .onClick(() => {
              // 显示二维码弹窗
              this.showQRCode()
            })

          // 箭头图标
          Image($r('app.media.arrow_right'))
            .width(16)
            .height(16)
            .fillColor('#C8C8C8')
            .margin({ right: 16 })
            .onClick(() => {
              // 点击箭头跳转到个人资料页面
              this.enterProfilePage()
            })

        }
        .width('100%')
        .height(90)
        .padding({ top: 16, bottom: 16 })
        .backgroundColor(Color.White)

        // 状态栏
        Row() {
          Button('+ 状态')
            .fontSize(14)
            .fontColor(Color.Black)
            .backgroundColor(Color.Transparent)
            .border({ width: 1, color: '#E5E5E5' })
            .borderRadius(40)
            .padding({ left: 16, right: 16, top: 6, bottom: 6 })
            .margin({ left: 87, right: 8 })
            .onClick(() => {
              promptAction.showToast({
                message:"功能未实现!!",
                duration:2000
              })
            })

          Image($r('app.media.refresh'))
            .width(20)
            .height(20)
            .fillColor('#666666')
            .margin({ right: 16 })
            .onClick(() => {
              promptAction.showToast({
                message:"已刷新!!",
                duration:2000
              })
            })
        }
        .width('100%')
        .height(50)
        .backgroundColor(Color.White)
        .justifyContent(FlexAlign.Start)
        .alignItems(VerticalAlign.Center)
      }

      // 功能列表
      List() {
        ListItem() {
          this.buildFunctionItem($r('app.media.service'), '服务')
        }

        ListItem() {
          this.buildFunctionItem($r('app.media.favorite'), '收藏')
        }

        ListItem() {
          this.buildFunctionItem($r('app.media.moments'), '朋友圈')
        }

        ListItem() {
          this.buildFunctionItem($r('app.media.card_package'), '卡包')
        }

        ListItem() {
          this.buildFunctionItem($r('app.media.emoji'), '表情')
        }

        ListItem() {
          this.buildFunctionItem($r('app.media.settings'), '设置')
        }
      }
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#f8f7f6')
      .divider({
        strokeWidth: 8,
        color: '#f8f7f6',
        startMargin: 0,
        endMargin: 0
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f8f7f6')
  }

  private showQRCode() {
    this.getUIContext().getRouter().pushUrl({
      url: 'pages/QRCodePage',
      params: {
        nickname: this.userInfo.nickname,
        wechatId: this.userInfo.wechatId,
        avatar: this.userInfo.avatar
      }
    })
  }




  // 显示未实现功能的提示
  private showUnimplementedToast() {
    promptAction.showToast({
      message: `该功能未实现!!`,
      duration: 2000
    })
  }


  // 新增方法:跳转到个人资料页面
  private enterProfilePage() {
    try {
      router.pushUrl({
        url: 'pages/ProfilePage',
        params: {
          avatar: this.userInfo.avatar,
          name: this.userInfo.nickname,
          wechatId: this.userInfo.wechatId,
          gender: '女',
          region: '郑州 中原区',
          phone: '138****8888',
          signature: '不是成魔就是成功೭(˵ˉꇴˉ˵)౨ ',
          ringtone: '勋章',
          address: '郑州市中原区中原中路41号',
          invoiceTitle: '个人',
          wechatBeans: '666'
        }
      })
      console.log('跳转到个人资料页面成功')
    } catch (error) {
      console.error('跳转到个人资料页面失败:', error)
    }
  }



}

用户信息展示区

  • 页面顶部显著展示用户的个人资料,包括64x64像素的圆角头像、昵称和微信号。
  • 通过UserInfo接口结构化管理用户数据,确保了数据的一致性和可维护性。
  • 在用户信息右侧集成了二维码和箭头两个交互图标,分别用于展示个人二维码和跳转到详细资料页。

状态栏功能

  • 在用户信息下方提供了一个轻量级的状态栏,包含"+ 状态"按钮和刷新图标。
  • "+ 状态"按钮采用边框设计,为用户发布个人状态预留了功能入口。
  • 刷新图标点击后会提示"已刷新",为动态内容更新提供了即时反馈。

功能列表入口

  • 主体部分采用List组件,集成了"服务"、“收藏”、“朋友圈”、“卡包”、"表情"和"设置"六个核心功能入口。
  • 通过@Builder装饰器创建了可复用的buildFunctionItem方法,统一了列表项的样式和交互逻辑。
  • 每个列表项均采用图标+文字的组合形式,右侧配有灰色箭头指示符,保持了微信的设计语言。

页面导航与交互

  • 实现了两个关键的页面跳转功能:点击二维码图标,通过router.pushUrl跳转到QRCodePage,并传递用户基本信息;点击箭头图标,跳转到ProfilePage个人资料页,并传递更详细的用户资料。
  • 所有其他功能项点击后均显示"该功能未实现"的提示,为后续开发预留了清晰的扩展点。

我的页面展示

3.3.5个人资料页面(ProfilePage)

该页面是一个用于展示和查看用户详细个人信息的界面。

import { router } from '@kit.ArkUI'

interface ProfileInfo {
  avatar: Resource
  name: string
  gender: string
  region: string
  phone: string
  wechatId: string
  signature: string
  ringtone: string
  address: string
  invoiceTitle: string
  wechatBeans: string
}

@Entry
@Component
struct ProfilePage {
  @State profileInfo: ProfileInfo = {
    avatar: $r('app.media.Yao'),
    name: '安於',
    gender: '女',
    region: '郑州 中原区',
    phone: '138****8888',
    wechatId: 'Cmy66666888888',
    signature: '不是成魔就是成功೭(˵ˉꇴˉ˵)౨ ',
    ringtone: '勋章',
    address: '郑州市中原区中原中路41号',
    invoiceTitle: '个人',
    wechatBeans: '666'
  }

  aboutToAppear(): void {
    try {
      const params:ProfileInfo = router.getParams() as ProfileInfo
      if (params) {
        this.profileInfo = {
          avatar: params.avatar || $r('app.media.Yao'),
          name: params.name || '安於',
          gender: params.gender || '女',
          region: params.region || '郑州 中原区',
          phone: params.phone || '138****8888',
          wechatId: params.wechatId || 'Cmy66666888888',
          signature: params.signature || '不是成魔就是成功೭(˵ˉꇴˉ˵)౨ ',
          ringtone: params.ringtone || '勋章',
          address: params.address || '郑州市中原区中原中路41号',
          invoiceTitle: params.invoiceTitle || '个人',
          wechatBeans: params.wechatBeans || '666'
        }
      }
    } catch (error) {
      console.error('获取个人资料失败:', error)
    }
  }

  @Builder ProfileItem(title: string, value: string, showArrow: boolean = true) {
    Row() {
      Text(title)
        .fontSize(16)
        .fontColor('#333333')
        .width(100)

      Text(value)
        .fontSize(16)
        .fontColor('#666666')
        .layoutWeight(1)
        .textAlign(TextAlign.End)
        .margin({ right: 8 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      if (showArrow) {
        Image($r('app.media.arrow_right'))
          .width(16)
          .height(16)
          .fillColor('#c8c8c8')
          .margin({ right: 16 })
      }
    }
    .width('100%')
    .height(50)
    .backgroundColor(Color.White)
    .padding({ left: 16 })
    .justifyContent(FlexAlign.SpaceBetween)
  }

  // 控制头像弹窗显示状态
  @State showAvatarDialog: boolean = false
  // 头像弹窗构建器
  @Builder AvatarDialog() {
    Column() {
      // 弹窗内容
      Column() {
        // 关闭按钮
        Row() {
          Blank()
          Image($r('app.media.close'))
            .width(24)
            .height(24)
            .fillColor('#666666')
            .onClick(() => {
              this.showAvatarDialog = false
            })
        }
        .width('100%')
        .margin({ bottom: 20 })
        // 放大的头像
        Image(this.profileInfo.avatar)
          .width(280)
          .height(280)
          .borderRadius(8)
          .objectFit(ImageFit.Contain)
        // 用户信息
        Text(this.profileInfo.name)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .margin({ top: 20, bottom: 8 })

        Text('微信号: ' + this.profileInfo.wechatId)
          .fontSize(14)
          .fontColor('#666666')
          .margin({ bottom: 30 })
      }
      .padding(20)
      .backgroundColor(Color.White)
      .borderRadius(12)
      .margin({left:20,right:20})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0, 0, 0, 0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      // 点击背景关闭弹窗
      this.showAvatarDialog = false
    })
  }

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Image($r('app.media.arrow_back'))
          .width(24)
          .height(24)
          .margin({ left: 16 })
          .onClick(() => {
            router.back()
          })

        Text('个人资料')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .margin({ left: 112 })
          .layoutWeight(1)
      }
      .width('100%')
      .height(56)
      .backgroundColor('#f8f8f8')
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)

      Scroll() {
        Column() {
          // 头像区域
          Row() {
            Text('头像')
              .fontSize(16)
              .fontColor('#333333')
              .width(100)

            Image(this.profileInfo.avatar)
              .width(60)
              .height(60)
              .borderRadius(5)
              .margin({ left: 126 })

              .onClick(()=>{
                // 点击头像显示弹窗
                this.showAvatarDialog = true
              })

            Image($r('app.media.arrow_right'))
              .width(16)
              .height(16)
              .fillColor('#c8c8c8')
              .margin({ right: 16 })
          }
          .width('100%')
          .height(80)
          .backgroundColor(Color.White)
          .padding({ left: 16 })
          .justifyContent(FlexAlign.SpaceBetween)
          .alignItems(VerticalAlign.Center)


          // 分割线
          Divider()
            .color('#e5e5e5')
            .strokeWidth(1)
            .margin({ left: 116 })

          // 基本信息
          this.ProfileItem('名字', this.profileInfo.name)

          Divider()
            .color('#e5e5e5')
            .strokeWidth(1)
            .margin({ left: 16 })

          this.ProfileItem('性别', this.profileInfo.gender)

          Divider()
            .color('#e5e5e5')
            .strokeWidth(1)
            .margin({ left: 16 })

          this.ProfileItem('地区', this.profileInfo.region)

          Divider()
            .color('#e5e5e5')
            .strokeWidth(1)
            .margin({ left: 16 })

          this.ProfileItem('手机号', this.profileInfo.phone)

          // 微信号区域
          Column() {

            this.ProfileItem('微信号', this.profileInfo.wechatId)

            Divider()
              .color('#e5e5e5')
              .strokeWidth(1)
              .margin({ left: 16 })

            // 我的二维码
            Row() {
              Text('我的二维码')
                .fontSize(16)
                .fontColor('#333333')
                .width(100)

              Image($r('app.media.arrow_right'))
                .width(16)
                .height(16)
                .fillColor('#c8c8c8')
                .margin({ right: 16 })

            }
            .width('100%')
            .height(50)
            .backgroundColor(Color.White)
            .padding({ left: 16 })
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
            .onClick(() => {
              this.getUIContext().getRouter().pushUrl({
                url:'pages/QRCodePage',
                params: {
                  nickname: this.profileInfo.name,
                  wechatId: this.profileInfo.wechatId,
                  avatar: this.profileInfo.avatar
                }
              })
            })
          }
          .margin({ top: 10 })

          // 拍一拍
          Column() {
            Row() {
              Text('拍一拍')
                .fontSize(16)
                .fontColor('#333333')
                .width(100)

              Text('变成气鼓鼓的小河豚🐡 ')
                .fontSize(16)
                .fontColor('#666666')
                .layoutWeight(1)
                .textAlign(TextAlign.End)
                .margin({ right: 8 })


              Image($r('app.media.arrow_right'))
                .width(16)
                .height(16)
                .fillColor('#c8c8c8')
                .margin({ right: 16 })
            }
            .width('100%')
            .height(50)
            .backgroundColor(Color.White)
            .padding({ left: 16 })
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
          }
          .margin({ top: 10 })

          Divider()
            .color('#e5e5e5')
            .strokeWidth(1)
            .margin({ left: 116 })


          // 签名
          this.ProfileItem('签名', this.profileInfo.signature)

          // 更多设置
          Column() {
            this.ProfileItem('来电铃声', this.profileInfo.ringtone)

            Divider()
              .color('#e5e5e5')
              .strokeWidth(1)
              .margin({ left: 16 })

            this.ProfileItem('我的地址', this.profileInfo.address)

            Divider()
              .color('#e5e5e5')
              .strokeWidth(1)
              .margin({ left: 16 })

            this.ProfileItem('我的发票抬头', this.profileInfo.invoiceTitle)

            Divider()
              .color('#e5e5e5')
              .strokeWidth(1)
              .margin({ left: 16 })

            this.ProfileItem('微信豆', this.profileInfo.wechatBeans)
          }
          .margin({ top: 10 })
        }
      }
      .layoutWeight(1)
      .backgroundColor('#f5f5f5')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f5f5')
    // 绑定弹窗
    .bindContentCover(this.showAvatarDialog, this.AvatarDialog(), {
      modalTransition: ModalTransition.NONE,
      backgroundColor: Color.Transparent
    })

  }
}

数据管理与初始化

  • 通过ProfileInfo接口结构化定义了个人资料的数据模型,涵盖头像、姓名、性别、地区、联系方式等十余个字段。
  • 在aboutToAppear生命周期中,通过router.getParams()接收并解析上级页面传递的参数,实现了页面间的数据同步。

界面布局与结构

  • 页面顶部为自定义导航栏,包含返回按钮和"个人资料"标题,符合应用整体设计规范。
  • 主体内容采用Scroll组件包裹,确保在信息量较大时用户可以滚动查看,提升了页面的适应性。

核心交互功能

  • 头像预览:点击头像可触发一个全屏遮罩弹窗,展示280x280像素的放大头像,用户可通过点击关闭按钮或背景来关闭弹窗。
  • 二维码跳转:点击"我的二维码"选项,通过router.pushUrl方法跳转到二维码展示页,并传递必要的用户信息。
  • 导航返回:顶部返回按钮通过router.back()实现页面回退。

个人资料界面展示

头像点击放大展示

3.3.6二维码页面(QRCodePage)

该页面是一个用于展示用户个人二维码的独立界面,为其他用户通过扫码添加好友提供了便捷的入口。

import { promptAction, router } from '@kit.ArkUI'

@Entry
@Component
struct QRCodePage {
  @State params:object=this.getUIContext().getRouter().getParams()
  @State nickname: string = ''
  @State wechatId: string = ''
  @State avatar: Resource = $r('app.media.Yao')

  aboutToAppear(): void {
    const params = router.getParams() as Record<string, Object>
    if (params) {
      this.nickname = (params['nickname'] as string) || ''
      this.wechatId = (params['wechatId'] as string) || ''
      this.avatar = (params['avatar'] as Resource) || $r('app.media.Yao')
    }
  }

  build() {

    Column() {
      // 顶部返回栏
      Row() {
        Image($r('app.media.arrow_back'))
          .width(24)
          .height(24)
          .margin({ left: 16 })
          .onClick(() => {
            router.back()
          })

        Text('我的二维码')
          .fontWeight(FontWeight.Bold)
          .fontSize(16)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)

        Row().width(24).height(24).margin({ right: 16 })
      }
      .backgroundColor("#f8f7f6")
      .width('100%')
      .height(50)
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(VerticalAlign.Center)
      .backgroundColor('#f8f7f6')

      Row(){
        // 用户头像
        Image(this.avatar)
          .width(80)
          .height(80)
          .borderRadius(5)
          .margin({ top: 40 })

        // 昵称
        Text(this.nickname)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.Black)
          .margin({ top: 14,left:10})

      }
      .width('100%')
      .justifyContent(FlexAlign.Start)
      .margin({left:112})


      // 二维码图片(使用预置图片)
      Image($r('app.media.qr_code'))
        .width(250)
        .height(250)
        .borderRadius(5)
        .margin({ top: 30 })
      Text("扫一扫上面的二维码图案,加我为朋友")
        .fontSize(10)
        .fontColor("#bbbab9")
        .margin({top:30})

      Row(){
        // 扫一扫按钮
        Button('扫一扫')
          .width('30%')
          .height(40)
          .fontSize(16)
          .fontColor("#0047AB")
          .backgroundColor(Color.White)
          .borderRadius(20)
          .margin({ top: 40 })
          .onClick(() => {
            promptAction.showToast({
              message:"未实际该功能!!",
              duration:2000
            })
          })
        Button('换个样式')
          .width('30%')
          .height(40)
          .fontSize(16)
          .fontColor("#0047AB")
          .backgroundColor(Color.White)
          .borderRadius(20)
          .margin({ top: 40 })
          .onClick(() => {
            promptAction.showToast({
              message:"未实际该功能!!",
              duration:2000
            })
          })
        Button('保存图片')
          .width('30%')
          .height(40)
          .fontSize(16)
          .fontColor("#0047AB")
          .backgroundColor(Color.White)
          .borderRadius(20)
          .margin({ top: 40 })
          .onClick(() => {
            promptAction.showToast({
              message:"未实际该功能!!",
              duration:2000
            })
          })
      }

    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
    .alignItems(HorizontalAlign.Center)
  }
}

页面初始化与数据接收

  • 页面通过router.getParams()方法接收从上一级页面(如个人中心)传递的用户信息。
  • 在aboutToAppear生命周期钩子中,解析并初始化用户的昵称、微信号和头像资源。

二维码展示

  • 中央展示一个250x250像素的二维码图片(当前为预置资源)。
  • 二维码下方配有引导性提示文字"扫一扫上面的二维码图案,加我为朋友",字体为灰色,字号较小,起到了辅助说明的作用。

功能操作区

  • 页面底部水平排列了三个核心功能按钮:“扫一扫”、“换个样式"和"保存图片”。
  • 所有按钮均采用白色背景、蓝色文字的轻量化设计,宽度均等,保持了界面的对称性。
  • 当前所有按钮点击后均显示"未实际该功能"的提示,为后续功能扩展(如调用相机、切换二维码样式、保存到相册)预留了清晰的接口。

二维码界面展示

3.4数据模型架构
3.4.1基础模型 (ContactModel.ets)
export interface ContactModel{
  avatar:Resource
  nickName:string
}
3.4.2分组模型 (ContactGroupModel.ets)
import { ContactModel } from "./ContactModel";

export interface  ContactGroupModel{
  title:string,
  contacts:ContactModel[]
}
3.4.3消息模型 (MessageModel.ets)
import { ContactModel } from "./ContactModel"

export interface MessageModel extends ContactModel{
  message:string
  time:string
}
3.4.4模型关系图

3.5页面跳转关系
3.5.1完整的用户流程

3.5.2参数传递示例
  • 从通讯录跳转到聊天
  private enterChatPage(contact: ContactModel) {
    try {
      router.pushUrl({
        url: 'pages/ChatWrapperPage',
        params: {
          avatar: contact.avatar,
          nickName: contact.nickName,
          messages: []
        }
      })
      console.log('从通讯录进入聊天页面成功:', contact.nickName)
    } catch (error) {
      console.error('从通讯录进入聊天页面失败:', error)
    }
  }
  • 在ChatWrapperPage中接收参数
aboutToAppear(): void {
    // 获取路由传递的参数
    const routerParams = router.getParams() as ChatParams
    if (routerParams) {
      this.params.avatar = routerParams.avatar || $r('app.media.Yao')
      this.params.nickName = routerParams.nickName || '好友'
      this.params.messages = routerParams.messages || []

    }
    console.log('ChatWrapperPage aboutToAppear, params:', this.params)
  }
3.6通用组件详解
3.6.1导航栏组件 (NavTitleBar.ets)

该组件是一个高度可复用的自定义导航栏,为应用内多个页面提供了统一且功能丰富的顶部导航体验。


import { promptAction, router } from "@kit.ArkUI"

@Component
export struct NavTileBar{
  @Prop title:string
  @State showMenu: boolean = false // 控制菜单显示状态

  build() {
    Stack() {
      //导航栏
      Row(){
        Image($r('app.media.arrow_back'))
          .width(24)
          .height(24)
          .margin({ left: 16 })
          .onClick(() => {
            // 返回按钮点击事件
            router.back()
          })

        Text(this.title)
          .fontWeight(800)
          .fontSize(16)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)

        Row() {
          Image($r('app.media.find'))
            .width(24)
            .height(24)
            .margin({ right: 16 })
            .onClick(() => {
              // 搜索按钮点击事件 - 跳转到搜索页面
              router.pushUrl({
                url: 'pages/SearchPage'
              })
            })

          // 加号按钮 - 只有点击这个按钮才显示菜单
          Image($r('app.media.add_top'))
            .width(24)
            .height(24)
            .margin({ right: 16 })
            .bindMenu([
              {
                value: '发起群聊',
                icon: $r('app.media.group_chat'),
                action: () => {
                  this.showUnimplementedToast('发起群聊')
                }
              },
              {
                value: '添加朋友',
                icon: $r('app.media.add_friend'),
                action: () => {
                  this.showUnimplementedToast('添加朋友')
                }
              },
              {
                value: '扫一扫',
                icon: $r('app.media.Nav_scan'),
                action: () => {
                  this.showUnimplementedToast('扫一扫')
                }
              },
              {
                value: '收付款',
                icon: $r('app.media.payment'),
                action: () => {
                  this.showUnimplementedToast('收付款')
                }
              }
            ])
        }
      }
      .width('100%')
      .height(50)
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(VerticalAlign.Center)
      .backgroundColor('#f8f7f6')
    }
    .width('100%')
    .height(50)
  }

  // 显示未实现功能的提示
  private showUnimplementedToast(featureName: string) {
    promptAction.showToast({
      message: `${featureName}功能未实现!!`,
      duration: 2000
    })
  }
}

核心布局与设计

  • 采用Row水平布局,整体高度为50像素,背景色为浅灰色(#f8f7f6),符合微信的视觉风格。
  • 导航栏从左至右依次为:返回按钮、标题文本、功能图标组,结构清晰,符合用户操作习惯。

 交互功能实现

  • 返回功能:左侧返回按钮通过router.back()方法实现页面回退,是导航的基础功能。
  • 搜索功能:右侧的搜索图标点击后可跳转到SearchPage,为用户提供全局搜索入口。
  • 扩展菜单:通过bindMenu属性为加号按钮绑定了一个下拉菜单,集成了四个核心快捷操作。

 扩展菜单功能
菜单项以列表形式展示,每个选项包含图标和文字,点击后触发相应动作:

  • 发起群聊:用于创建新的群组对话。
  • 添加朋友:提供添加新联系人的入口。
  • 扫一扫:调用扫码功能。
  • 收付款:进入支付相关界面。

当前所有菜单项点击后均显示"功能未实现"的提示,为后续功能开发预留了清晰的接口。

扩展菜单界面展示

3.6.2搜索组件 (SearchComponent.ets)

该组件是一个功能完备的搜索界面,集成了搜索输入、历史记录管理和结果展示等功能,为用户提供了便捷的站内搜索体验。

// 定义搜索结果接口 - 移到组件外部
interface SearchResult {
  avatar: Resource
  name: string
  description: string
  type: string
}

@Component
export struct SearchComponent {
  @Prop onClose: () => void
  @State searchText: string = ''
  @State searchHistory: string[] = ['朋友圈', '文章', '公众号', '小程序', '音乐', '表情']
  @State searchResults: SearchResult[] = []

  build() {
    Column() {
      // 搜索栏
      Row() {
        Image($r('app.media.arrow_back'))
          .width(24)
          .height(24)
          .margin({ left: 16 })
          .onClick(() => {
            if (this.onClose) {
              this.onClose()
            }
          })

        // 搜索输入框
        Row() {
          Image($r('app.media.search_icon'))
            .width(20)
            .height(20)
            .margin({ left: 12, right: 8 })
            .fillColor('#999999')

          TextInput({ placeholder: '搜索' })
            .width('100%')
            .height(40)
            .backgroundColor(Color.Transparent)
            .fontSize(16)
            .fontColor('#333333')
            .onChange((value: string) => {
              this.searchText = value
              // 模拟搜索逻辑
              if (value) {
                this.searchResults = [
                  {
                    avatar: $r('app.media.Yao'),
                    name: '安於(*^ω^*)',
                    description: '微信号: Cmy66666888888',
                    type: 'friend'
                  },
                  {
                    avatar: $r('app.media.GuiGui'),
                    name: 'a闺蜜٩(*´◐`*)۶',
                    description: '微信号: lisi456',
                    type: 'friend'
                  }
                ]
              } else {
                this.searchResults = []
              }
            })
        }
        .width('70%')
        .height(36)
        .backgroundColor('#F5F5F5')
        .borderRadius(18)
        .margin({ left: 12, right: 16 })
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)

      // 搜索内容区域
      if (this.searchText === '') {
        // 搜索历史
        Column() {
          Row() {
            Text('搜索历史')
              .fontSize(14)
              .fontColor('#999999')
              .margin({ left: 16 })
            Blank()
            Image($r('app.media.delete'))
              .width(16)
              .height(16)
              .margin({ right: 16 })
              .fillColor('#999999')
              .onClick(() => {
                // 清空搜索历史
                this.searchHistory = []
              })
          }
          .width('100%')
          .height(40)
          .justifyContent(FlexAlign.SpaceBetween)
          .alignItems(VerticalAlign.Center)

          // 历史标签
          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
            ForEach(this.searchHistory, (item: string) => {
              Text(item)
                .fontSize(14)
                .fontColor('#333333')
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, bottom: 8 })
                .backgroundColor('#F5F5F5')
                .borderRadius(15)
                .onClick(() => {
                  this.searchText = item
                })
            })
          }
          .width('100%')
          .padding({ left: 16, right: 16 })
        }
        .width('100%')
        .backgroundColor('#FFFFFF')
        .margin({ top: 1 })
      } else {
        // 搜索结果
        Column() {
          ForEach(this.searchResults, (item: SearchResult) => {
            Row() {
              Image(item.avatar)
                .width(40)
                .height(40)
                .borderRadius(4)
                .margin({ left: 16, right: 12 })

              Column() {
                Text(item.name)
                  .fontSize(16)
                  .fontColor('#333333')
                  .fontWeight(500)
                Text(item.description)
                  .fontSize(12)
                  .fontColor('#999999')
                  .margin({ top: 2 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)

              if (item.type === 'friend') {
                Text('添加')
                  .fontSize(14)
                  .fontColor('#07C160')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(4)
                  .border({ width: 1, color: '#07C160' })
                  .margin({ right: 16 })
              }
            }
            .width('100%')
            .height(64)
            .backgroundColor('#FFFFFF')
            .justifyContent(FlexAlign.Start)
            .alignItems(VerticalAlign.Center)
            .onClick(() => {
              // 点击搜索结果
            })
          })
        }
        .width('100%')
        .backgroundColor('#FFFFFF')
      }

      Blank()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F8F8')
  }
}

搜索栏设计

  • 顶部搜索栏集成了返回、搜索图标和输入框,采用圆角背景设计,视觉上与微信搜索功能保持一致。
  • 输入框具有实时响应能力,通过onChange事件监听文本变化,动态触发搜索逻辑。
  • 返回按钮通过@Prop onClose回调函数与父组件通信,实现了灵活的页面关闭控制。

搜索历史功能

  • 在未输入搜索关键词时,界面展示用户的搜索历史记录。
  • 历史记录以灵活的标签形式(Flex布局)展示,用户可点击任意标签快速填充搜索框。
  • 提供了清空历史功能,点击删除图标即可清空所有历史记录,操作简单直观。

搜索结果展示

  • 当用户输入内容时,界面切换至搜索结果视图。
  • 每个结果项显示用户头像、昵称和微信号,并根据类型(如好友)提供相应的操作按钮(如"添加")。

搜索页面展示

四、软件图标更改步骤

准备图标素材

  • 找一张你想要的图标图片(PNG格式,建议512×512像素)。
  • 准备两张图片:一张背景图(如绿色背景)和一张前景图(如白色图标)。

替换图标资源文件

  • 将背景图命名为 background.png,替换到 entry/src/main/resources/base/media/ 目录。
  • 将前景图命名为 foreground.png,替换到同一目录。
 修改配置文件
  • 打开 entry/src/main/module.json5 文件。
  • 找到 abilities 下的 icon 字段,确保值为 $media:layered_image。
"icon": "$media:layered_image"
  • 打开 entry/src/main/resources/base/media/layered_image.json 文件。
  • 确保配置指向你的背景图和前景图。
{
  "layered-image":
  {
    "background" : "$media:background",
    "foreground" : "$media:foreground"
  }
}

运行后的图标界面

五、学习路径建议

5.1初学者路径

  1. 理解项目结构:先搞清楚文件组织方式。
  2. 学习数据模型:掌握ContactModel、MessageModel等核心结构。
  3. 熟悉页面流程:从PreLogin到Index的完整跳转流程。
  4. 掌握组件使用:学会使用NavTitleBar等通用组件的复用方法。
  5. 实践页面跳转:理解router.pushUrl的跨页面使用。

5.2进阶学习方向

  1. 动画效果:为页面切换添加平滑过渡动画。
  2. 网络请求:对接真实后端 API,实现数据动态加载。
  3. 数据持久化:使用本地存储(如 Preferences)保存用户数据。
  4. 性能优化:优化列表渲染和内存占用,提升流畅度。
  5. 多语言支持:通过 i18n 实现国际化,适配不同语言用户。

六、开发环境配置

6.1必需工具

  1. DevEco Studio:HarmonyOS 官方集成开发环境。
  2. HarmonyOS SDK:包含开发所需的核心库和工具。
  3. 模拟器或真机:用于应用测试和调试。

6.2项目导入步骤

  • 下载并安装DevEco Studio。
  • 创建新的HarmonyOS项目。

打开DevEco Studio,选择File > New > Create Project。

选择模板Empty Ability,点击Next。

填写项目信,点击Finish,等待项目创建完成。

  • 将项目文件复制到对应目录。
  • 配置资源文件(图片、字符串等)。
  • 点击运行按钮,进行调试。

七、功能实现状态

 7.1已实现功能

  1. 完整的登录流程(包括验证逻辑)。
  2. 底部 Tab 导航(首页、聊天、通讯录等)。
  3. 聊天消息的模拟发送与接收。
  4. 通讯录字母索引(按姓名首字母分组)。
  5. 个人信息展示(头像、昵称、微信号等)。
  6. 搜索功能(模拟搜索历史和结果)。
  7. 二维码展示(个人二维码页面)。

7.2待完善功能

  1. 朋友圈功能(发布、浏览、点赞)。
  2. 视频号模块(短视频浏览与播放)。
  3. 真实网络通信(替换模拟数据为真实 API)。
  4. 数据持久化(保存用户设置和聊天记录)。
  5. 消息推送(实时通知新消息)。
  6. 文件传输(图片、文档等附件发送)。

八、项目总结

8.1学习成果

通过这个项目,你将掌握:

  1. 掌握 HarmonyOS ArkUI 框架的基本用法。
  2. 理解组件化开发思想,提升代码复用性。
  3. 掌握移动应用 UI 设计的基本原则(如布局、交互)。
  4. 熟悉状态管理和事件处理机制。
  5. 实现页面导航和参数传递。

8.2项目价值

  1. 完整用户体验:覆盖从登录到核心功能的完整流程。
  2. 规范代码结构:模块化设计便于维护和扩展。
  3. 丰富学习资源:详细注释和文档降低学习门槛。
  4. 实用开发技巧:可直接迁移到其他项目。

8.3后续发展

掌握了这个项目后,你可以:

  1. 开发更复杂的社交应用(如多端同步、实时音视频)。
  2. 在GitHub上分享自己的作品。
  3. 为开源社区贡献代码。

九、技术支持

请确保你的DevEco Studio和SDK版本与项目要求兼容。如果在学习过程中遇到问题,建议:

  1. 仔细阅读代码注释,理解设计意图。
  2. 查阅 HarmonyOS 官方文档(如 ArkUI 开发指南)。
  3. 在开发者社区提问(如 HarmonyOS 论坛)。
  4. 与同学和朋友讨论交流,集思广益。

十、参考文献标注

参考来源:主页面和通讯录页面的部分设计灵感来源于哔哩哔哩平台玄逸991的主页,链接如下:https://www.bilibili.com/video/BV1qf421f7De/?spm_id_from=333.1391.0.0&vd_source=daba134500045ebcd16a9a03ddee4b0f

源码下载

为了方便学习和对照,本项目的完整源码已上传到了GitCode,链接如下:

项目链接:https://gitcode.com/AnYuChen/MyHomeWork3/tree/master/FinalApplication

Logo

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

更多推荐