✨ feat(im): 优化输入框的样式
parent
384a0c134a
commit
dfbae06afa
|
|
@ -12,6 +12,7 @@ export interface ImManagerGroupMessageVO {
|
||||||
content: string
|
content: string
|
||||||
status: number
|
status: number
|
||||||
atUserIds?: number[]
|
atUserIds?: number[]
|
||||||
|
atUserNicknames?: string[]
|
||||||
receiptStatus?: number
|
receiptStatus?: number
|
||||||
sendTime: Date
|
sendTime: Date
|
||||||
createTime: Date
|
createTime: Date
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface ImManagerSensitiveWordVO {
|
||||||
word: string
|
word: string
|
||||||
status: number
|
status: number
|
||||||
creator?: string
|
creator?: string
|
||||||
|
creatorName?: string
|
||||||
createTime?: Date
|
createTime?: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,7 +254,8 @@ export enum DICT_TYPE {
|
||||||
|
|
||||||
// ========== IM - 即时通讯模块 ==========
|
// ========== IM - 即时通讯模块 ==========
|
||||||
IM_MESSAGE_TYPE = 'im_message_type', // IM 消息类型
|
IM_MESSAGE_TYPE = 'im_message_type', // IM 消息类型
|
||||||
IM_MESSAGE_STATUS = 'im_message_status', // IM 消息状态
|
IM_PRIVATE_MESSAGE_STATUS = 'im_private_message_status', // IM 私聊消息状态:0=未读 / 2=已撤回 / 3=已读
|
||||||
|
IM_GROUP_MESSAGE_STATUS = 'im_group_message_status', // IM 群聊消息状态:0=正常 / 2=已撤回
|
||||||
IM_GROUP_MESSAGE_RECEIPT_STATUS = 'im_group_message_receipt_status', // IM 群消息回执状态
|
IM_GROUP_MESSAGE_RECEIPT_STATUS = 'im_group_message_receipt_status', // IM 群消息回执状态
|
||||||
IM_FRIEND_STATUS = 'im_friend_status', // IM 好友状态
|
IM_FRIEND_STATUS = 'im_friend_status', // IM 好友状态
|
||||||
IM_GROUP_STATUS = 'im_group_status' // IM 群状态
|
IM_GROUP_STATUS = 'im_group_status' // IM 群状态
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
- 拖拽区在右边缘,鼠标变 col-resize
|
- 拖拽区在右边缘,鼠标变 col-resize
|
||||||
-->
|
-->
|
||||||
<aside
|
<aside
|
||||||
class="relative flex flex-col shrink-0 bg-[var(--el-bg-color)] border-r border-[var(--el-border-color-lighter)] shadow-[2px_0_8px_rgba(0,0,0,0.05)]"
|
class="relative flex flex-col shrink-0 bg-[var(--el-fill-color-light)] border-r border-[var(--el-border-color-lighter)] shadow-[2px_0_8px_rgba(0,0,0,0.05)]"
|
||||||
:style="{ width: asideWidth + 'px' }"
|
:style="{ width: asideWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
|
||||||
|
|
@ -1,98 +1,111 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<!--
|
||||||
class="relative flex flex-col bg-[var(--el-bg-color)] border-t border-[var(--el-border-color-lighter)]"
|
外层底色与消息流(bg-color-page)保持一致,让"消息 → 输入"无色差过渡;
|
||||||
>
|
padding 给内层白卡片呼吸空间,卡片自带边框就够区分输入区,不再需要一条 border-t
|
||||||
|
-->
|
||||||
|
<div class="relative bg-[var(--el-bg-color-page)] px-3 pt-2 pb-3">
|
||||||
<!--
|
<!--
|
||||||
输入区在上:contenteditable div(取代 textarea,对齐微信 PC:输入区在上,操作在下)
|
内层白色圆角卡片 = editor + 工具栏;border + rounded 模拟微信"输入框"边界,
|
||||||
- 让 @ 浮层能拿到真实光标 rect(textarea 拿不到)
|
避免之前"无框 Web 输入"的散开感;border 走 scoped CSS(UnoCSS 不带 border-style preflight)
|
||||||
- 让 @ 成员以 <span data-id> token 节点存在,删 token 即删 id,避免 stale atUserIds
|
|
||||||
- placeholder 通过 data-empty + ::before 模拟(contenteditable 没有原生 placeholder)
|
|
||||||
-->
|
-->
|
||||||
<div
|
<div class="message-input__card relative flex flex-col bg-[var(--el-bg-color)] rounded-lg">
|
||||||
ref="editorRef"
|
<!--
|
||||||
class="message-input__editor"
|
输入区在上:contenteditable div(取代 textarea,对齐微信 PC:输入区在上,操作在下)
|
||||||
contenteditable="true"
|
- 让 @ 浮层能拿到真实光标 rect(textarea 拿不到)
|
||||||
data-placeholder="按 Enter 发送,Shift+Enter 换行"
|
- 让 @ 成员以 <span data-id> token 节点存在,删 token 即删 id,避免 stale atUserIds
|
||||||
data-empty=""
|
- placeholder 通过 data-empty + ::before 模拟(contenteditable 没有原生 placeholder)
|
||||||
role="textbox"
|
-->
|
||||||
@keydown="onKeydown"
|
<div
|
||||||
@input="onInput"
|
ref="editorRef"
|
||||||
@scroll.passive="onEditorScroll"
|
class="message-input__editor"
|
||||||
@paste.prevent="onPaste"
|
contenteditable="true"
|
||||||
></div>
|
data-placeholder="按 Enter 发送,Shift+Enter 换行"
|
||||||
|
data-empty=""
|
||||||
|
role="textbox"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
@input="onInput"
|
||||||
|
@scroll.passive="onEditorScroll"
|
||||||
|
@paste.prevent="onPaste"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
底部工具栏:左侧操作图标 + 右侧发送按钮(对齐微信 PC:操作图标统一放底部)
|
底部工具栏:左侧操作图标 + 右侧发送按钮(对齐微信 PC:操作图标统一放底部)
|
||||||
- relative 给 EmojiPicker 提供 absolute 锚点,picker 用 bottom-full 向上弹出
|
- relative 给 EmojiPicker 提供 absolute 锚点,picker 用 bottom-full 向上弹出
|
||||||
- 图标统一 30×30 点击区(18px icon + p-1.5),gap-1 让间距贴合微信观感
|
- 图标统一 30×30 点击区(18px icon + p-1.5),gap-1 让间距贴合微信观感
|
||||||
-->
|
- border-t 在编辑区与工具栏之间画一条与 card 边框同色的细线(scoped CSS 避绕 UnoCSS preflight 缺失)
|
||||||
<div class="relative flex items-center justify-between gap-2 px-3 pb-2">
|
-->
|
||||||
<div class="flex items-center gap-1">
|
<div
|
||||||
<!--
|
class="message-input__toolbar relative flex items-center justify-between gap-2 px-3 py-2"
|
||||||
所有 icon 统一走 Iconify(ant-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>
|
|
||||||
|
|
||||||
<!-- 群聊:发送按钮 + ▼ 下拉菜单(点主按钮普通发送 / 点 ▼ 选「发送回执消息」),对齐微信 PC -->
|
|
||||||
<el-dropdown
|
|
||||||
v-if="isGroup"
|
|
||||||
split-button
|
|
||||||
type="primary"
|
|
||||||
:disabled="!canSend"
|
|
||||||
@click="handleSend()"
|
|
||||||
@command="handleSendCommand"
|
|
||||||
>
|
>
|
||||||
发 送
|
<div class="flex items-center gap-1">
|
||||||
<template #dropdown>
|
<!--
|
||||||
<el-dropdown-menu>
|
所有 icon 统一走 Iconify(ant-design outlined 系列):
|
||||||
<el-dropdown-item command="receipt">发送回执消息</el-dropdown-item>
|
- 视觉风格更接近微信 PC(线性、圆角,比 Element Plus 内置的更轻量)
|
||||||
</el-dropdown-menu>
|
- 笑脸 / 图片 / 文件夹 / 麦克风 同源,避免一个走 ep 一个走 antd 视觉割裂
|
||||||
</template>
|
- 外层 span 复用 .message-input__tool 的 padding / hover 样式,scoped CSS 的 :deep(svg) 仍能命中
|
||||||
</el-dropdown>
|
-->
|
||||||
<!-- 私聊:普通发送按钮(私聊没有群回执概念) -->
|
<el-tooltip content="表情" placement="top">
|
||||||
<el-button v-else type="primary" :disabled="!canSend" @click="handleSend()">发 送</el-button>
|
<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>
|
||||||
|
|
||||||
<!-- 表情面板:bottom-full 让 picker 下沿贴工具栏顶部,向上弹出(对齐工具栏左侧首图标) -->
|
<!-- 群聊:发送按钮 + ▼ 下拉菜单(点主按钮普通发送 / 点 ▼ 选「发送回执消息」),对齐微信 PC -->
|
||||||
<EmojiPicker
|
<el-dropdown
|
||||||
v-model:visible="emojiVisible"
|
v-if="isGroup"
|
||||||
class="bottom-full left-3 mb-2"
|
split-button
|
||||||
@select="insertText"
|
type="primary"
|
||||||
/>
|
:disabled="!canSend"
|
||||||
|
@click="handleSend()"
|
||||||
|
@command="handleSendCommand"
|
||||||
|
>
|
||||||
|
发 送
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="receipt">发送回执消息</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<!-- 私聊:普通发送按钮(私聊没有群回执概念) -->
|
||||||
|
<el-button v-else 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- @ 选择浮层:群聊才启用 -->
|
<!-- @ 选择浮层:群聊才启用 -->
|
||||||
|
|
@ -686,6 +699,15 @@ async function onVoiceSend(payload: { blob: Blob; duration: number }) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 输入框卡片外框 + 编辑区与工具栏之间的分隔线:UnoCSS 不带 border-style preflight,
|
||||||
|
border-* 类只设色 / 宽不出线,统一走 scoped 显式 shorthand 兜底 */
|
||||||
|
.message-input__card {
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
.message-input__toolbar {
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
/* el-icon 全局规则 .el-icon{color:var(--color,inherit); font-size:inherit; width:1em; height:1em}
|
/* el-icon 全局规则 .el-icon{color:var(--color,inherit); font-size:inherit; width:1em; height:1em}
|
||||||
会盖过 UnoCSS 原子类;用字面选择器 + !important 兜底。
|
会盖过 UnoCSS 原子类;用字面选择器 + !important 兜底。
|
||||||
颜色取 Element Plus 主题变量,暗色自动切到浅灰 */
|
颜色取 Element Plus 主题变量,暗色自动切到浅灰 */
|
||||||
|
|
@ -700,14 +722,14 @@ async function onVoiceSend(payload: { blob: Blob; duration: number }) {
|
||||||
color: var(--el-color-primary) !important;
|
color: var(--el-color-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入区在上、工具栏在下时,编辑区视觉上承担"主体",min-height 撑大一些贴近微信观感;
|
/* 输入区在上、工具栏在下时,编辑区视觉上承担"主体",min-height / padding 都比早期版本撑大,
|
||||||
max-height 不再无限增长,超过则内部滚动,避免聊天列表被挤太短 */
|
贴近微信 PC 的"大输入框"观感;max-height 限内部滚动,避免聊天列表被挤太短 */
|
||||||
.message-input__editor {
|
.message-input__editor {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100px;
|
min-height: 120px;
|
||||||
max-height: 160px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px 14px;
|
padding: 14px 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-1 flex-col min-w-0 bg-[var(--el-fill-color-light)]">
|
<div class="flex flex-1 flex-col min-w-0 bg-[var(--el-fill-color-light)]">
|
||||||
<template v-if="conversationStore.activeConversation">
|
<template v-if="conversationStore.activeConversation">
|
||||||
<!-- 顶部:会话名 + 右侧功能图标 -->
|
<!-- 顶部:会话名(群聊带人数)+ 右侧功能图标;border 走 scoped CSS(项目 UnoCSS 不带 border-style preflight) -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between h-14 px-5 bg-[var(--el-fill-color-light)] border-b border-[var(--el-border-color-light)]"
|
class="message-panel__header flex items-center justify-between h-14 px-5 bg-[var(--el-fill-color-light)]"
|
||||||
>
|
>
|
||||||
<span class="text-base font-medium text-[var(--el-text-color-primary)]">
|
<span class="flex items-baseline gap-1.5 min-w-0">
|
||||||
{{ conversationStore.activeConversation?.name || '' }}
|
<span
|
||||||
|
class="overflow-hidden text-base font-medium truncate text-[var(--el-text-color-primary)]"
|
||||||
|
>
|
||||||
|
{{ conversationStore.activeConversation?.name || '' }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isGroup && headerMemberCount > 0"
|
||||||
|
class="flex-shrink-0 text-sm text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
|
({{ headerMemberCount }})
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
<!-- 聊天历史:从输入区底部工具栏挪到顶部右上角,对齐微信 PC(点击弹窗承接历史消息) -->
|
<!-- 聊天历史:从输入区底部工具栏挪到顶部右上角,对齐微信 PC(点击弹窗承接历史消息) -->
|
||||||
|
|
@ -140,6 +150,21 @@ const isGroup = computed(
|
||||||
() => conversationStore.activeConversation?.type === ImConversationType.GROUP
|
() => conversationStore.activeConversation?.type === ImConversationType.GROUP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群聊 header 显示的人数:优先 groupStore.memberCount(无需等成员列表),无值再回退 members.length
|
||||||
|
*
|
||||||
|
* 之所以不直接用 groupMembers.value.length:成员列表是按需懒加载的,刚切到群时未加载完,
|
||||||
|
* 而 groupInfo.memberCount 跟群信息一起来,能更早显示人数避免"先空再蹦"
|
||||||
|
*/
|
||||||
|
const headerMemberCount = computed(() => {
|
||||||
|
const conversation = conversationStore.activeConversation
|
||||||
|
if (!conversation || conversation.type !== ImConversationType.GROUP) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const group = groupStore.getGroup(conversation.targetId)
|
||||||
|
return group?.memberCount ?? group?.members?.length ?? 0
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MessageInput 的 :key —— 切群时强制 unmount + remount,让 editor / mention range /
|
* MessageInput 的 :key —— 切群时强制 unmount + remount,让 editor / mention range /
|
||||||
* 上一会话草稿全部归零;用 fallback 'none' 避开 activeConversation 短暂为 null 的窗口
|
* 上一会话草稿全部归零;用 fallback 'none' 避开 activeConversation 短暂为 null 的窗口
|
||||||
|
|
@ -149,13 +174,9 @@ const messageInputKey = computed(() => {
|
||||||
return conv ? getConversationKey(conv) : 'none'
|
return conv ? getConversationKey(conv) : 'none'
|
||||||
})
|
})
|
||||||
|
|
||||||
/** "是否停留在底部"的阈值:距离底部 < 80px 视为底部 */
|
const BOTTOM_THRESHOLD = 80 // "是否停留在底部"的阈值:距离底部 < 80px 视为底部
|
||||||
const BOTTOM_THRESHOLD = 80
|
const showJumpToBottom = ref(false) // 当前是否已不在底部(显示"回到底部"按钮)
|
||||||
|
const newMessageCount = ref(0) // 不在底部期间累计的新消息数
|
||||||
/** 当前是否已不在底部(显示"回到底部"按钮) */
|
|
||||||
const showJumpToBottom = ref(false)
|
|
||||||
/** 不在底部期间累计的新消息数 */
|
|
||||||
const newMessageCount = ref(0)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前激活的群详情:优先 groupStore(带详细字段),未加载完时用 activeConversation 兜底
|
* 当前激活的群详情:优先 groupStore(带详细字段),未加载完时用 activeConversation 兜底
|
||||||
|
|
@ -376,6 +397,11 @@ watch(
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 顶部分隔线:UnoCSS 不带 border-style preflight,class 写法只设色 / 宽不出线,走 scoped 显式 shorthand */
|
||||||
|
.message-panel__header {
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
/* el-icon 全局规则 .el-icon{color:var(--color,inherit)} 优先级胜过 UnoCSS,这里用 :deep + !important 兜底;
|
/* el-icon 全局规则 .el-icon{color:var(--color,inherit)} 优先级胜过 UnoCSS,这里用 :deep + !important 兜底;
|
||||||
颜色直接引用 Element Plus 主题变量,暗色模式自动切到更亮的灰 */
|
颜色直接引用 Element Plus 主题变量,暗色模式自动切到更亮的灰 */
|
||||||
.message-panel__header-icon,
|
.message-panel__header-icon,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
<div class="flex flex-1 min-w-0 h-full">
|
<div class="flex flex-1 min-w-0 h-full">
|
||||||
<!-- 左侧会话列表(可拖拽宽度) -->
|
<!-- 左侧会话列表(可拖拽宽度) -->
|
||||||
<ResizableAside :default-width="260" :storage-key="StorageKeys.asideWidth">
|
<ResizableAside :default-width="260" :storage-key="StorageKeys.asideWidth">
|
||||||
<!-- 顶部:搜索框 + "+" 号下拉(对齐微信 PC:发起群聊 / 添加朋友) -->
|
<!-- 顶部:搜索框 + "+" 号下拉(对齐微信 PC:发起群聊 / 添加朋友);h-14 与右侧 MessagePanel 头部对齐 -->
|
||||||
<div
|
<div
|
||||||
class="flex flex-shrink-0 gap-2 items-center px-4 py-2 border-b border-[var(--el-border-color-lighter)]"
|
class="flex flex-shrink-0 gap-2 items-center h-14 px-4 border-b border-[var(--el-border-color-lighter)]"
|
||||||
>
|
>
|
||||||
<el-input v-model="keyword" placeholder="搜索" clearable class="flex-1">
|
<el-input v-model="keyword" placeholder="搜索" clearable class="flex-1">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
|
|
|
||||||
|
|
@ -154,18 +154,6 @@ export const playAudioTip = () => {
|
||||||
|
|
||||||
// ==================== 管理后台展示工具 ====================
|
// ==================== 管理后台展示工具 ====================
|
||||||
|
|
||||||
/** 消息内容(JSON)取首层 content 字段做列表预览,解析失败时回退原文 */
|
|
||||||
export const getContentPreview = (content?: string): string => {
|
|
||||||
if (!content) return ''
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(content)
|
|
||||||
if (typeof parsed === 'object' && parsed.content) return String(parsed.content)
|
|
||||||
return content
|
|
||||||
} catch {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 详情弹窗里把 content JSON 美化成 2 缩进 */
|
/** 详情弹窗里把 content JSON 美化成 2 缩进 */
|
||||||
export const formatJson = (content?: string): string => {
|
export const formatJson = (content?: string): string => {
|
||||||
if (!content) return ''
|
if (!content) return ''
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue