评论区的封装实现(富文本)

 

简介

        在日常的生活中,我们在浏览视频,文章的时候,往往都会有评论的功能,然后这个也是在开发中比较常见的一个功能,在鸿蒙里面往往采用富文本来实现图文混排发表功能的效果,下面我们来看一下这个是如何实现的。

实现

实现思路

        首先,评论区是一个组件或者弹框,这里我们使用的是一个封装好的组件,然后结合一个状态变量,来控制评论框组件的显隐。

        其次,再发表评论的时候,我们可能会发表文字或者表情,亦或是图片,如果单独的使用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,但是在日常的开发中却是常见的功能业务,后期还会对其进行更为完善的功能补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值