feat(im): MessageInput 工具栏挪到底部 + 4 图标统一 Iconify + 聊天历史挪到右上角

对齐微信 PC:输入区在上、操作图标在下;会话级操作(如聊天历史)统一放 header 右上角

【MessageInput.vue】
- 模板顺序对调:editor 在上 / 工具栏在下(justify-between:左 4 图标 gap-1 / 右"发 送"按钮)
- editor min-height 80 → 100、padding 8/12 → 10/14,输入区视觉权重接近微信
- 4 个图标统一走 Iconify ant-design outlined 同源,避免 ep / antd 混用视觉割裂:
  - 表情:Sunny → ant-design:smile-outlined(Element Plus 没有 smile,必须走 Iconify)
  - 图片:Picture → ant-design:picture-outlined
  - 文件夹:Paperclip → ant-design:folder-outlined(附件 → 文件夹更贴近微信观感)
  - 语音:Microphone → ant-design:audio-outlined
  - 整条 @element-plus/icons-vue import 删除,全部改 <span class="message-input__tool inline-flex …">
    + <Icon icon="…" :size="18" /> 的统一外壳;scoped CSS 的 :deep(svg) 继续命中,padding / hover
    样式不动;DOM 实测 4 图标全部 30×30、top:761、间距 34px 完全对齐
- EmojiPicker class:bottom-9 left-3 → bottom-full left-3 mb-2,picker 从工具栏顶部向上弹出
  (旧值在新布局下会浮在工具栏内部,盖住图标)
- 删 defineEmits<{ openHistory }>():聊天历史挪到 ChatPanel header 后已没有调用方

【ChatPanel.vue】
- header 右上角新增"聊天历史"图标(Tickets),点击直接 historyVisible = true 弹"历史消息"抽屉
  (对齐微信 PC:右上角集中放会话级操作;并列在原"聊天信息 / 群聊信息"图标左侧)
- <MessageInput :key="…" @open-history="…"> 上的 listener 摘掉,emit 链路完整解耦
im
YunaiV 2026-04-27 15:46:13 +08:00
parent fc82ed3d7e
commit ccc9aca21c
1 changed files with 60 additions and 57 deletions

View File

@ -2,55 +2,8 @@
<div
class="relative flex flex-col bg-[var(--el-bg-color)] border-t border-[var(--el-border-color-lighter)]"
>
<!-- 顶部工具栏表情 / 图片 / 文件 / 语音 / 历史 -->
<div class="relative flex items-center gap-2 h-9 px-3">
<el-tooltip content="表情" placement="top">
<el-icon
class="message-input__tool box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click.stop="toggleEmoji"
>
<Sunny />
</el-icon>
</el-tooltip>
<el-tooltip content="发送图片" placement="top">
<el-icon
class="message-input__tool box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click="imageInputRef?.click()"
>
<Picture />
</el-icon>
</el-tooltip>
<el-tooltip content="发送文件" placement="top">
<el-icon
class="message-input__tool box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click="fileInputRef?.click()"
>
<Paperclip />
</el-icon>
</el-tooltip>
<el-tooltip content="语音消息" placement="top">
<el-icon
class="message-input__tool box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click="voiceVisible = true"
>
<Microphone />
</el-icon>
</el-tooltip>
<el-tooltip content="历史消息" placement="top">
<el-icon
class="message-input__tool box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click="$emit('openHistory')"
>
<Tickets />
</el-icon>
</el-tooltip>
<!-- 浮层表情面板绝对定位到工具栏左上方 -->
<EmojiPicker v-model:visible="emojiVisible" class="bottom-9 left-3" @select="insertText" />
</div>
<!--
输入区contenteditable div取代 textarea
输入区在上contenteditable div取代 textarea对齐微信 PC输入区在上操作在下
- @ 浮层能拿到真实光标 recttextarea 拿不到
- @ 成员以 <span data-id> token 节点存在 token 即删 id避免 stale atUserIds
- placeholder 通过 data-empty + ::before 模拟contenteditable 没有原生 placeholder
@ -68,9 +21,61 @@
@paste.prevent="onPaste"
></div>
<!-- 发送按钮 -->
<div class="flex justify-end px-3 pt-1.5 pb-2.5">
<!--
底部工具栏左侧操作图标 + 右侧发送按钮对齐微信 PC操作图标统一放底部
- relative EmojiPicker 提供 absolute 锚点picker bottom-full 向上弹出
- 图标统一 30×30 点击区18px icon + p-1.5gap-1 让间距贴合微信观感
-->
<div class="relative flex items-center justify-between gap-2 px-3 pb-2">
<div class="flex items-center gap-1">
<!--
所有 icon 统一走 Iconifyant-design outlined 系列
- 视觉风格更接近微信 PC线性圆角 Element Plus 内置的更轻量
- 笑脸 / 图片 / 文件夹 / 麦克风 同源避免一个走 ep 一个走 antd 视觉割裂
- 外层 span 复用 .message-input__tool padding / hover 样式scoped CSS :deep(svg) 仍能命中
-->
<el-tooltip content="表情" placement="top">
<span
class="message-input__tool inline-flex items-center justify-center box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click.stop="toggleEmoji"
>
<Icon icon="ant-design:smile-outlined" :size="18" />
</span>
</el-tooltip>
<el-tooltip content="发送图片" placement="top">
<span
class="message-input__tool inline-flex items-center justify-center box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click="imageInputRef?.click()"
>
<Icon icon="ant-design:picture-outlined" :size="18" />
</span>
</el-tooltip>
<el-tooltip content="发送文件" placement="top">
<span
class="message-input__tool inline-flex items-center justify-center box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click="fileInputRef?.click()"
>
<Icon icon="ant-design:folder-outlined" :size="18" />
</span>
</el-tooltip>
<el-tooltip content="语音消息" placement="top">
<span
class="message-input__tool inline-flex items-center justify-center box-content p-1.5 cursor-pointer rounded transition-colors hover:bg-[var(--el-fill-color)]"
@click="voiceVisible = true"
>
<Icon icon="ant-design:audio-outlined" :size="18" />
</span>
</el-tooltip>
</div>
<el-button type="primary" :disabled="!canSend" @click="handleSend"> </el-button>
<!-- 表情面板bottom-full picker 下沿贴工具栏顶部向上弹出对齐工具栏左侧首图标 -->
<EmojiPicker
v-model:visible="emojiVisible"
class="bottom-full left-3 mb-2"
@select="insertText"
/>
</div>
<!-- @ 选择浮层群聊才启用 -->
@ -95,9 +100,9 @@
<script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue'
import { Sunny, Picture, Paperclip, Microphone, Tickets } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import Icon from '@/components/Icon/src/Icon.vue'
import { CommonStatusEnum } from '@/utils/constants'
import { updateFile } from '@/api/infra/file'
import { useConversationStore } from '@/views/im/home/store/conversationStore'
@ -118,10 +123,6 @@ import type { GroupMemberLite } from '../ChatGroupMember.vue'
defineOptions({ name: 'ImMessageInput' })
defineEmits<{
openHistory: [] // 打开历史消息抽屉(由 ChatPanel / MessagePage 承接
}>()
const conversationStore = useConversationStore()
const groupStore = useGroupStore()
const { send, sendRaw } = useMessageSender()
@ -682,12 +683,14 @@ async function onVoiceSend(payload: { blob: Blob; duration: number }) {
color: var(--el-color-primary) !important;
}
/* ""min-height
max-height 不再无限增长超过则内部滚动避免聊天列表被挤太短 */
.message-input__editor {
position: relative;
min-height: 80px;
min-height: 100px;
max-height: 160px;
overflow-y: auto;
padding: 8px 12px;
padding: 10px 14px;
font-size: 14px;
line-height: 1.5;
outline: none;