简介
在日常的生活中,我们在浏览视频,文章的时候,往往都会有评论的功能,然后这个也是在开发中比较常见的一个功能,在鸿蒙里面往往采用富文本来实现图文混排发表功能的效果,下面我们来看一下这个是如何实现的。
实现
实现思路
首先,评论区是一个组件或者弹框,这里我们使用的是一个封装好的组件,然后结合一个状态变量,来控制评论框组件的显隐。
其次,再发表评论的时候,我们可能会发表文字或者表情,亦或是图片,如果单独的使用Text组件,或者是Image组件显然是没有很好的实现这个效果,因此我们需要使用RichEditor(富文本)组件来实现图文的混排。
然后,我们需要准备好自己的表情,或者是使用系统自带的emoji表情也是可以的,主要是根据自己的业务和后端的字段来实现。接下来就是使用Stack和Grid布局来实现Emoji表情的排列,然后就是需要通过RichEditor的控制器来解析输入的内容,最后就是将内容解析成后端需要的字段进行提交,最后就是评论数据的渲染。
代码实现
效果
代码
import { Key } from '../constant/key'
import { inputMethod } from '@kit.IMEKit'
interface EmojiData {
img: ResourceStr
}
@Entry
@Component
struct Index {
@StorageProp(Key.topHEIGHT) topHeight: number = 0
@State isShow: boolean = false
build() {
Column() {
Button('点击发表评论')
.onClick(() => {
this.isShow = !this.isShow
console.log('isShow', this.isShow)
})
if (this.isShow) {
EmojiComment({ title: '请输入内容' })
.position({ bottom: 0 })
}
}
.width('100%')
.height('100%')
.padding({ top: this.topHeight, bottom: 10 })
}
}
@Component
struct EmojiComment {
@Prop title: string = ''
controllerRich: RichEditorController = new RichEditorController(); //富文本控制器
@State isEdit: boolean = false
@State currentIndex: number = 0 //当前常用表情的索引
@State CartIndex: number = 0 //光标的索引
@State isShow: boolean = false //显示键盘
//全部表情
AllEmojiList: Array<EmojiData> = Array.from<Array<EmojiData>, EmojiData>({ length: 102 },
(_, i): EmojiData => ({ img: $r(`app.media.em_${i + 1}`) }));
//常用表情
MostUseEmojiList: Array<EmojiData> = [
{ img: $r('app.media.em_85') },
{ img: $r('app.media.em_100') },
{ img: $r('app.media.em_101') },
{ img: $r('app.media.em_93') },
{ img: $r('app.media.em_4') },
{ img: $r('app.media.em_8') },
{ img: $r('app.media.em_15') },
{ img: $r('app.media.em_96') },
]
//常用表情的下标
private IndexOfMostUseEmojiList: Array<number> = [85, 100, 101, 93, 4, 8, 15, 96]
//发送内容
confirm: (content: string) => void = () => {
}
build() {
Column() {
Row({ space: 9 }) {
RichEditor({ controller: this.controllerRich })
.height(38)
.layoutWeight(1)
.placeholder(this.title,
{ fontColor: '#C4C4C4', font: { size: 14, weight: FontWeight.Regular, family: 'PingFang SC', } })
.backgroundColor('#FFFFFF')
.borderRadius(4)
.padding({ left: 10, top: 9, bottom: 8 })
.caretColor('#22BFA7')
.defaultFocus(true)//主动聚焦
.id('RichEdit')
.enableKeyboardOnFocus(true)
.onDidChange(() => {
this.CartIndex = this.controllerRich.getCaretOffset()
this.CartIndex > 0 ? this.isEdit = true : this.isEdit = false
})
Row({ space: 9 }) {
Image($r('app.media.expression'))
.width(28)
.height(28)
.id('EmojiBtn')
.focusable(true)
.onClick(() => {
this.isShow = !this.isShow
this.isShow ? inputMethod.getController().stopInputSession() : inputMethod.getController().showTextInput()
})
if (this.isEdit) {
Text('发送')
.width(64)
.height(38)
.textAlign(TextAlign.Center)
.backgroundColor('#22BFA7')
.borderRadius(4)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.onClick(() => {
let res = this.controllerRich.getSpans();
// console.log('res', JSON.stringify(res, null, 2));
// 提取 value 和 valueResourceStr 字段
const extractedValues: Array<string> = [];
res.forEach((item) => {
if (item && typeof item === 'object') {
if (item['value'] && typeof item['value'] === 'string') {
// 提取文字类型的 value
extractedValues.push(item['value']);
} else if (item['valueResourceStr'] && typeof item['valueResourceStr'] === 'string') {
// 提取图片类型的 valueResourceStr
const valueResourceStr = item['valueResourceStr'];
const match = valueResourceStr.match(/\/([^\/]+)\.png$/)!
extractedValues.push(`[${match[1]}]`);
}
}
});
let resultString = extractedValues.join('');
this.confirm(resultString)
this.getUIContext()
.getPromptAction()
.showToast({ message: JSON.stringify(resultString, null, 2), alignment: Alignment.Center })
console.log('末尾删除:', this.CartIndex)
this.controllerRich.deleteSpans({ start: 0, end: this.CartIndex })
})
}
}
}
//常用表情
Row() {
ForEach(this.MostUseEmojiList, (item: EmojiData, index) => {
Image(item.img)
.width(30)
.height(30)
.onClick(() => {
this.currentIndex = this.IndexOfMostUseEmojiList[index];
this.controllerRich.addImageSpan(this.AllEmojiList[this.currentIndex-1].img, {
imageStyle: {
size: [20, 20]
}
})
})
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 10, })
.padding({ left: 10, right: 10 })
//所有的表情
Stack() {
Grid() {
ForEach(this.AllEmojiList, (item: EmojiData) => {
GridItem() {
Image(item.img)
.width(30)
.height(30)
.onClick(() => {
this.controllerRich.addImageSpan(item.img, {
imageStyle: {
size: [20, 20]
}
})
})
}
})
}
.maxCount(8)
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.width('100%')
.rowsGap(10)
.height(280)
.padding({
top: 10,
bottom: 10,
})
.scrollBar(BarState.Off)
Row() {
Image($r('app.media.backspace'))
.width(22)
.height(16)
.fillColor(Color.White)
.onClick(() => {
this.controllerRich.deleteSpans({ start: this.CartIndex - 1, end: this.CartIndex })
})
}
.width(64)
.height(38)
.backgroundColor(Color.White)
.position({ bottom: 10, right: 0 })
.padding({ left: 21 })
}
}
.width('100%')
.backgroundColor('#F8F9FB')
.padding({ top: 7, left: 10, right: 10 })
}
}
结语
这目前只是一个可以发表评论和简单解析输入内容的一个建议demo,但是在日常的开发中却是常见的功能业务,后期还会对其进行更为完善的功能补充。