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也不例外,需要不断收集用户意见,对功能、界面和性能进行迭代升级。