#端云一体化开发# #HarmonyOS Next#《说书人》鸿蒙原生基于角色的对话式文本编辑开发方案

1、写在前面

过去的一百年里,在"编程"的这个行业诞生之初,人们采用面向过程的方式进行开发,但是,伴随着程序规模的日益增大,程序的复杂度也随之增加,使用结构化编程方法来管理复杂的程序逻辑变得越来越困难。因此,开发者们引入了"面向对象"的概念,采用将数据和操作封装在"对象"中的方式,令程序设计更加条理。

而现如今,市场上的文本类软件越来越多,创作者的门槛越来越低,无数文字作品争相涌现,长篇小说的字数也不断突破记录,可是,对于一个经验不够丰富的创作者而言,常常会发生文章内容跑偏,角色性格无法把握的问题,特别是在小说文字长度越来越长时,这样的问题便愈发严重。而在这样的情况下,我决定仿照"面向对象"的方式,开发一款基于"角色"定义,进行剧本编写的软件,帮助文字创作者们能够基于"角色"来编写文章,提高创作的效率与质量。

在选择开发平台时,我们注意到鸿蒙平台上还没有基于对话式的文本类APP,这使得我在实现基本功能的同时,还可以在这方面填充市场的空缺。此外,我也希望能够参与鸿蒙生态建设,助力国产科技企业发展,成为openHarmony漫天星光中的一员。

综上,我们决定开发一个鸿蒙原生的,基于角色定义的对话式文本编写软件,以达到解决市场空缺,解决用户需求的目的。

本文就是分享《说书人》的一部分鸿蒙技术实践过程,欢迎各位阅读

2、路由展示

2.1、页面展示

《说书人》的界面十分简洁,主要的界面包括:角色/剧本列表,用户空间,动态空间,以及阅读界面等,如下图所示

2.2、路由展示

(1)Tabs组件简介

Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。TabContent是内容页,TabBar是导航页签栏,页面结构如下图所示,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。

Tabs组件的实践

《说书人》的角色/书本列表,动态空间,个人页面,主要分为UserSpacePage,scriptInterface和DynamicPage三个组件,结合Tabs组件,进行页面布局,如下列代码所示:

import { frame, frameDataSet, ThisPagefontColor } from '../Data/frameData'
import { userOwnData, userOwn, userTheyData, userTheyTemplate } from '../Data/userData'

import UserSpacePage from './mainPage/userSpacePage'
import scriptInterface from './mainPage/scriptPage'
import DynamicPage from './mainPage/dynamicPage'
import router from '@ohos.router'

@Entry
@Component
struct UserSpace {
  aboutToAppear(): void {
    let params = router.getParams() as Record<string, number>

    this.pageIndex = params['goPageIndex']

  }

  @StorageProp('frame') frameData: frame = frameDataSet
  @StorageProp('fontColor') ThisColor: ResourceColor = ThisPagefontColor

  @StorageProp('userOwnData') userOwn: userOwnData = userOwn
  @StorageProp('userTheyData') userTheyData: userTheyTemplate = userTheyData

  @State pageIndex: number = 0

  build() {
    Flex(){
      Tabs({
        index: this.pageIndex == undefined ? 0 : this.pageIndex,
      }){
        TabContent(){
          scriptInterface()
        }.tabBar('推荐')
        TabContent(){
          DynamicPage()
        }.tabBar('动态')
        TabContent(){
          UserSpacePage()
        }.tabBar('空间')
      }
      .barPosition(BarPosition.End)
    }
    .backgroundColor(this.frameData.backGround)
  }
}复制

3、端云一体化的实践应用

在本项目中,接入了华为云提供的认证服务与数据库服务,将AGC的serverless云和HarmonyOS的端进行结合开发,以下是开发示例

1、登录AppGallery Connect

2、在全部服务中,选择"认证服务",启动手机号码认证

3、我的项目中,添加项目,再选择添加应用,随后根据实际情况填写即可

4、在"项目设置"中,下载SDK配置,并放在自己项目的如图所示的目录中

5、修改oh-package.json5文件,添加以下内容

"dependencies": { "@hw-agconnect/hmcore": "^1.0.1", "@hw-agconnect/cloud": "^1.0.1" }复制复制复制

6、修改EntryAbility.ets文件,在onCreate中添加以下内容

async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
  let input = await this.context.resourceManager.getRawFileContent('agconnect-services.json')
  let jsonString  = util.TextDecoder.create('utf-8', {
    ignoreBOM: true
  }).decodeWithStream(input, {
    stream: false
  });
  initialize(this.context, JSON.parse(jsonString));
}复制复制复制

7、打开需要使用验证码的界面,导入代码

// 导入
import cloud from '@hw-agconnect/cloud';
import { Auth, VerifyCodeAction } from '@hw-agconnect/cloud';
import { promptAction } from '@kit.ArkUI';

// 申请验证码
function requestVerifyCode(phoneNumber: string){
  cloud.auth().requestVerifyCode({
    action: VerifyCodeAction.REGISTER_LOGIN,
    lang: 'zh_CN',
    sendInterval: 60,
    verifyCodeType: {
      phoneNumber: phoneNumber,
      countryCode: '86',
      kind: "phone"
    }
  }).then(verifyCodeResult => {
    //验证码申请成功
    promptAction.showToast({
      message: '申请成功'
    })
  }).catch(() => {
    //验证码申请失败
    promptAction.showToast({
      message: "申请失败 "
    })
  });
}

// 注册用户
function createUser(phoneNumber: string, phoneCode: string){
  cloud.auth().createUser({
    kind: 'phone',
    countryCode: '86',
    phoneNumber: phoneNumber,
    password: '123456789',//可以给用户设置初始密码,后续可以用密码来登录
    verifyCode: phoneCode
  }).then(result => {
    // 创建用户成功
    promptAction.showToast({
      message: '创建成功'
    })
  }).catch(() => {
    // 创建用户失败
    promptAction.showToast({
      message: '创建失败'
    })
  })
}复制

以上是手机号部分的开发流程,数据库部分也与其相似,可以在AGC官网查看文档进行开发

4、角色/剧本创建部分

对于角色/剧本的创建,总是会遇见需要"多维数组"等云数据库默认无法实现的情况,这种情况下,就采用分隔符来对数据进行处理,以字符串类型存储在云端,再在本地读取时进行反处理

角色创建部分如下:

import { frame, frameDataSet,ThisPagefontColor, frameData } from '../../Data/frameData'
import { userOwnData, userOwn } from '../../Data/userData'
import { roleDataTemplate, roleData, roleCustom } from '../../Data/roleData'
import router from '@ohos.router';

import EntryPage from '../popUp/EntryPage'
import CarePage from '../popUp/CarePage'
import DelCarePage from '../popUp/DelCarePage'
import closePage from '../popUp/closePage'

import promptAction from '@ohos.promptAction';

@Extend(Text) function textCare(size: number){
  .fontColor('#3c3f41')
  .backgroundColor('#e0dcda')
  .fontSize(size)
  .fontWeight(300)
  .padding({
    left: 8,
    right: 8,
    top: 5,
    bottom: 5
  })
  .borderRadius(12)
  .opacity(.8)
}


@Entry
@Component
struct UserCreate {

  entryPageOpen: CustomDialogController = new CustomDialogController({
    builder: EntryPage({
      onCancel: (Name: string, Value: string) => {
        this.roleCustom.push({
          name: Name,
          value: Value
        })
      }
    })
  })

  carePageOpen: CustomDialogController = new CustomDialogController({
    builder: CarePage({
      onCancel: (Value: string) => {
        this.roleLabel.push(Value)
      }
    })
  })

  delcarePageOpen: CustomDialogController = new CustomDialogController({
    builder: DelCarePage({
      onCancel: (Value: boolean) => {
        if (Value) {
          this.roleLabel.splice(this.delRoleIndex, 1)
        }
      },
    })
  })

  closeOpen: CustomDialogController = new CustomDialogController({
    builder: closePage({
      onCancel: () => {
        this.roleData = roleData
        this.newRoleData = this.roleData
        this.roleData.createUserID = this.userOwn.userID
        this.roleData.createUserName = this.userOwn.userName

        router.pushUrl({
          "url": "pages/Main/userSpace",
          "params": {
            "goPageIndex" : 0
          }
        })
      },
    })
  })

  @StorageProp('frame') frameData: frame = frameDataSet
  @StorageProp('fontColor') ThisColor: ResourceColor = ThisPagefontColor

  @StorageProp('userOwnData') userOwn: userOwnData = userOwn
  @StorageLink('roleData') roleData: roleDataTemplate = roleData

  @State newRoleData: roleDataTemplate = this.roleData

  @State openText: boolean = false

  @State roleCustom: roleCustom[] = []

  @State roleLabel: string[] = this.newRoleData.roleLabel

  @State delRoleIndex: number = 0

  loadRoleData(){
    this.roleData = this.newRoleData
    this.roleData.roleLabel = this.roleLabel
    this.roleData.createUserID = this.userOwn.userID
    this.roleData.createUserName = this.userOwn.userName

    for (let index = 0; index < this.roleCustom.length; index++) {
      this.roleData.roleCustomName.push(this.roleCustom[index].name)
      this.roleData.roleCustomValue.push(this.roleCustom[index].value)
    }

    router.pushUrl({
      "url": "pages/dynamicSystem/previewPage/userPreviewPage"
    })
  }

  build() {
    Flex({
      wrap: FlexWrap.Wrap
    }){
      Column({ space: 24 }){
        // 标题
        Row(){
          frameData()
            .width(50)
          Text('角色')
            .fontSize(this.frameData.typefaceSize + 8)
            .fontColor(this.ThisColor)
          Text('')
            .width(50)
        }.width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Scroll(){
          Column({ space: 24 }){
            // 封面标题作者标签ID
            Row({space: 16}){
              Text(String(this.newRoleData.roleName == '' ? '角色名称' : this.newRoleData.roleName).slice(String(this.newRoleData.roleName == '' ? '角色名称' : this.newRoleData.roleName).length - 2, String(this.newRoleData.roleName == '' ? '角色名称' : this.newRoleData.roleName).length))
                .fontSize(18)
                .width(120)
                .height(120)
                .borderRadius(60)
                .backgroundColor(this.frameData.backGround)
                .border({
                  width: 1
                })
                .textOverflow({overflow: TextOverflow.Ellipsis})
                .padding(2)
                .textAlign(TextAlign.Center)
                .letterSpacing(3)
              Column({ space: 9 }){

                // 角色昵称
                Row(){
                  Text(this.newRoleData.roleName == '' ? '角色名称' : this.newRoleData.roleName)
                    .fontSize(this.frameData.typefaceSize + 6)
                    .fontColor(this.ThisColor)
                    .fontWeight(600)
                }

                // ID
                Row(){
                  Text(this.newRoleData.roleAge.toString())
                    .fontSize(this.frameData.typefaceSize - 4)
                    .fontColor(Color.Gray)
                    .fontWeight(400)
                }

                // 作者
                Row({ space: 6 }){
                  Text(String(this.userOwn.userName).slice(String(this.userOwn.userName).length - 2, String(this.userOwn.userName).length))
                    .fontSize(8)
                    .width(28)
                    .height(28)
                    .borderRadius(14)
                    .backgroundColor(this.frameData.backGround)
                    .border({
                      width: 1
                    })
                    .textOverflow({overflow: TextOverflow.Ellipsis})
                    .padding(2)
                    .textAlign(TextAlign.Center)
                  Text(this.userOwn.userName)
                    .fontSize(this.frameData.typefaceSize - 2)
                    .fontColor(Color.Gray)
                }

                // 标签
                Flex({
                  wrap: FlexWrap.Wrap,
                  justifyContent: FlexAlign.Start
                }){
                  ForEach(this.roleLabel, (item: string) => {
                    Text(item)
                      .textCare(this.frameData.typefaceSize - 3)
                      .margin({
                        right: 2,
                        bottom: 2
                      })
                  })
                }

              }.width('100%')
              .alignItems(HorizontalAlign.Start)
              .height(120)
            }

            // 简介
            Column({ space: 12 }){
              Text('简介')
                .fontSize(this.frameData.typefaceSize + 4)
                .fontColor(this.ThisColor)
                .fontWeight(600)
              Row(){
                Text(`${this.newRoleData.roleBrief}`)
                  .maxLines(this.openText ? 999 : 4)
                  .textOverflow({overflow: this.openText ? TextOverflow.None : TextOverflow.Ellipsis })
                  .fontSize(this.frameData.typefaceSize)
                  .fontColor(this.ThisColor)
              }.width('100%')
              Row(){
                Text(this.openText ? '收起' : '展开')
                  .fontSize(this.frameData.typefaceSize - 2)
                  .fontColor(this.ThisColor)
                  .fontWeight(300)
                  .onClick(() => {
                    this.openText = !this.openText
                  })
              }.width('100%')
            }.width('100%')
            .alignItems(HorizontalAlign.Start)

            // 详细档案
            Column(){
              Row(){
                Text('详细档案')
                  .fontSize(this.frameData.typefaceSize + 4)
                  .fontColor(this.ThisColor)
                  .fontWeight(600)
              }.width('100%')
              .justifyContent(FlexAlign.SpaceBetween)
              .border({
                width: {
                  bottom: 2
                }
              })
              .padding({
                bottom: 6
              })

              // 详细档案中的档案
              Column(){
                // 词条
                Column(){
                  // 姓名
                  Row(){
                    Text('姓名:')
                      .fontSize(this.frameData.typefaceSize + 2)
                      .fontColor(this.ThisColor)
                      .fontWeight(500)

                    TextInput({ placeholder: this.newRoleData.roleName == '' ? '请输入角色姓名' : this.newRoleData.roleName })
                      .width('100%')
                      .backgroundColor(this.frameData.backGround)
                      .borderRadius(0)
                      .border({
                        width:{
                          bottom: 1
                        },
                        color: this.ThisColor
                      })
                      .fontColor(this.ThisColor)
                      .placeholderColor(this.ThisColor)
                      .fontSize(this.frameData.typefaceSize + 2)
                      .maxLength(16)
                      .onChange(value => {
                        this.newRoleData.roleName = value
                      })
                  }.width('100%')
                  .justifyContent(FlexAlign.SpaceBetween)
                  .padding({
                    left: 12,
                    right: 12,
                    top: 12,
                    bottom: 12
                  })
                  // 年龄
                  Row(){
                    Text('年龄:')
                      .fontSize(this.frameData.typefaceSize + 2)
                      .fontColor(this.ThisColor)
                      .fontWeight(500)

                    TextInput({ placeholder: this.newRoleData.roleAge == '0' ? '请输入角色年龄' : this.newRoleData.roleAge.toString()})
                      .width('100%')
                      .backgroundColor(this.frameData.backGround)
                      .borderRadius(0)
                      .border({
                        width:{
                          bottom: 1
                        },
                        color: this.ThisColor
                      })
                      .fontColor(this.ThisColor)
                      .placeholderColor(this.ThisColor)
                      .fontSize(this.frameData.typefaceSize + 2)
                      .maxLength(8)
                      .type(InputType.Number)
                      .onChange(value => {
                        this.newRoleData.roleAge = value
                      })
                  }.width('100%')
                  .justifyContent(FlexAlign.SpaceBetween)
                  .padding({
                    left: 12,
                    right: 12,
                    top: 12,
                    bottom: 12
                  })
                  // 性别
                  Row(){
                    Text('性别:')
                      .fontSize(this.frameData.typefaceSize + 2)
                      .fontColor(this.ThisColor)
                      .fontWeight(500)

                    TextInput({ placeholder: this.newRoleData.roleGender == '' ? '请输入角色性别' : this.newRoleData.roleGender })
                      .width('100%')
                      .backgroundColor(this.frameData.backGround)
                      .borderRadius(0)
                      .border({
                        width:{
                          bottom: 1
                        },
                        color: this.ThisColor
                      })
                      .fontColor(this.ThisColor)
                      .placeholderColor(this.ThisColor)
                      .fontSize(this.frameData.typefaceSize + 2)
                      .maxLength(6)
                      .onChange(value => {
                        this.newRoleData.roleGender = value
                      })
                  }.width('100%')
                  .justifyContent(FlexAlign.SpaceBetween)
                  .padding({
                    left: 12,
                    right: 12,
                    top: 12,
                    bottom: 12
                  })

                  ForEach(this.roleCustom, (item: roleCustom, index: number) => {
                    // 姓名
                    Row(){
                      Text(item.name + ':')
                        .fontSize(this.frameData.typefaceSize + 2)
                        .fontColor(this.ThisColor)
                        .fontWeight(500)
                        .textAlign(TextAlign.End)
                        .width('17%')

                      Row(){
                        TextInput({ placeholder: '请输入角色姓名', text: item.value })
                          .width('71%')
                          .backgroundColor(this.frameData.backGround)
                          .borderRadius(0)
                          .border({
                            width:{
                              bottom: 1
                            },
                            color: this.ThisColor
                          })
                          .fontColor(this.ThisColor)
                          .placeholderColor(this.ThisColor)
                          .fontSize(this.frameData.typefaceSize + 2)
                          .maxLength(16)

                        Text('删除')
                          .onClick(() => {
                            this.roleCustom.splice(index, 1)
                          })
                          .textAlign(TextAlign.End)
                          .width(50)
                      }

                    }.width('100%')
                    .justifyContent(FlexAlign.SpaceBetween)
                    .padding({
                      left: 12,
                      right: 12,
                      top: 12,
                      bottom: 12
                    })
                  })

                  Row(){
                    Button('添加自定义词条')
                      .border({
                        width: 1
                      })
                      .backgroundColor(this.frameData.backGround)
                      .fontColor(this.ThisColor)
                      .width('100%')
                      .onClick(() => {
                        this.entryPageOpen.open()
                      })
                  }.width('100%')
                  .padding({
                    left: 12,
                    right: 12,
                    top: 12,
                    bottom: 12
                  })
                }

                // 标签
                Flex({
                  wrap: FlexWrap.Wrap,
                  justifyContent: FlexAlign.SpaceAround
                }){
                  ForEach(this.roleLabel, (item: string, index) => {
                    Row(){
                      Button(item)
                        .border({
                          width: 1
                        })
                        .backgroundColor(this.frameData.backGround)
                        .fontColor(this.ThisColor)
                        .width('45%')
                        .onClick(() => {
                          this.delRoleIndex = index
                          this.delcarePageOpen.open()
                        })
                    }.padding({
                      left: 12,
                      right: 12,
                      top: 12,
                      bottom: 12
                    })
                  })
                  Row(){
                    Button('添加角色标签')
                      .border({
                        width: 1
                      })
                      .backgroundColor(this.frameData.backGround)
                      .fontColor(this.ThisColor)
                      .width('45%')
                      .onClick(() => {
                        this.carePageOpen.open()
                      })
                  }.padding({
                    left: 12,
                    right: 12,
                    top: 12,
                    bottom: 12
                  })
                }.width('100%')

                // 剧本
                Column({ space: 6 }){
                  Row(){
                    Text('角色简介:')
                      .fontSize(this.frameData.typefaceSize + 4)
                      .fontColor(this.ThisColor)
                      .fontWeight(600)
                  }.width('100%')
                  .padding({
                    bottom: 6
                  })

                  // 简介
                  Row(){
                    TextArea({ placeholder: '请输入角色简介'})
                      .width('100%')
                      .height(120)
                      .padding(16)
                      .fontSize(this.frameData.typefaceSize + 2)
                      .fontColor(this.ThisColor)
                      .fontWeight(500)
                      .onChange(value => {
                        this.newRoleData.roleBrief = value
                      })
                      .maxLength(200)
                  }.width('100%')
                }
              }

            }.width('100%')
            .alignItems(HorizontalAlign.Center)
          }
          .alignItems(HorizontalAlign.Start)

        }.edgeEffect(EdgeEffect.Spring)
        .scrollBar(BarState.Off)
        .height('85%')

        Row(){
          Button('预览')
            .fontSize(this.frameData.typefaceSize + 2)
            .fontColor(this.ThisColor)
            .backgroundColor(this.frameData.backGround)
            .width('50%')
            .padding({
              top: 16,
              bottom: 16
            })
            .onClick(() => {
              this.loadRoleData()
            })
          Button('取消')
            .fontSize(this.frameData.typefaceSize + 2)
            .fontColor(this.ThisColor)
            .backgroundColor(this.frameData.backGround)
            .width('50%')
            .padding({
              top: 16,
              bottom: 16
            })
            .onClick(() => {
              this.closeOpen.open()
            })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceAround)
        .alignItems(VerticalAlign.Center)
        .border({
          width: {
            top: 2
          }
        })

      }
      .padding({
        top: 16,
        left: 24,
        right: 24,
        bottom: 16
      })
      .height('100%')
    }
  }
}复制

5、总结

说书人APP是一款基于鸿蒙平台开发的原生应用,旨在通过角色定义的方式提高文字创作的效率与质量。

不仅填补了市场上对话式文本类APP的空缺,而且通过华为端云一体化开发,接入了短信认证服务和云数据库,确保了应用的高兼容性、稳定性、低功耗和高性能。

项目背景与目标:随着市场上文本创作软件的增多,创作者面临的挑战也随之增加,尤其是对于经验不足的创作者而言,如何有效管理角色和剧情成为一大难题。说书人APP应运而生,目的是帮助这些创作者通过"面向对象"的方式编写剧本,从而提高创作效率和作品质量。

技术实现:应用采用了华为的端云一体化开发模式,利用DevEco Studio进行编译运行,并支持API version 9版本SDK,确保了应用的技术先进性和安全性。

功能特色:包括注册登录系统、文章创建与编辑、章节管理、角色创建与自定义、以及用户个性化设置等,全面满足文字创作者的需求。特别是其基于角色的对话式编写功能,为创作者提供了全新的创作体验。

优势方面

技术创新:作为鸿蒙平台上的原生应用,说书人APP在技术上具有天然的优势,包括更好的系统兼容性和性能优化。 用户体验:通过角色定义的创作方式,简化了复杂的剧情管理,使得创作者可以更加专注于内容的创作。

生态贡献:参与鸿蒙生态建设,不仅丰富了应用市场的多样性,也为国产科技企业的发展贡献了一份力量。

需要改进的地方

用户引导:对于新用户来说,应用的功能可能较为复杂,需要更加直观的用户引导和教程来帮助用户快速上手。

社交互动:虽然应用提供了丰富的创作工具,但在社交互动方面似乎有所欠缺。增加创作者之间的交流和合作功能,可能会进一步提升用户的活跃度和粘性。

持续优化:任何应用都需要根据用户反馈进行持续优化。说书人APP也不例外,需要不断收集用户意见,对功能、界面和性能进行迭代升级。

相关推荐
大G哥1 小时前
鸿蒙NEXT开发中使用星闪服务
华为·harmonyos
BruceGerGer1 小时前
HarmonyOS鸿蒙开发 应用开发常见问题总结(持续更新...)
前端·鸿蒙
马剑威(威哥爱编程)1 小时前
鸿蒙NEXT使用request模块实现本地文件上传
华为·harmonyos·harmonyos-next
轻口味2 小时前
【每日学点鸿蒙知识】tensorflowlite编译、音频编码线程、沉浸式状态栏、TextArea最大字节数限制等
华为·音视频·harmonyos
夜阑卧听风吹雨,铁马冰河入梦来2 小时前
Hypium纯血鸿蒙系统 HarmonyOS NEXT自动化测试框架
华为·harmonyos
Xzzzz9114 小时前
华为配置 之 RIP
华为
Xzzzz9114 小时前
华为配置 之 链路聚合
linux·服务器·网络·windows·计算机网络·华为
我是Feri8 小时前
Harmony OS开发-ArkUI框架速成四
harmonyos·arkts·arkui
轻口味11 小时前
【每日学点鸿蒙知识】低功耗蓝牙、指纹识别认证、读取raw文件示例、CommonEvent是否跨线程、定位参数解释等
华为·harmonyos
御承扬11 小时前
从零开始开发纯血鸿蒙应用之实现内部文件处理页
华为·harmonyos·arkts·文本编辑·文本浏览