更新记录

1.0.0(2026-05-09)

第一版


平台兼容性

uni-app(4.0)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

其他

多语言 暗黑模式 宽屏模式

uni-app @用户 编辑器插件说明

1. 插件简介

本插件提供两个通用组件:

  • pz-at-editor:支持输入 @ 选择用户,并输出结构化消息数据。
  • pz-at-editor-echo:将结构化消息数据反解成文本回显。

组件目录:

components/
├─ pz-at-editor/
│  └─ pz-at-editor.vue
└─ pz-at-editor-echo/
   └─ pz-at-editor-echo.vue

页面只需要负责三件事:

  1. 准备用户列表。
  2. 接收 pz-at-editorchange 数据。
  3. 发送或展示时使用 messageList

2. 功能说明

pz-at-editor

  • 输入 @ 后弹出用户选择面板。
  • 选择用户后插入不可编辑的 @昵称 节点。
  • 支持自定义 @用户 文本颜色。
  • 实时输出纯文本、HTML、被 @ 的用户 ID 列表、结构化消息数组。

pz-at-editor-echo

  • 接收 messageList
  • textuser 类型按顺序回显成文本。
  • user 类型会自动显示为 @昵称,并使用传入颜色高亮。

3. 平台兼容

pz-at-editor 使用了:

  • contenteditable
  • renderjs
  • DOM API
  • Selection / Range

因此当前主要适用于:

  • H5
  • App-Vue

不建议直接用于小程序端。小程序端如需兼容,建议单独实现 textarea + 高亮层 方案。

pz-at-editor-echo 是普通展示组件,通常可用于 H5、App、小程序。


4. 消息数据格式

最终推荐发送给后端的数据是 messageList

[
  {
    "type": "text",
    "content": "你好 "
  },
  {
    "type": "user",
    "content": "小明",
    "id": "u1001"
  }
]

字段说明:

字段 类型 说明
type String 消息类型,支持 textuser
content String 文本内容;当 typeuser 时是用户昵称
id String 用户 ID,仅 user 类型有

5. pz-at-editor 使用方式

引入组件

import MentionEditor from '@/components/pz-at-editor/pz-at-editor.vue'

基础示例

<template>
  <mention-editor
    :user-list="userList"
    :mention-text-color="mentionTextColor"
    @change="handleEditorChange"
  />
</template>

<script>
import MentionEditor from '@/components/pz-at-editor/pz-at-editor.vue'

export default {
  components: {
    MentionEditor
  },
  data() {
    return {
      mentionTextColor: '#ff2442',
      userList: [
        { userCode: 'u1001', nickName: '小明' },
        { userCode: 'u1002', nickName: '小红' }
      ],
      editorState: {
        html: '',
        text: '',
        userCodeList: [],
        messageList: []
      }
    }
  },
  methods: {
    handleEditorChange(payload) {
      this.editorState = payload
    }
  }
}
</script>

Props

参数 类型 默认值 说明
userList Array [] 用户列表
userIdKey String 'userCode' 用户 ID 字段名
userNameKey String 'nickName' 用户昵称字段名
mentionTextColor String '#ff2442' @用户 文本颜色
placeholder String '请输入内容,输入 @ 选择用户' 输入框占位文案
popupTitle String '选择要 @ 的用户' 选人弹窗标题

用户列表格式

默认字段:

[
  {
    userCode: 'u1001',
    nickName: '小明'
  }
]

如果接口返回字段不同,可以用 user-id-keyuser-name-key 指定:

<mention-editor
  :user-list="userList"
  user-id-key="id"
  user-name-key="name"
  @change="handleEditorChange"
/>

对应数据:

[
  {
    id: 'u1001',
    name: '小明'
  }
]

事件

pz-at-editor 只有一个对外事件:change

输入内容变化、选择用户后都会触发:

handleEditorChange(payload) {
  console.log(payload)
}

payload 格式:

{
  html: '<span data-mention="true">...</span>',
  text: '你好 @小明',
  userCodeList: ['u1001'],
  messageList: [
    { type: 'text', content: '你好 ' },
    { type: 'user', content: '小明', id: 'u1001' }
  ]
}

字段说明:

字段 类型 说明
html String 编辑器内部 HTML
text String 纯文本内容
userCodeList String[] @ 的用户 ID 列表
messageList Array 推荐提交给后端的结构化消息数组

6. pz-at-editor-echo 使用方式

引入组件

import MessageEcho from '@/components/pz-at-editor-echo/pz-at-editor-echo.vue'

基础示例

<template>
  <message-echo
    :message-list="messageList"
    :mention-text-color="mentionTextColor"
  />
</template>

<script>
import MessageEcho from '@/components/pz-at-editor-echo/pz-at-editor-echo.vue'

export default {
  components: {
    MessageEcho
  },
  data() {
    return {
      mentionTextColor: '#ff2442',
      messageList: [
        { type: 'text', content: '你好 ' },
        { type: 'user', content: '小明', id: 'u1001' }
      ]
    }
  }
}
</script>

Props

参数 类型 默认值 说明
messageList Array [] 结构化消息数组
mentionTextColor String '#2563eb' 回显区 @用户 文本颜色

7. 推荐接入流程

  1. 页面层通过接口获取用户列表。
  2. 把用户列表传给 pz-at-editor
  3. 监听 change,保存最新的 editorState
  4. 点击发送时,取 editorState.messageList 提交给后端。
  5. 需要回显时,把后端返回的 messageList 传给 pz-at-editor-echo

推荐以后端协议使用 messageList 为准,htmltextuserCodeList 作为页面辅助字段使用。


8. 完整页面示例

如果插件上传时只上传了 components 目录,可以按下面方式在业务页面接入。

示例文件:pages/index/index.vue

<template>
    <view class="page">
        <view class="hero-card">
            <view class="hero-header">
                <text class="hero-title">@ 富文本编辑器</text>
                <text class="hero-desc">评论输入框与回显区域已拆成两个通用组件,首页仅负责传参和接收结果。</text>
            </view>

            <!-- 页面说明区域 -->
            <view class="tips">
                <text class="tips-label">模拟用户:</text>
                <view class="tag-list">
                    <text v-for="user in userList" :key="user.userCode" class="tag">@{{ user.nickName }}</text>
                </view>
            </view>

            <!-- 模拟用户数据展示 -->
            <view class="section-card">
                <view class="section-head">
                    <text class="section-title">评论内容</text>
                    <text class="section-tip">试试输入:大家好 @</text>
                </view>

                <mention-editor
                    :user-list="userList"
                    :mention-text-color="mentionTextColor"
                    @change="handleEditorChange"
                />

                <!-- 输入区:只负责承载通用输入组件和发送按钮 -->
                <view class="action-row">
                    <button class="action-btn primary" @tap="handleSend">发送内容</button>
                </view>
            </view>

            <!-- 调试信息展示区:便于观察输入组件实时输出的数据 -->
            <view class="section-card">
                <view class="state-row">
                    <text class="state-label">纯文本:</text>
                    <text class="state-value">{{ editorState.text || '暂无内容' }}</text>
                </view>
                <view class="state-row">
                    <text class="state-label">被 @ 用户:</text>
                    <text class="state-value">{{ editorState.userCodeList.length ? editorState.userCodeList.join('、') : '暂无' }}</text>
                </view>
                <view class="state-row">
                    <text class="state-label">消息数组:</text>
                    <text class="state-value state-json">{{ JSON.stringify(editorState.messageList || [], null, 2) }}</text>
                </view>
            </view>

            <!-- 文本回显区:外层负责标题,组件只负责核心文本内容 -->
            <view class="section-card">
                <text class="section-title block-title">JSON数据文本回显</text>
                <message-echo
                    :message-list="echoMessageList"
                    :mention-text-color="mentionTextColor"
                />
            </view>

            <!-- 最终发送结果 JSON 展示 -->
            <view class="section-card">
                <text class="section-title block-title">发送结果 JSON</text>
                <text class="result-json">{{ formattedResult }}</text>
            </view>
        </view>
    </view>
</template>

<script>
    import MentionEditor from '@/components/pz-at-editor/pz-at-editor.vue'
    import MessageEcho from '@/components/pz-at-editor-echo/pz-at-editor-echo.vue'

    const MENTION_TEXT_COLOR = '#ff2442'

    // 模拟用户数据,实际接入时可替换为接口返回
    const mockUsers = [{
        userCode: 'u1001',
        nickName: '小明'
    }, {
        userCode: 'u1002',
        nickName: '小红'
    }, {
        userCode: 'u1003',
        nickName: '阿杰'
    }, {
        userCode: 'u1004',
        nickName: '设计喵'
    }, {
        userCode: 'u1005',
        nickName: '前端星'
    }]

    export default {
        components: {
            MentionEditor,
            MessageEcho
        },
        data() {
            return {
                mentionTextColor: MENTION_TEXT_COLOR,
                // 页面维护的用户列表
                userList: mockUsers,
                editorState: {
                    html: '',
                    text: '',
                    userCodeList: [],
                    messageList: []
                },
                sendResult: []
            }
        },
        computed: {
            // 有发送结果时优先展示发送结果,否则展示实时编辑结果
            echoMessageList() {
                return this.sendResult.length ? this.sendResult : this.editorState.messageList
            },
            // 最终 JSON 字符串展示
            formattedResult() {
                return JSON.stringify(this.echoMessageList || [], null, 2)
            }
        },
        methods: {
            // 接收输入组件实时变化
            handleEditorChange(payload) {
                this.editorState = Object.assign({
                    html: '',
                    text: '',
                    userCodeList: [],
                    messageList: []
                }, payload || {})
            },
            // 模拟发送:将当前消息数组复制为最终发送结果
            handleSend() {
                this.sendResult = JSON.parse(JSON.stringify(this.editorState.messageList || []))
                uni.showToast({
                    title: '已生成发送数据',
                    icon: 'none'
                })
            }
        }
    }
</script>

<style>
    page {
        background: #f4f7fb;
    }

    .page {
        min-height: 100vh;
        padding: 32rpx;
        box-sizing: border-box;
    }

    .hero-card {
        padding: 32rpx;
        background: #ffffff;
        border-radius: 24rpx;
        box-shadow: 0 18rpx 50rpx rgba(255, 36, 66, 0.10);
    }

    .hero-header {
        display: flex;
        flex-direction: column;
        gap: 16rpx;
    }

    .hero-title {
        font-size: 40rpx;
        font-weight: 700;
        color: #2b2b2b;
    }

    .hero-desc {
        font-size: 28rpx;
        line-height: 1.6;
        color: #7a7a7a;
    }

    .tips {
        margin-top: 28rpx;
        padding: 24rpx;
        border-radius: 20rpx;
        background: linear-gradient(135deg, #fff1f3 0%, #fff7f7 100%);
    }

    .tips-label {
        font-size: 26rpx;
        color: #8a3040;
    }

    .section-card {
        margin-top: 28rpx;
        padding: 28rpx;
        background: #ffffff;
        border-radius: 24rpx;
        box-shadow: 0 18rpx 50rpx rgba(255, 36, 66, 0.10);
    }

    .section-head {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 20rpx;
    }

    .section-title {
        font-size: 30rpx;
        font-weight: 600;
        color: #2b2b2b;
    }

    .block-title {
        display: block;
        margin-bottom: 16rpx;
    }

    .section-tip {
        font-size: 24rpx;
        color: #b06b76;
    }

    .action-row {
        display: flex;
        gap: 20rpx;
        margin-top: 24rpx;
    }

    .action-btn {
        flex: 1;
        height: 84rpx;
        line-height: 84rpx;
        border-radius: 18rpx;
        font-size: 28rpx;
    }

    .action-btn::after {
        border: 0;
    }

    .action-btn.primary {
        background: linear-gradient(135deg, #ff8a9b 0%, #ff6f83 100%);
        color: #ffffff;
    }

    .tag-list {
        display: flex;
        flex-wrap: wrap;
        margin-top: 16rpx;
        gap: 16rpx;
    }

    .tag {
        padding: 10rpx 20rpx;
        border-radius: 999rpx;
        font-size: 24rpx;
        color: #ff2442;
        background: rgba(255, 36, 66, 0.10);
    }

    .state-row {
        display: flex;
        align-items: flex-start;
        margin-bottom: 16rpx;
    }

    .state-row:last-child {
        margin-bottom: 0;
    }

    .state-label {
        width: 160rpx;
        font-size: 26rpx;
        color: #8f5b64;
    }

    .state-value {
        flex: 1;
        font-size: 26rpx;
        line-height: 1.6;
        color: #2b2b2b;
        word-break: break-all;
    }

    .state-json {
        white-space: pre-wrap;
    }

    .result-json {
        display: block;
        padding: 24rpx;
        border-radius: 20rpx;
        font-size: 24rpx;
        line-height: 1.7;
        color: #7f3340;
        background: linear-gradient(135deg, #fff1f3 0%, #ffe4e8 100%);
        white-space: pre-wrap;
        word-break: break-all;
    }
</style>

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。