✨ feat(im): 将 style 尽量多的改成 unocss,ai 友好
parent
fc812aef26
commit
0a07d4a2e4
|
|
@ -16,7 +16,7 @@
|
|||
:style="{ left: adjustedPosition.x + 'px', top: adjustedPosition.y + 'px' }"
|
||||
>
|
||||
<template v-for="(item, index) in contextMenu.items" :key="item.key">
|
||||
<!-- divided 项上方插一条分割线(首项跳过,避免空白);用 bg+h-[1px] 而非 border,UnoCSS 不带 border-style preflight -->
|
||||
<!-- divided 项上方插一条分割线(首项跳过,避免空白) -->
|
||||
<div
|
||||
v-if="item.divided && index > 0"
|
||||
class="my-1 mx-2 h-[1px] bg-[var(--el-border-color-lighter)]"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
- 拖拽区在右边缘,鼠标变 col-resize
|
||||
-->
|
||||
<aside
|
||||
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)]"
|
||||
class="relative flex flex-col shrink-0 bg-[var(--el-fill-color-light)] border-r border-r-solid border-[var(--el-border-color-lighter)] shadow-[2px_0_8px_rgba(0,0,0,0.05)]"
|
||||
:style="{ width: asideWidth + 'px' }"
|
||||
>
|
||||
<slot></slot>
|
||||
|
|
@ -102,8 +102,7 @@ function stopResize() {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 拖拽手柄的 hover / 拖拽中变色:UnoCSS 同时控制"handle 状态 → 子 line 样式"的选择器链比较绕,
|
||||
用 scoped CSS 直接描述更清晰 */
|
||||
/* hover / 拖拽中 把内部 line 加粗变深,提示手柄可拖;状态在父 handle 上 → 通过后代选择器联动子 line */
|
||||
.im-resizable-aside__handle:hover .im-resizable-aside__line,
|
||||
.im-resizable-aside__handle.is-resizing .im-resizable-aside__line {
|
||||
width: 3px;
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const goProfile = () => router.push({ name: 'Profile' })
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* el-badge 子组件内部类 UnoCSS 够不到,单独贴一条 :deep 覆盖 */
|
||||
/* :deep 穿透 el-badge 子组件内部 .el-badge__content;右上角红点位置 + 去掉描边 */
|
||||
.tool-bar__badge :deep(.el-badge__content) {
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
- 整卡 click 由调用方监听(@click),组件不内嵌业务逻辑
|
||||
-->
|
||||
<div
|
||||
class="flex flex-col w-[240px] rounded-md overflow-hidden bg-[var(--el-bg-color)] border border-[var(--el-border-color-lighter)]"
|
||||
class="flex flex-col w-[240px] rounded-md overflow-hidden bg-[var(--el-bg-color)] border border-solid border-[var(--el-border-color-lighter)]"
|
||||
:class="{ 'cursor-pointer': clickable }"
|
||||
>
|
||||
<div class="flex gap-2.5 items-center px-3 py-2.5">
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="px-3 py-1 text-12px border-t text-[var(--el-text-color-placeholder)] border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-lighter)]"
|
||||
class="px-3 py-1 text-12px border-t border-t-solid text-[var(--el-text-color-placeholder)] border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
{{ labelInfo.label }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<div
|
||||
v-for="user in visibleUsers"
|
||||
:key="user.id"
|
||||
class="flex gap-3 items-center px-2 py-2.5 border-b border-[var(--el-border-color-lighter)]"
|
||||
class="flex gap-3 items-center px-2 py-2.5 border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<UserAvatar
|
||||
:id="user.id"
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ async function handleOk() {
|
|||
<style scoped lang="scss">
|
||||
@use '../picker/picker-dialog' as picker;
|
||||
|
||||
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||
.im-picker-dialog {
|
||||
@include picker.styles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ async function handleOk() {
|
|||
<style scoped lang="scss">
|
||||
@use '../picker/picker-dialog' as picker;
|
||||
|
||||
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||
.im-picker-dialog {
|
||||
@include picker.styles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ async function handleOk() {
|
|||
<style scoped lang="scss">
|
||||
@use '../picker/picker-dialog' as picker;
|
||||
|
||||
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||
.im-picker-dialog {
|
||||
@include picker.styles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ async function handleOk() {
|
|||
<style scoped lang="scss">
|
||||
@use '../picker/picker-dialog' as picker;
|
||||
|
||||
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||
.im-picker-dialog {
|
||||
@include picker.styles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ async function handleOk() {
|
|||
<style scoped lang="scss">
|
||||
@use '../picker/picker-dialog' as picker;
|
||||
|
||||
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||
.im-picker-dialog {
|
||||
@include picker.styles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,27 +12,33 @@
|
|||
:close-on-click-modal="false"
|
||||
class="im-group-request-list__dialog"
|
||||
>
|
||||
<div v-loading="loading" class="im-group-request-list__body">
|
||||
<div
|
||||
v-loading="loading"
|
||||
class="flex flex-col gap-3 max-h-[60vh] overflow-y-auto pr-1"
|
||||
>
|
||||
<!-- 空态 -->
|
||||
<el-empty v-if="!loading && list.length === 0" description="暂无进群申请" />
|
||||
|
||||
<!-- 顶部卡片:最新一条 -->
|
||||
<div v-if="latest" class="im-group-request-list__card">
|
||||
<div class="im-group-request-list__row">
|
||||
<div
|
||||
v-if="latest"
|
||||
class="flex flex-col gap-2.5 p-3.5 rounded-[10px] border border-solid border-[var(--el-border-color-lighter)] bg-[var(--el-bg-color)] shadow-[0_1px_3px_rgba(0,0,0,0.04)]"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<UserAvatar
|
||||
:url="latest.userAvatar"
|
||||
:name="latest.userNickname"
|
||||
:size="44"
|
||||
:clickable="false"
|
||||
/>
|
||||
<div class="im-group-request-list__main">
|
||||
<div class="im-group-request-list__name truncate">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="truncate text-sm font-medium leading-[1.4] text-[var(--el-text-color-primary)]">
|
||||
{{ latest.userNickname || `用户 ${latest.userId}` }}
|
||||
</div>
|
||||
<div class="im-group-request-list__source truncate">
|
||||
<div class="truncate mt-[2px] text-xs leading-[1.5] text-[var(--el-text-color-secondary)]">
|
||||
<template v-if="latest.inviterUserId">
|
||||
通过
|
||||
<span class="im-group-request-list__inviter">
|
||||
<span class="text-[var(--el-color-primary)]">
|
||||
{{ latest.inviterNickname || `用户 ${latest.inviterUserId}` }}
|
||||
</span>
|
||||
的邀请进群
|
||||
|
|
@ -42,17 +48,17 @@
|
|||
</div>
|
||||
<span
|
||||
v-if="latest.handleResult === ImGroupRequestHandleResult.AGREED"
|
||||
class="im-group-request-list__done"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--el-text-color-placeholder)]"
|
||||
>
|
||||
已同意
|
||||
</span>
|
||||
<span
|
||||
v-else-if="latest.handleResult === ImGroupRequestHandleResult.REFUSED"
|
||||
class="im-group-request-list__done"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--el-text-color-placeholder)]"
|
||||
>
|
||||
已拒绝
|
||||
</span>
|
||||
<div v-else class="im-group-request-list__actions">
|
||||
<div v-else class="flex gap-1.5 flex-shrink-0">
|
||||
<button
|
||||
class="im-group-request-list__btn im-group-request-list__btn--primary"
|
||||
:disabled="actingId === latest.id"
|
||||
|
|
@ -70,8 +76,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- 申请理由:邀请场景显示邀请人 + 留言;主动申请显示申请人 + 留言 -->
|
||||
<div v-if="latest.applyContent" class="im-group-request-list__quote">
|
||||
<span class="im-group-request-list__quote-name">
|
||||
<div
|
||||
v-if="latest.applyContent"
|
||||
class="px-3 py-2 rounded-md text-[13px] leading-[1.5] break-all bg-[var(--el-fill-color-light)] text-[var(--el-text-color-regular)]"
|
||||
>
|
||||
<span class="text-[var(--el-color-primary)]">
|
||||
{{
|
||||
latest.inviterUserId
|
||||
? latest.inviterNickname || `用户 ${latest.inviterUserId}`
|
||||
|
|
@ -83,7 +92,10 @@
|
|||
</div>
|
||||
|
||||
<!-- 分割线:仅在有更早申请时出现 -->
|
||||
<div v-if="histories.length > 0" class="im-group-request-list__divider">
|
||||
<div
|
||||
v-if="histories.length > 0"
|
||||
class="flex items-center justify-center mt-1.5 -mb-0.5 text-xs text-[var(--el-text-color-placeholder)]"
|
||||
>
|
||||
<span>以下为更早的申请</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -91,23 +103,23 @@
|
|||
<div
|
||||
v-for="item in histories"
|
||||
:key="item.id"
|
||||
class="im-group-request-list__card im-group-request-list__card--compact"
|
||||
class="flex flex-col gap-2.5 px-3.5 py-2.5 rounded-[10px] border border-solid border-[var(--el-border-color-lighter)] bg-[var(--el-bg-color)] shadow-[0_1px_3px_rgba(0,0,0,0.04)]"
|
||||
>
|
||||
<div class="im-group-request-list__row">
|
||||
<div class="flex items-center gap-3">
|
||||
<UserAvatar
|
||||
:url="item.userAvatar"
|
||||
:name="item.userNickname"
|
||||
:size="40"
|
||||
:clickable="false"
|
||||
/>
|
||||
<div class="im-group-request-list__main">
|
||||
<div class="im-group-request-list__name truncate">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="truncate text-sm font-medium leading-[1.4] text-[var(--el-text-color-primary)]">
|
||||
{{ item.userNickname || `用户 ${item.userId}` }}
|
||||
</div>
|
||||
<div class="im-group-request-list__source truncate">
|
||||
<div class="truncate mt-[2px] text-xs leading-[1.5] text-[var(--el-text-color-secondary)]">
|
||||
<template v-if="item.inviterUserId">
|
||||
通过
|
||||
<span class="im-group-request-list__inviter">
|
||||
<span class="text-[var(--el-color-primary)]">
|
||||
{{ item.inviterNickname || `用户 ${item.inviterUserId}` }}
|
||||
</span>
|
||||
的邀请进群
|
||||
|
|
@ -117,17 +129,17 @@
|
|||
</div>
|
||||
<span
|
||||
v-if="item.handleResult === ImGroupRequestHandleResult.AGREED"
|
||||
class="im-group-request-list__done"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--el-text-color-placeholder)]"
|
||||
>
|
||||
已同意
|
||||
</span>
|
||||
<span
|
||||
v-else-if="item.handleResult === ImGroupRequestHandleResult.REFUSED"
|
||||
class="im-group-request-list__done"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--el-text-color-placeholder)]"
|
||||
>
|
||||
已拒绝
|
||||
</span>
|
||||
<div v-else class="im-group-request-list__actions">
|
||||
<div v-else class="flex gap-1.5 flex-shrink-0">
|
||||
<button
|
||||
class="im-group-request-list__btn im-group-request-list__btn--primary"
|
||||
:disabled="actingId === item.id"
|
||||
|
|
@ -281,88 +293,7 @@ function updateLocalResult(id: number, handleResult: number) {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-group-request-list__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.im-group-request-list__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.im-group-request-list__card--compact {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.im-group-request-list__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.im-group-request-list__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.im-group-request-list__name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.im-group-request-list__source {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.im-group-request-list__inviter {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.im-group-request-list__quote {
|
||||
padding: 8px 12px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.im-group-request-list__quote-name {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.im-group-request-list__divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 6px 0 -2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.im-group-request-list__actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 自绘按钮:贴近微信小药丸样式;el-button 默认尺寸偏大、圆角偏方 */
|
||||
/* 自绘按钮:贴近微信小药丸样式;与 :disabled、:hover:not(:disabled) 等伪类叠加 modifier 类的组合选择器写在 class 里成本高,留 SCSS */
|
||||
.im-group-request-list__btn {
|
||||
flex-shrink: 0;
|
||||
min-width: 56px;
|
||||
|
|
@ -401,15 +332,10 @@ function updateLocalResult(id: number, handleResult: number) {
|
|||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.im-group-request-list__done {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* el-dialog 内部 body 通过 teleport 渲染到 body,scoped 选不到,留非 scoped 全局覆盖 */
|
||||
.im-group-request-list__dialog .el-dialog__body {
|
||||
padding: 12px 20px 8px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
- Panel 不带 el-dialog 壳;dialog 由业务壳持有
|
||||
- footer slot 渲染在右栏已选列表下方,业务壳放预览卡 / 留言 / 提交按钮
|
||||
-->
|
||||
<div class="flex h-full im-conversation-picker">
|
||||
<div class="flex h-full">
|
||||
<!-- 左栏 -->
|
||||
<div
|
||||
class="flex flex-col w-[280px] border-r border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]"
|
||||
class="flex flex-col w-[280px] border-r border-r-solid border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]"
|
||||
>
|
||||
<!-- 搜索框 -->
|
||||
<div class="flex-shrink-0 px-3 py-2">
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
<!-- 移除模式:右上角 × 圆角标,点击把这条 key 从 recentForwardConversationKeys 删掉 -->
|
||||
<span
|
||||
v-if="recentRemoveMode"
|
||||
class="flex absolute -top-1 -right-1 justify-center items-center w-4 h-4 rounded-full cursor-pointer im-conversation-picker__recent-remove"
|
||||
class="flex absolute -top-1 -right-1 justify-center items-center w-4 h-4 rounded-full cursor-pointer bg-[var(--el-fill-color-dark)] text-[var(--el-text-color-primary)]"
|
||||
@click.stop="emit('remove-recent', getConversationKey(conversation))"
|
||||
>
|
||||
<Icon icon="ant-design:close-outlined" :size="10" />
|
||||
|
|
@ -71,8 +71,8 @@
|
|||
class="flex absolute -top-1 -right-1 justify-center items-center w-4 h-4 rounded-full transition-colors"
|
||||
:class="
|
||||
isSelected(conversation)
|
||||
? 'im-conversation-picker__recent-badge'
|
||||
: 'border border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
? 'bg-[#07c160] border border-solid border-[#07c160]'
|
||||
: 'border border-solid border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
"
|
||||
>
|
||||
<Icon
|
||||
|
|
@ -121,8 +121,8 @@
|
|||
class="flex flex-shrink-0 justify-center items-center w-5 h-5 rounded-full transition-colors"
|
||||
:class="
|
||||
isSelected(conversation)
|
||||
? 'im-conversation-picker__check--checked'
|
||||
: 'border border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
? 'bg-[#07c160] border border-solid border-[#07c160]'
|
||||
: 'border border-solid border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
"
|
||||
>
|
||||
<Icon
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<!-- 标题:选 0/1「发送给」、多个「分别发送给」(与微信文案一致) -->
|
||||
<div
|
||||
class="flex-shrink-0 px-4 py-3 border-b text-13px text-[var(--el-text-color-secondary)] border-[var(--el-border-color-lighter)]"
|
||||
class="flex-shrink-0 px-4 py-3 border-b border-b-solid text-13px text-[var(--el-text-color-secondary)] border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
{{ sendTitle }}
|
||||
</div>
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
<Icon
|
||||
icon="ant-design:close-outlined"
|
||||
:size="14"
|
||||
class="flex-shrink-0 cursor-pointer im-conversation-picker__remove"
|
||||
class="flex-shrink-0 cursor-pointer transition-colors text-[var(--el-text-color-placeholder)] hover:text-[var(--el-color-danger)]"
|
||||
@click="handleToggle(conversation)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -216,7 +216,7 @@
|
|||
<!-- 业务壳塞预览卡 / 留言 / 提交按钮的位置 -->
|
||||
<div
|
||||
v-if="$slots.footer"
|
||||
class="flex-shrink-0 border-t border-[var(--el-border-color-lighter)]"
|
||||
class="flex-shrink-0 border-t border-t-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
|
|
@ -352,20 +352,7 @@ function handleToggle(conversation: Conversation) {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 选中态圆形指示器:微信绿底色 + 白对勾,不污染主题色 */
|
||||
.im-conversation-picker__check--checked,
|
||||
.im-conversation-picker__recent-badge {
|
||||
background-color: #07c160;
|
||||
border: 1px solid #07c160;
|
||||
}
|
||||
|
||||
/* 最近转发移除模式的 × 角标:浅灰底 + 次要字色 */
|
||||
.im-conversation-picker__recent-remove {
|
||||
background-color: var(--el-fill-color-dark);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
/* 最近转发头像横向滚动条做窄一点,避免占视觉 */
|
||||
/* 横向滚动条做窄一点避免占视觉;走 ::-webkit-scrollbar 浏览器伪元素 */
|
||||
.im-conversation-picker__recent::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
|
@ -373,13 +360,4 @@ function handleToggle(conversation: Conversation) {
|
|||
background-color: var(--el-border-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 已选行 × 移除:常驻浅灰,hover 转危险色 */
|
||||
.im-conversation-picker__remove {
|
||||
color: var(--el-text-color-placeholder);
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.im-conversation-picker__remove:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
- Panel 不带 el-dialog 壳;dialog 由业务壳持有
|
||||
- 三态语义:hide > locked > disabled(详见 contract)
|
||||
-->
|
||||
<div class="flex h-full im-friend-picker">
|
||||
<div class="flex h-full">
|
||||
<!-- 左栏 -->
|
||||
<div
|
||||
class="flex flex-col flex-1 min-w-0 border-r border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]"
|
||||
class="flex flex-col flex-1 min-w-0 border-r border-r-solid border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]"
|
||||
>
|
||||
<!-- 搜索框 -->
|
||||
<div class="flex-shrink-0 px-3 py-2">
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<!-- 标题:已选数;高度对齐左侧 input default(32px),保证两侧第一项起点同水平 -->
|
||||
<div
|
||||
class="flex-shrink-0 h-12 px-4 leading-[3rem] border-b text-13px text-[var(--el-text-color-secondary)] border-[var(--el-border-color-lighter)]"
|
||||
class="flex-shrink-0 h-12 px-4 leading-[3rem] border-b border-b-solid text-13px text-[var(--el-text-color-secondary)] border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
已选择 {{ selectedCount }} 个好友
|
||||
</div>
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
v-if="!isLocked(friend)"
|
||||
icon="ant-design:close-outlined"
|
||||
:size="14"
|
||||
class="flex-shrink-0 cursor-pointer im-friend-picker__remove"
|
||||
class="flex-shrink-0 cursor-pointer transition-colors text-[var(--el-text-color-placeholder)] hover:text-[var(--el-color-danger)]"
|
||||
@click="handleToggle(friend)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
<!-- 业务壳塞额外内容的位置;FriendPickerPanel 主流场景不需要 footer -->
|
||||
<div
|
||||
v-if="$slots.footer"
|
||||
class="flex-shrink-0 border-t border-[var(--el-border-color-lighter)]"
|
||||
class="flex-shrink-0 border-t border-t-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
|
|
@ -224,12 +224,12 @@ function isSelected(friend: FriendLite): boolean {
|
|||
/** 圆形勾选指示器的 class:选中 / 锁定走绿底,禁用灰底,未选空心圆 */
|
||||
function getCheckClass(friend: FriendLite): string {
|
||||
if (isLocked(friend) || isSelected(friend)) {
|
||||
return 'im-friend-picker__check--checked'
|
||||
return 'bg-[#07c160] border border-solid border-[#07c160]'
|
||||
}
|
||||
if (isDisabled(friend)) {
|
||||
return 'im-friend-picker__check--disabled'
|
||||
return 'bg-[var(--el-fill-color)] border border-solid border-[var(--el-border-color)]'
|
||||
}
|
||||
return 'border border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
return 'border border-solid border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
}
|
||||
|
||||
/** 切换选中态:locked / disabled 不响应;右栏 × 移除 / 行 click 都走这里 */
|
||||
|
|
@ -252,25 +252,3 @@ function handleToggle(friend: FriendLite) {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 选中 / 锁定圆形指示器:微信绿底 + 白对勾,不污染主题色 */
|
||||
.im-friend-picker__check--checked {
|
||||
background-color: #07c160;
|
||||
border: 1px solid #07c160;
|
||||
}
|
||||
|
||||
/* 禁用项:浅灰底 + 浅灰边,提示「不可选」 */
|
||||
.im-friend-picker__check--disabled {
|
||||
background-color: var(--el-fill-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
/* 已选行 × 移除:常驻浅灰,hover 转危险色 */
|
||||
.im-friend-picker__remove {
|
||||
color: var(--el-text-color-placeholder);
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.im-friend-picker__remove:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
- Panel 不带 el-dialog 壳;dialog 由业务壳持有
|
||||
- 三态语义:hide > locked > disabled
|
||||
-->
|
||||
<div class="flex h-full im-group-member-picker">
|
||||
<div class="flex h-full">
|
||||
<!-- 左栏 -->
|
||||
<div
|
||||
class="flex flex-col flex-1 min-w-0 border-r border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]"
|
||||
class="flex flex-col flex-1 min-w-0 border-r border-r-solid border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]"
|
||||
>
|
||||
<!-- 搜索框 -->
|
||||
<div class="flex-shrink-0 px-3 py-2">
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<!-- 标题:已选数;高度对齐左侧 input default(32px) -->
|
||||
<div
|
||||
class="flex-shrink-0 h-12 px-4 leading-[3rem] border-b text-13px text-[var(--el-text-color-secondary)] border-[var(--el-border-color-lighter)]"
|
||||
class="flex-shrink-0 h-12 px-4 leading-[3rem] border-b border-b-solid text-13px text-[var(--el-text-color-secondary)] border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
已选择 {{ selectedCount }} 位成员
|
||||
</div>
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
v-if="!isLocked(member)"
|
||||
icon="ant-design:close-outlined"
|
||||
:size="14"
|
||||
class="flex-shrink-0 cursor-pointer im-group-member-picker__remove"
|
||||
class="flex-shrink-0 cursor-pointer transition-colors text-[var(--el-text-color-placeholder)] hover:text-[var(--el-color-danger)]"
|
||||
@click="handleToggle(member)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -205,12 +205,12 @@ function isSelected(member: GroupMemberLite): boolean {
|
|||
/** 圆形勾选指示器的 class */
|
||||
function getCheckClass(member: GroupMemberLite): string {
|
||||
if (isLocked(member) || isSelected(member)) {
|
||||
return 'im-group-member-picker__check--checked'
|
||||
return 'bg-[#07c160] border border-solid border-[#07c160]'
|
||||
}
|
||||
if (isDisabled(member)) {
|
||||
return 'im-group-member-picker__check--disabled'
|
||||
return 'bg-[var(--el-fill-color)] border border-solid border-[var(--el-border-color)]'
|
||||
}
|
||||
return 'border border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
return 'border border-solid border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
}
|
||||
|
||||
/** 切换选中态:locked / disabled 不响应;右栏 × / 行 click 都走这里 */
|
||||
|
|
@ -233,22 +233,3 @@ function handleToggle(member: GroupMemberLite) {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-group-member-picker__check--checked {
|
||||
background-color: #07c160;
|
||||
border: 1px solid #07c160;
|
||||
}
|
||||
|
||||
.im-group-member-picker__check--disabled {
|
||||
background-color: var(--el-fill-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.im-group-member-picker__remove {
|
||||
color: var(--el-text-color-placeholder);
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.im-group-member-picker__remove:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ function handleOk() {
|
|||
<style scoped lang="scss">
|
||||
@use '../picker/picker-dialog' as picker;
|
||||
|
||||
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||
.im-picker-dialog {
|
||||
@include picker.styles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ const audioRef = useMediaStreamElement<HTMLAudioElement>(() => props.participant
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 三点淡入淡出动画;@keyframes 必须 CSS 定义,再由 UnoCSS 之外的类名引用 */
|
||||
/* 三点淡入淡出动画;@keyframes 必须 CSS 定义 */
|
||||
.tile-dot {
|
||||
animation: tile-dot 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ const formattedDuration = computed(() =>
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 重连小点淡入淡出;@keyframes 必须 CSS 定义,再由非 UnoCSS 类名引用 */
|
||||
/* 重连小点淡入淡出;@keyframes 必须 CSS 定义 */
|
||||
.reconnect-dot {
|
||||
animation: reconnect-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
v-if="view === 'contact'"
|
||||
icon="ant-design:arrow-left-outlined"
|
||||
:size="16"
|
||||
class="cursor-pointer im-recommend-dialog__back"
|
||||
class="cursor-pointer text-[var(--el-text-color-secondary)] transition-colors duration-150 hover:text-[var(--el-color-primary)]"
|
||||
@click="view = 'conversation'"
|
||||
/>
|
||||
<span class="text-base text-[var(--el-text-color-primary)]">
|
||||
|
|
@ -315,16 +315,8 @@ async function handleCreateGroupAndSend() {
|
|||
<style scoped lang="scss">
|
||||
@use '../picker/picker-dialog' as picker;
|
||||
|
||||
/* :deep 穿透 el-dialog 内部类;复用 picker 公共 mixin */
|
||||
.im-picker-dialog {
|
||||
@include picker.styles;
|
||||
}
|
||||
|
||||
/* 返回箭头 hover 高亮,提示可点击 */
|
||||
.im-recommend-dialog__back {
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.im-recommend-dialog__back:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<ResizableAside :default-width="260" :storage-key="StorageKeys.asideWidth">
|
||||
<!-- 顶部:仅搜索框;h-14 与消息 Tab 顶部对齐,避免切换时搜索框上下抖动 -->
|
||||
<div
|
||||
class="flex flex-shrink-0 items-center h-14 px-4 border-b border-[var(--el-border-color-lighter)]"
|
||||
class="flex flex-shrink-0 items-center h-14 px-4 border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<el-input v-model="keyword" placeholder="搜索" clearable class="flex-1">
|
||||
<template #prefix>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
append-to-body
|
||||
modal-class="im-conversation-group-side__modal"
|
||||
>
|
||||
<div v-if="group" class="im-conversation-group-side flex flex-col h-full">
|
||||
<div v-if="group" class="flex flex-col h-full bg-[var(--el-bg-color)]">
|
||||
<!-- 上部:可滚动内容区 -->
|
||||
<div class="im-conversation-group-side__scroll flex-1 overflow-y-auto">
|
||||
<div class="flex-1 overflow-y-auto bg-[var(--el-fill-color-light)]">
|
||||
<!-- ==================== 群成员区 ==================== -->
|
||||
<div class="im-conversation-group-side__section im-conversation-group-side__members">
|
||||
<div class="px-4 pt-4 pb-[10px] bg-[var(--el-bg-color)]">
|
||||
<el-input v-model="searchText" placeholder="搜索群成员" clearable>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
</template>
|
||||
</el-input>
|
||||
|
||||
<div class="im-conversation-group-side__grid">
|
||||
<div class="flex flex-wrap gap-x-1 gap-y-[14px] mt-[14px]">
|
||||
<GroupMemberGrid
|
||||
v-for="member in displayMembers"
|
||||
:key="member.userId"
|
||||
|
|
@ -34,34 +34,34 @@
|
|||
|
||||
<!-- 添加(任何成员都能邀请) -->
|
||||
<div
|
||||
class="im-conversation-group-side__tile-wrap"
|
||||
class="im-conversation-group-side__tile-wrap flex flex-col items-center w-[66px] cursor-pointer"
|
||||
title="邀请好友入群"
|
||||
@click="handleOpenInvite"
|
||||
>
|
||||
<div class="im-conversation-group-side__icon-tile">
|
||||
<div class="im-conversation-group-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200">
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
</div>
|
||||
<div class="im-conversation-group-side__tile-label">添加</div>
|
||||
<div class="mt-1.5 text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center">添加</div>
|
||||
</div>
|
||||
|
||||
<!-- 移出(群主或管理员;管理员只能移出普通成员,由后端校验) -->
|
||||
<div
|
||||
v-if="isOwnerOrAdmin"
|
||||
class="im-conversation-group-side__tile-wrap"
|
||||
class="im-conversation-group-side__tile-wrap flex flex-col items-center w-[66px] cursor-pointer"
|
||||
title="移出群成员"
|
||||
@click="handleOpenRemove"
|
||||
>
|
||||
<div class="im-conversation-group-side__icon-tile">
|
||||
<div class="im-conversation-group-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200">
|
||||
<Icon icon="ant-design:minus-outlined" />
|
||||
</div>
|
||||
<div class="im-conversation-group-side__tile-label">移出</div>
|
||||
<div class="mt-1.5 text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center">移出</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大群折叠:默认只展示前 N 个,点 "查看更多" 全展开(搜索时不折叠) -->
|
||||
<div
|
||||
v-if="moreMembersHidden"
|
||||
class="im-conversation-group-side__more"
|
||||
class="flex items-center justify-center gap-1 mt-[10px] pt-1.5 pb-0.5 text-12px text-[var(--el-text-color-secondary)] cursor-pointer transition-colors duration-150 hover:text-[var(--el-color-primary)]"
|
||||
@click="showAllMembers = true"
|
||||
>
|
||||
查看更多
|
||||
|
|
@ -69,11 +69,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im-conversation-group-side__spacer"></div>
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
|
||||
<!-- ==================== 群信息 ==================== -->
|
||||
<!-- label 在上、value 在下,纵向堆叠(对齐微信 PC 设计);只有 "群公告" 因为内容长加 > chevron -->
|
||||
<div class="im-conversation-group-side__section">
|
||||
<div class="bg-[var(--el-bg-color)]">
|
||||
<!-- 群聊名称(群主可改) -->
|
||||
<el-popover
|
||||
v-if="isOwner"
|
||||
|
|
@ -84,10 +84,10 @@
|
|||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--vertical im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">群聊名称</span>
|
||||
<span class="im-conversation-group-side__value truncate">{{ group.name }}</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群聊名称</span>
|
||||
<span class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] truncate">{{ group.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
@ -100,10 +100,10 @@
|
|||
</el-popover>
|
||||
<div
|
||||
v-else
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--vertical"
|
||||
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 transition-colors duration-150"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">群聊名称</span>
|
||||
<span class="im-conversation-group-side__value truncate">{{ group.name }}</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群聊名称</span>
|
||||
<span class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] truncate">{{ group.name }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 群公告(群主可改):内容可能很长,加 > chevron 表示可展开编辑 -->
|
||||
|
|
@ -116,23 +116,23 @@
|
|||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--vertical im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
<div class="im-conversation-group-side__row-header">
|
||||
<span class="im-conversation-group-side__label">群公告</span>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群公告</span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
class="im-conversation-group-side__chevron"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-if="group.notice"
|
||||
class="im-conversation-group-side__value im-conversation-group-side__value--clamp"
|
||||
class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] line-clamp-2"
|
||||
>
|
||||
{{ group.notice }}
|
||||
</span>
|
||||
<span v-else class="im-conversation-group-side__value-placeholder">未设置</span>
|
||||
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">未设置</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
@ -152,16 +152,16 @@
|
|||
</el-popover>
|
||||
<div
|
||||
v-else
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--vertical"
|
||||
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 transition-colors duration-150"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">群公告</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群公告</span>
|
||||
<span
|
||||
v-if="group.notice"
|
||||
class="im-conversation-group-side__value im-conversation-group-side__value--clamp"
|
||||
class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] line-clamp-2"
|
||||
>
|
||||
{{ group.notice }}
|
||||
</span>
|
||||
<span v-else class="im-conversation-group-side__value-placeholder">未设置</span>
|
||||
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">未设置</span>
|
||||
</div>
|
||||
|
||||
<!-- 备注(仅自己可见;保存后会替换会话列表 / 顶部群名展示) -->
|
||||
|
|
@ -173,16 +173,16 @@
|
|||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--vertical im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">备注</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">备注</span>
|
||||
<span
|
||||
v-if="group.groupRemark"
|
||||
class="im-conversation-group-side__value im-conversation-group-side__value--clamp"
|
||||
class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] line-clamp-2"
|
||||
>
|
||||
{{ group.groupRemark }}
|
||||
</span>
|
||||
<span v-else class="im-conversation-group-side__value-placeholder">
|
||||
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">
|
||||
群聊的备注仅自己可见
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -212,16 +212,16 @@
|
|||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--vertical im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">我在本群的昵称</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">我在本群的昵称</span>
|
||||
<span
|
||||
v-if="group.remarkNickName"
|
||||
class="im-conversation-group-side__value truncate"
|
||||
class="text-13px text-[var(--el-text-color-regular)] break-all leading-[1.6] truncate"
|
||||
>
|
||||
{{ group.remarkNickName }}
|
||||
</span>
|
||||
<span v-else class="im-conversation-group-side__value-placeholder">点击设置</span>
|
||||
<span v-else class="text-13px text-[var(--el-text-color-placeholder)] leading-[1.6]">点击设置</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
@ -234,55 +234,55 @@
|
|||
</el-popover>
|
||||
</div>
|
||||
|
||||
<div class="im-conversation-group-side__spacer"></div>
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
|
||||
<!-- ==================== 查找聊天内容 ==================== -->
|
||||
<!-- 点击 → 父组件打开 MessageHistory 弹窗 -->
|
||||
|
||||
<div class="im-conversation-group-side__spacer"></div>
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
|
||||
<div class="im-conversation-group-side__section">
|
||||
<div class="bg-[var(--el-bg-color)]">
|
||||
<div
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
@click="emit('open-history')"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">查找聊天内容</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">查找聊天内容</span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
class="im-conversation-group-side__chevron"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
<!-- 分享群名片:弹 RecommendCardDialog,把当前群作为名片消息推荐给其他会话 -->
|
||||
<div
|
||||
v-if="group"
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
@click="handleShareGroupCard"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">分享群名片</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">分享群名片</span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
class="im-conversation-group-side__chevron"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im-conversation-group-side__spacer"></div>
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
|
||||
<!-- ==================== 开关项 ==================== -->
|
||||
<div class="im-conversation-group-side__section">
|
||||
<div class="im-conversation-group-side__row">
|
||||
<span class="im-conversation-group-side__label">消息免打扰</span>
|
||||
<div class="bg-[var(--el-bg-color)]">
|
||||
<div class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">消息免打扰</span>
|
||||
<el-switch :model-value="!!conversation?.silent" @change="onMutedChange" />
|
||||
</div>
|
||||
<div class="im-conversation-group-side__row">
|
||||
<span class="im-conversation-group-side__label">置顶聊天</span>
|
||||
<div class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">置顶聊天</span>
|
||||
<el-switch :model-value="!!conversation?.top" @change="onTopChange" />
|
||||
</div>
|
||||
<!-- 全群禁言:仅群主或管理员可操作 -->
|
||||
<div v-if="isOwnerOrAdmin" class="im-conversation-group-side__row">
|
||||
<span class="im-conversation-group-side__label">全群禁言</span>
|
||||
<div v-if="isOwnerOrAdmin" class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">全群禁言</span>
|
||||
<el-switch :model-value="!!currentMutedAll" @change="onMuteAllChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -290,24 +290,24 @@
|
|||
<!-- ==================== 进群审批 ==================== -->
|
||||
<!-- 单独一段:群主开关 + 紧跟「- 进群申请」子项;与微信群管理布局对齐 -->
|
||||
<template v-if="isOwner || (isOwnerOrAdmin && !!group.joinApproval)">
|
||||
<div class="im-conversation-group-side__spacer"></div>
|
||||
<div class="im-conversation-group-side__section">
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
<div class="bg-[var(--el-bg-color)]">
|
||||
<!-- 进群审批:仅群主可操作;开启后普通成员的「申请」「邀请」路径都需群主 / 管理员同意;群主 / 管理员邀请直进 -->
|
||||
<div v-if="isOwner" class="im-conversation-group-side__row">
|
||||
<span class="im-conversation-group-side__label">进群需要群主 / 群管理确认</span>
|
||||
<div v-if="isOwner" class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">进群需要群主 / 群管理确认</span>
|
||||
<el-switch :model-value="!!group.joinApproval" @change="onJoinApprovalChange" />
|
||||
</div>
|
||||
<!-- 进群申请子项:仅当开启审批 + 当前用户是 owner / admin 时出现;点击进列表 dialog -->
|
||||
<div
|
||||
v-if="isOwnerOrAdmin && !!group.joinApproval"
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
@click="handleOpenRequestList"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">- 进群申请</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">- 进群申请</span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
class="im-conversation-group-side__chevron"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -316,28 +316,28 @@
|
|||
<!-- ==================== 群主操作 ==================== -->
|
||||
<!-- 仅群主可见,含管理员设置 + 群主管理权转让 -->
|
||||
<template v-if="isOwner">
|
||||
<div class="im-conversation-group-side__spacer"></div>
|
||||
<div class="im-conversation-group-side__section">
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
<div class="bg-[var(--el-bg-color)]">
|
||||
<div
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
@click="handleOpenAdminSet"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">群管理员</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群管理员</span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
class="im-conversation-group-side__chevron"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||
class="im-conversation-group-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
@click="handleOpenTransferOwner"
|
||||
>
|
||||
<span class="im-conversation-group-side__label">群主管理权转让</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">群主管理权转让</span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
class="im-conversation-group-side__chevron"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -345,11 +345,11 @@
|
|||
</div>
|
||||
|
||||
<!-- ==================== 底部:退出 / 解散群聊 ==================== -->
|
||||
<div class="im-conversation-group-side__footer">
|
||||
<div class="flex-shrink-0 px-4 pt-[14px] pb-[18px] bg-[var(--el-bg-color)] border-t border-t-solid border-[var(--el-border-color-lighter)]">
|
||||
<!-- 群主:解散群聊 -->
|
||||
<el-button
|
||||
v-if="isOwner"
|
||||
class="im-conversation-group-side__quit-btn"
|
||||
class="w-full !h-9 text-14px"
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDissolve"
|
||||
|
|
@ -359,7 +359,7 @@
|
|||
<!-- 非群主:退出群聊 -->
|
||||
<el-button
|
||||
v-else
|
||||
class="im-conversation-group-side__quit-btn"
|
||||
class="w-full !h-9 text-14px"
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleQuit"
|
||||
|
|
@ -796,174 +796,22 @@ function handleOpenTransferOwner() {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-conversation-group-side {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
/* 滚动区底色用浅灰,和 section 的白色形成 “块” 视觉,spacer 自动融入背景就不用单独画线 */
|
||||
.im-conversation-group-side__scroll {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.im-conversation-group-side__section {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
/* 群成员区:搜索 + 宫格 + 查看更多 */
|
||||
.im-conversation-group-side__members {
|
||||
padding: 16px 16px 10px;
|
||||
}
|
||||
|
||||
.im-conversation-group-side__grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px 4px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
/* 添加 / 移出 宫格 wrapper */
|
||||
.im-conversation-group-side__tile-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 66px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 添加 / 移出 的方块按钮:用浅底 + 虚线边,hover 走主色让交互可读 */
|
||||
.im-conversation-group-side__icon-tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 20px;
|
||||
color: var(--el-text-color-regular);
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
transition:
|
||||
color 0.18s,
|
||||
border-color 0.18s,
|
||||
background-color 0.18s;
|
||||
}
|
||||
/* 「添加 / 移出」瓦片:hover 时联动内部 icon-tile 走主色,跨子元素的 hover 联动无法用单元素工具类表达 */
|
||||
.im-conversation-group-side__tile-wrap:hover .im-conversation-group-side__icon-tile {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
.im-conversation-group-side__tile-label {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-regular);
|
||||
text-align: center;
|
||||
}
|
||||
/* el-icon 全局 color 在暗色模式下被主题盖过;:deep(svg) 锁 fill 到当前色 */
|
||||
|
||||
/* :deep 穿透 Icon 内部 svg; el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
|
||||
.im-conversation-group-side__icon-tile :deep(svg) {
|
||||
fill: currentColor !important;
|
||||
}
|
||||
|
||||
/* 查看更多 */
|
||||
.im-conversation-group-side__more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 0 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.im-conversation-group-side__more:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* section 之间的灰条:靠滚动区底色透出来即可,spacer 高度决定块间距 */
|
||||
.im-conversation-group-side__spacer {
|
||||
flex-shrink: 0;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* 信息行:默认左 label 右 value(开关行),__row--vertical 切到 label 在上、value 在下(编辑字段行) */
|
||||
.im-conversation-group-side__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 13px 16px;
|
||||
font-size: 14px;
|
||||
min-height: 24px;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
|
||||
.im-conversation-group-side__row + .im-conversation-group-side__row {
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.im-conversation-group-side__row--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.im-conversation-group-side__row--clickable:hover {
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
}
|
||||
.im-conversation-group-side__row--vertical {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.im-conversation-group-side__label {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
/* 编辑字段行的 header:label 居左,可选 chevron 居右 */
|
||||
.im-conversation-group-side__row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.im-conversation-group-side__value {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
word-break: break-all;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.im-conversation-group-side__value--clamp {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.im-conversation-group-side__value-placeholder {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* chevron 统一调成 placeholder 灰,避免 opacity hack 在暗色下偏色 */
|
||||
.im-conversation-group-side__chevron {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
/* 底部退出群聊:浅灰分隔条 + 内边距,给按钮喘气空间 */
|
||||
.im-conversation-group-side__footer {
|
||||
flex-shrink: 0;
|
||||
padding: 14px 16px 18px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.im-conversation-group-side__quit-btn {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- el-drawer 用 append-to-body 后被传送出当前 scoped 边界,scoped CSS 的 data-v 不会落到 body 上;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
/>
|
||||
<span
|
||||
v-show="!conversation.silent && conversation.unreadCount > 0"
|
||||
class="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1.5 text-11px leading-[18px] text-white text-center bg-[#f56c6c] border border-white dark:border-[var(--el-bg-color)] rounded-full box-border whitespace-nowrap"
|
||||
class="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1.5 text-11px leading-[18px] text-white text-center bg-[#f56c6c] border border-solid border-white dark:border-[var(--el-bg-color)] rounded-full box-border whitespace-nowrap"
|
||||
>
|
||||
{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
|
||||
</span>
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
type="primary"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="conversation-item__tag"
|
||||
class="conversation-item__tag flex-shrink-0 !h-[18px] !px-1 !leading-4"
|
||||
>
|
||||
群
|
||||
</el-tag>
|
||||
|
|
@ -269,13 +269,8 @@ function handleContextMenu(e: MouseEvent) {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* el-tag 内部尺寸走 CSS 变量,UnoCSS 的高度/内边距会被 el-tag 自身的样式覆盖,用 :deep 微调 */
|
||||
/* transition:none 是为了消掉 el-tag 切会话时 active 底色变化的渐变(看起来像闪烁) */
|
||||
/* 消掉 el-tag 切会话时 active 底色变化的渐变(看起来像闪烁); :deep 穿透 el-tag 自身样式 */
|
||||
.conversation-item__tag {
|
||||
flex-shrink: 0;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
line-height: 16px;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,39 +13,39 @@
|
|||
append-to-body
|
||||
modal-class="im-conversation-private-side__modal"
|
||||
>
|
||||
<div v-if="friend" class="im-conversation-private-side flex flex-col h-full">
|
||||
<div class="im-conversation-private-side__scroll flex-1 overflow-y-auto">
|
||||
<div v-if="friend" class="flex flex-col h-full bg-[var(--el-bg-color)]">
|
||||
<div class="flex-1 overflow-y-auto bg-[var(--el-fill-color-light)]">
|
||||
<!-- 好友宫格:原 tile + "+" tile,对齐 GroupSide 视觉,让两种抽屉看起来是一家的 -->
|
||||
<div class="im-conversation-private-side__section im-conversation-private-side__friend">
|
||||
<div class="im-conversation-private-side__tile-wrap">
|
||||
<div class="flex flex-wrap gap-1 px-4 pt-4 pb-[14px] bg-[var(--el-bg-color)]">
|
||||
<div class="flex flex-col items-center w-[66px]">
|
||||
<UserAvatar
|
||||
:id="friend.friendUserId"
|
||||
:url="friend.avatar"
|
||||
:name="friend.nickname"
|
||||
:size="50"
|
||||
/>
|
||||
<div class="im-conversation-private-side__tile-label">
|
||||
<div class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center truncate">
|
||||
{{ displayName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- + tile:点击调起 GroupCreateDialog,把对方 id 作为 lockedIds 传入 -->
|
||||
<div
|
||||
class="im-conversation-private-side__tile-wrap im-conversation-private-side__tile-wrap--clickable"
|
||||
class="im-conversation-private-side__tile-wrap-clickable flex flex-col items-center w-[66px] cursor-pointer"
|
||||
title="发起群聊"
|
||||
@click="handleOpenCreateGroup"
|
||||
>
|
||||
<div class="im-conversation-private-side__icon-tile">
|
||||
<div class="im-conversation-private-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--el-text-color-regular)] bg-[var(--el-fill-color-lighter)] border border-dashed border-[var(--el-border-color)] rounded-md transition-colors duration-200">
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
</div>
|
||||
<div class="im-conversation-private-side__tile-label">添加</div>
|
||||
<div class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--el-text-color-regular)] text-center truncate">添加</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im-conversation-private-side__spacer"></div>
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
|
||||
<!-- 备注(仅自己可见):点击弹 popover 编辑,保存后立即刷新本抽屉 + 会话列表展示名 -->
|
||||
<div class="im-conversation-private-side__section">
|
||||
<div class="bg-[var(--el-bg-color)]">
|
||||
<el-popover
|
||||
v-model:visible="displayNamePopoverVisible"
|
||||
trigger="click"
|
||||
|
|
@ -54,16 +54,16 @@
|
|||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="im-conversation-private-side__row im-conversation-private-side__row--vertical im-conversation-private-side__row--clickable"
|
||||
class="im-conversation-private-side__row flex flex-col items-stretch gap-1.5 px-4 py-[14px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
<span class="im-conversation-private-side__label">备注</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">备注</span>
|
||||
<span
|
||||
v-if="friend.displayName"
|
||||
class="im-conversation-private-side__value im-conversation-private-side__value--clamp"
|
||||
class="text-13px leading-[1.6] text-[var(--el-text-color-regular)] break-all line-clamp-2"
|
||||
>
|
||||
{{ friend.displayName }}
|
||||
</span>
|
||||
<span v-else class="im-conversation-private-side__value-placeholder">
|
||||
<span v-else class="text-13px leading-[1.6] text-[var(--el-text-color-placeholder)]">
|
||||
好友备注仅自己可见
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -85,33 +85,33 @@
|
|||
</el-popover>
|
||||
</div>
|
||||
|
||||
<div class="im-conversation-private-side__spacer"></div>
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
|
||||
<!-- 查找聊天内容 -->
|
||||
<div class="im-conversation-private-side__section">
|
||||
<div class="bg-[var(--el-bg-color)]">
|
||||
<div
|
||||
class="im-conversation-private-side__row im-conversation-private-side__row--clickable"
|
||||
class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 cursor-pointer transition-colors duration-150 hover:bg-[var(--el-fill-color-lighter)]"
|
||||
@click="emit('open-history')"
|
||||
>
|
||||
<span class="im-conversation-private-side__label">查找聊天内容</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">查找聊天内容</span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
class="im-conversation-private-side__chevron"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im-conversation-private-side__spacer"></div>
|
||||
<div class="flex-shrink-0 h-[10px]"></div>
|
||||
|
||||
<!-- 开关项 -->
|
||||
<div class="im-conversation-private-side__section">
|
||||
<div class="im-conversation-private-side__row">
|
||||
<span class="im-conversation-private-side__label">消息免打扰</span>
|
||||
<div class="bg-[var(--el-bg-color)]">
|
||||
<div class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">消息免打扰</span>
|
||||
<el-switch :model-value="!!conversation?.silent" @change="handleMutedChange" />
|
||||
</div>
|
||||
<div class="im-conversation-private-side__row">
|
||||
<span class="im-conversation-private-side__label">置顶聊天</span>
|
||||
<div class="im-conversation-private-side__row flex items-center justify-between gap-3 px-4 py-[13px] text-14px min-h-6 transition-colors duration-150">
|
||||
<span class="flex-shrink-0 text-14px text-[var(--el-text-color-primary)]">置顶聊天</span>
|
||||
<el-switch :model-value="!!conversation?.top" @change="handleTopChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -250,137 +250,22 @@ function handleGroupCreated(groupId: number) {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-conversation-private-side {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
/* 滚动区底色用浅灰,配合 section 的白色形成 “块” 视觉,spacer 自动透出来当分隔条 */
|
||||
.im-conversation-private-side__scroll {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.im-conversation-private-side__section {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
/* 好友宫格区:留白和 GroupSide__members 对齐,friend tile + "+" tile 横排 */
|
||||
.im-conversation-private-side__friend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 16px 16px 14px;
|
||||
}
|
||||
|
||||
.im-conversation-private-side__tile-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 66px;
|
||||
}
|
||||
.im-conversation-private-side__tile-wrap--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.im-conversation-private-side__tile-label {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-regular);
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* "+" 加号 tile:浅底 + 虚线边,hover 走主色让交互可读,与 GroupSide 一致 */
|
||||
.im-conversation-private-side__icon-tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 20px;
|
||||
color: var(--el-text-color-regular);
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
transition:
|
||||
color 0.18s,
|
||||
border-color 0.18s,
|
||||
background-color 0.18s;
|
||||
}
|
||||
.im-conversation-private-side__tile-wrap--clickable:hover .im-conversation-private-side__icon-tile {
|
||||
/* 「+」 tile: hover 时联动内部 icon-tile 走主色; 跨子元素的 hover 联动无法用单元素工具类表达 */
|
||||
.im-conversation-private-side__tile-wrap-clickable:hover .im-conversation-private-side__icon-tile {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
/* el-icon 全局 color 在暗色模式下被主题盖过;:deep(svg) 锁 fill 到当前色 */
|
||||
|
||||
/* :deep 穿透 Icon 内部 svg; el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
|
||||
.im-conversation-private-side__icon-tile :deep(svg) {
|
||||
fill: currentColor !important;
|
||||
}
|
||||
|
||||
/* section 间隔条 */
|
||||
.im-conversation-private-side__spacer {
|
||||
flex-shrink: 0;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* 信息行:和 GroupSide 完全一致的尺寸 / hover,避免两个抽屉切换时 jitter */
|
||||
.im-conversation-private-side__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 13px 16px;
|
||||
font-size: 14px;
|
||||
min-height: 24px;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
|
||||
.im-conversation-private-side__row + .im-conversation-private-side__row {
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.im-conversation-private-side__row--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.im-conversation-private-side__row--clickable:hover {
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
}
|
||||
/* 备注行:label 在上、value 在下,编辑场景才用 */
|
||||
.im-conversation-private-side__row--vertical {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.im-conversation-private-side__label {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.im-conversation-private-side__value {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-regular);
|
||||
word-break: break-all;
|
||||
}
|
||||
.im-conversation-private-side__value--clamp {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.im-conversation-private-side__value-placeholder {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.im-conversation-private-side__chevron {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 同 GroupSide:el-drawer append-to-body 后 scoped CSS 的 data-v 不会落到 body 上,靠 modal-class 作祖先选择器写一段全局规则 -->
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
<div class="grid grid-cols-5 gap-2 p-3">
|
||||
<!-- 上传入口固定放第一格;dashed border 与表情格子区分视觉语义,对齐 el-upload 观感 -->
|
||||
<button
|
||||
class="im-face-upload-btn aspect-square flex items-center justify-center rounded-md cursor-pointer transition-colors"
|
||||
class="aspect-square flex items-center justify-center rounded-md cursor-pointer transition-colors border border-dashed border-[var(--el-border-color)] bg-transparent text-[var(--el-text-color-placeholder)] hover:border-[var(--el-color-primary)] hover:text-[var(--el-color-primary)] disabled:cursor-not-allowed disabled:text-[var(--el-text-color-disabled)]"
|
||||
type="button"
|
||||
:disabled="uploading"
|
||||
:title="uploading ? '上传中…' : '上传图片到个人表情'"
|
||||
|
|
@ -115,12 +115,16 @@
|
|||
<!-- 底部 tab 栏:[ emoji / 个人 / 系统包 1..N ];mode='emoji' 时隐藏 -->
|
||||
<div
|
||||
v-if="isFullMode"
|
||||
class="flex flex-shrink-0 items-center gap-1 px-2 py-1.5 border-t border-[var(--el-border-color-lighter)]"
|
||||
class="flex flex-shrink-0 items-center gap-1 px-2 py-1.5 border-t border-t-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<el-tooltip content="Emoji 表情" placement="top" :show-after="300">
|
||||
<button
|
||||
class="im-face-tab"
|
||||
:class="{ 'im-face-tab--active': activeTab === FACE_TAB.EMOJI }"
|
||||
class="inline-flex items-center justify-center w-[30px] h-[30px] border-none rounded bg-transparent cursor-pointer transition-colors hover:bg-[var(--el-fill-color)]"
|
||||
:class="
|
||||
activeTab === FACE_TAB.EMOJI
|
||||
? 'bg-[var(--el-fill-color)] text-[var(--el-color-primary)]'
|
||||
: 'text-[var(--el-text-color-regular)]'
|
||||
"
|
||||
type="button"
|
||||
@click="activeTab = FACE_TAB.EMOJI"
|
||||
>
|
||||
|
|
@ -129,8 +133,12 @@
|
|||
</el-tooltip>
|
||||
<el-tooltip content="个人表情" placement="top" :show-after="300">
|
||||
<button
|
||||
class="im-face-tab"
|
||||
:class="{ 'im-face-tab--active': activeTab === FACE_TAB.MINE }"
|
||||
class="inline-flex items-center justify-center w-[30px] h-[30px] border-none rounded bg-transparent cursor-pointer transition-colors hover:bg-[var(--el-fill-color)]"
|
||||
:class="
|
||||
activeTab === FACE_TAB.MINE
|
||||
? 'bg-[var(--el-fill-color)] text-[var(--el-color-primary)]'
|
||||
: 'text-[var(--el-text-color-regular)]'
|
||||
"
|
||||
type="button"
|
||||
@click="activeTab = FACE_TAB.MINE"
|
||||
>
|
||||
|
|
@ -145,8 +153,12 @@
|
|||
:show-after="300"
|
||||
>
|
||||
<button
|
||||
class="im-face-tab"
|
||||
:class="{ 'im-face-tab--active': activeTab === packTabKey(pack.id) }"
|
||||
class="inline-flex items-center justify-center w-[30px] h-[30px] border-none rounded bg-transparent cursor-pointer transition-colors hover:bg-[var(--el-fill-color)]"
|
||||
:class="
|
||||
activeTab === packTabKey(pack.id)
|
||||
? 'bg-[var(--el-fill-color)] text-[var(--el-color-primary)]'
|
||||
: 'text-[var(--el-text-color-regular)]'
|
||||
"
|
||||
type="button"
|
||||
@click="activeTab = packTabKey(pack.id)"
|
||||
>
|
||||
|
|
@ -339,42 +351,4 @@ onUnmounted(() => {
|
|||
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
|
||||
/* tab 按钮样式:被选中走主色高亮,鼠标悬停浅底 */
|
||||
.im-face-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: transparent;
|
||||
color: var(--el-text-color-regular);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.15s,
|
||||
color 0.15s;
|
||||
}
|
||||
.im-face-tab:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
.im-face-tab--active {
|
||||
background-color: var(--el-fill-color);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 个人表情上传按钮:dashed border 区分视觉语义,对齐 el-upload */
|
||||
.im-face-upload-btn {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
background-color: transparent;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
.im-face-upload-btn:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.im-face-upload-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--el-text-color-disabled);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,20 +7,19 @@
|
|||
<!-- 禁言 / 封禁覆盖层:优先级 封禁 > 全群禁言 > 成员禁言 -->
|
||||
<div
|
||||
v-if="muteOverlay"
|
||||
class="message-input__mute-overlay"
|
||||
:class="{
|
||||
'message-input__mute-overlay--banned': muteOverlay.icon === 'ant-design:stop-outlined'
|
||||
}"
|
||||
class="absolute top-2 right-3 bottom-3 left-3 z-10 flex items-center justify-center gap-2 rounded-lg border border-solid text-sm"
|
||||
:class="
|
||||
muteOverlay.icon === 'ant-design:stop-outlined'
|
||||
? 'text-[var(--el-color-danger-dark-2)] bg-[var(--el-color-danger-light-9)] border-[var(--el-color-danger-light-5)]'
|
||||
: 'text-[var(--el-color-warning-dark-2)] bg-[var(--el-color-warning-light-9)] border-[var(--el-color-warning-light-5)]'
|
||||
"
|
||||
>
|
||||
<Icon :icon="muteOverlay.icon" :size="18" />
|
||||
<span>{{ muteOverlay.text }}</span>
|
||||
</div>
|
||||
<!--
|
||||
内层白色圆角卡片 = editor + 工具栏;border + rounded 模拟微信"输入框"边界,
|
||||
避免之前"无框 Web 输入"的散开感;border 走 scoped CSS(UnoCSS 不带 border-style preflight)
|
||||
-->
|
||||
<!-- 内层白色圆角卡片 = editor + 工具栏;border + rounded 模拟微信「输入框」边界 -->
|
||||
<div
|
||||
class="relative flex flex-col bg-[var(--el-bg-color)] rounded-lg border border-[var(--el-border-color-lighter)]"
|
||||
class="relative flex flex-col bg-[var(--el-bg-color)] rounded-lg border border-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<!--
|
||||
输入区在上:contenteditable div(取代 textarea,对齐微信 PC:输入区在上,操作在下)
|
||||
|
|
@ -54,10 +53,10 @@
|
|||
底部工具栏:左侧操作图标 + 右侧发送按钮(对齐微信 PC:操作图标统一放底部)
|
||||
- relative 给 FacePicker 提供 absolute 锚点,picker 用 bottom-full 向上弹出
|
||||
- 图标统一 30×30 点击区(18px icon + p-1.5),gap-1 让间距贴合微信观感
|
||||
- border-t 在编辑区与工具栏之间画一条与 card 边框同色的细线(scoped CSS 避绕 UnoCSS preflight 缺失)
|
||||
- border-t 在编辑区与工具栏之间画一条与 card 边框同色的细线
|
||||
-->
|
||||
<div
|
||||
class="relative flex items-center justify-between gap-2 px-3 py-2 border-t border-[var(--el-border-color-lighter)]"
|
||||
class="relative flex items-center justify-between gap-2 px-3 py-2 border-t border-t-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<!--
|
||||
|
|
@ -1117,9 +1116,8 @@ async function onVideoPicked(e: Event) {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* el-icon 全局规则 .el-icon{color:var(--color,inherit); font-size:inherit; width:1em; height:1em}
|
||||
会盖过 UnoCSS 原子类;用字面选择器 + !important 兜底。
|
||||
颜色取 Element Plus 主题变量,暗色自动切到浅灰 */
|
||||
/* el-icon 全局规则 .el-icon{color:var(--color,inherit); font-size:inherit; width:1em; height:1em} 优先级更高,
|
||||
用字面选择器 + !important 锁死;颜色取 Element Plus 主题变量,暗色自动切到浅灰 */
|
||||
.message-input__tool,
|
||||
.message-input__tool:deep(svg) {
|
||||
font-size: 18px !important;
|
||||
|
|
@ -1143,26 +1141,4 @@ async function onVideoPicked(e: Event) {
|
|||
.message-input__editor :deep(.mention-token) {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 禁言 / 封禁覆盖层:绝对定位在外层容器上,遮挡整个输入卡片 */
|
||||
.message-input__mute-overlay {
|
||||
position: absolute;
|
||||
inset: 8px 12px 12px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-color-warning-dark-2);
|
||||
background-color: var(--el-color-warning-light-9);
|
||||
border: 1px solid var(--el-color-warning-light-5);
|
||||
}
|
||||
/* 封禁态:红底,区别于禁言的橙底 */
|
||||
.message-input__mute-overlay--banned {
|
||||
color: var(--el-color-danger-dark-2);
|
||||
background-color: var(--el-color-danger-light-9);
|
||||
border-color: var(--el-color-danger-light-5);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="multiSelect.state.active"
|
||||
class="im-multi-select-bar flex items-center justify-center gap-12 px-5 w-full h-full border-t bg-[var(--el-bg-color)] border-[var(--el-border-color-lighter)]"
|
||||
class="flex items-center justify-center gap-12 px-5 w-full h-full border-t border-t-solid bg-[var(--el-bg-color)] border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<span
|
||||
class="absolute left-5 top-1/2 -translate-y-1/2 text-12px text-[var(--el-text-color-secondary)]"
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
</span>
|
||||
|
||||
<button
|
||||
class="im-multi-select-bar__action"
|
||||
class="inline-flex flex-col items-center gap-1 px-3 py-1 text-12px rounded-md border-0 bg-transparent cursor-pointer transition-colors text-[var(--el-text-color-primary)] hover:text-[var(--el-color-primary)] hover:bg-[var(--el-fill-color)] disabled:text-[var(--el-text-color-disabled)] disabled:cursor-not-allowed disabled:bg-transparent"
|
||||
:disabled="selectedCount === 0"
|
||||
@click="handleForwardOneByOne"
|
||||
>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
class="im-multi-select-bar__action"
|
||||
class="inline-flex flex-col items-center gap-1 px-3 py-1 text-12px rounded-md border-0 bg-transparent cursor-pointer transition-colors text-[var(--el-text-color-primary)] hover:text-[var(--el-color-primary)] hover:bg-[var(--el-fill-color)] disabled:text-[var(--el-text-color-disabled)] disabled:cursor-not-allowed disabled:bg-transparent"
|
||||
:disabled="selectedCount === 0"
|
||||
@click="handleForwardMerged"
|
||||
>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
class="im-multi-select-bar__action im-multi-select-bar__action--danger"
|
||||
class="inline-flex flex-col items-center gap-1 px-3 py-1 text-12px rounded-md border-0 bg-transparent cursor-pointer transition-colors text-[var(--el-text-color-primary)] hover:bg-[var(--el-fill-color)] hover:text-[var(--el-color-danger)] disabled:text-[var(--el-text-color-disabled)] disabled:cursor-not-allowed disabled:bg-transparent"
|
||||
:disabled="selectedCount === 0"
|
||||
@click="handleDelete"
|
||||
>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
class="im-multi-select-bar__close absolute right-5 top-1/2 -translate-y-1/2"
|
||||
class="absolute right-5 top-1/2 -translate-y-1/2 inline-flex items-center justify-center w-7 h-7 rounded-full border-0 bg-transparent cursor-pointer transition-colors text-[var(--el-text-color-secondary)] hover:text-[var(--el-text-color-primary)] hover:bg-[var(--el-fill-color)]"
|
||||
@click="handleCancel"
|
||||
>
|
||||
<Icon icon="ant-design:close-outlined" :size="20" />
|
||||
|
|
@ -140,56 +140,3 @@ function handleCancel() {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-multi-select-bar__action {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s,
|
||||
background 0.15s;
|
||||
}
|
||||
|
||||
.im-multi-select-bar__action:hover:not(:disabled) {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
|
||||
.im-multi-select-bar__action:disabled {
|
||||
color: var(--el-text-color-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.im-multi-select-bar__action--danger:hover:not(:disabled) {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.im-multi-select-bar__close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: var(--el-text-color-secondary);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s,
|
||||
background 0.15s;
|
||||
}
|
||||
|
||||
.im-multi-select-bar__close:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
background: var(--el-fill-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ onUnmounted(() => {
|
|||
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
|
||||
/* 脉冲呼吸动画:keyframes 在 UnoCSS 原子类里不好表达,保留 scoped */
|
||||
/* 录音中的脉冲呼吸动画;@keyframes 必须 CSS 定义 */
|
||||
.im-voice-recorder__pulse {
|
||||
animation: im-voice-pulse 1s infinite;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,55 @@
|
|||
<template>
|
||||
<!-- 群聊置顶消息:仅群聊 + 有置顶时显示,悬挂在群聊头部下方左上角;不占整行(对齐微信 PC) -->
|
||||
<div v-if="pinnedMessages.length > 0" class="im-group-pinned-message">
|
||||
<div
|
||||
v-if="pinnedMessages.length > 0"
|
||||
class="im-group-pinned-message relative flex flex-shrink-0 flex-col items-start px-4 pt-1.5 pb-2 bg-[var(--el-fill-color-light)]"
|
||||
>
|
||||
<!-- 顶部胶囊:单条点击跳转;多条折叠点击展开;多条展开点击折叠 -->
|
||||
<div
|
||||
class="im-group-pinned-message__row im-group-pinned-message__row--clickable"
|
||||
class="flex items-center gap-1.5 w-[360px] px-3 py-1.5 rounded-[10px] text-13px text-[var(--el-text-color-primary)] bg-[var(--el-bg-color)] shadow-[0_1px_2px_rgba(0,0,0,0.04)] cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
|
||||
@click="handleTopClick"
|
||||
>
|
||||
<Icon icon="ant-design:pushpin-outlined" :size="14" class="im-group-pinned-message__icon" />
|
||||
<span class="im-group-pinned-message__sender">{{ getSenderName(latest) }}:</span>
|
||||
<span class="im-group-pinned-message__text">{{ getPreview(latest) }}</span>
|
||||
<Icon icon="ant-design:pushpin-outlined" :size="14" class="flex-shrink-0 text-[var(--el-color-warning)]" />
|
||||
<span class="flex-shrink-0 text-[var(--el-text-color-secondary)]">{{ getSenderName(latest) }}:</span>
|
||||
<span class="flex-1 min-w-0 truncate">{{ getPreview(latest) }}</span>
|
||||
<!-- 单条:移除按钮;多条折叠:共 N 条;多条展开:收起箭头 -->
|
||||
<span
|
||||
v-if="pinnedMessages.length === 1 && canManage"
|
||||
v-loading="removingId === latest.id"
|
||||
class="im-group-pinned-message__remove"
|
||||
class="flex-shrink-0 text-[var(--el-color-primary)] cursor-pointer text-13px hover:text-[var(--el-color-primary-light-3)]"
|
||||
@click.stop="handleRemove(latest)"
|
||||
>
|
||||
移除
|
||||
</span>
|
||||
<template v-else-if="pinnedMessages.length > 1">
|
||||
<span class="im-group-pinned-message__count">共 {{ pinnedMessages.length }} 条</span>
|
||||
<span class="flex-shrink-0 text-[var(--el-text-color-secondary)] text-12px">共 {{ pinnedMessages.length }} 条</span>
|
||||
<Icon
|
||||
:icon="expanded ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
|
||||
:size="11"
|
||||
class="im-group-pinned-message__chevron"
|
||||
class="flex-shrink-0 text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 多条展开:浅色面板包裹完整列表,每条独立胶囊;点击跳转到对应消息位置 -->
|
||||
<div v-if="pinnedMessages.length > 1 && expanded" class="im-group-pinned-message__list">
|
||||
<div
|
||||
v-if="pinnedMessages.length > 1 && expanded"
|
||||
class="im-group-pinned-message__list absolute top-full left-1.5 z-10 flex flex-col gap-2.5 w-[380px] p-3 rounded-xl bg-[var(--el-bg-color)] shadow-[0_6px_16px_rgba(0,0,0,0.12)]"
|
||||
style="margin-top: -1px;"
|
||||
>
|
||||
<div
|
||||
v-for="msg in pinnedMessages"
|
||||
:key="msg.id"
|
||||
class="im-group-pinned-message__row im-group-pinned-message__row--list im-group-pinned-message__row--clickable"
|
||||
class="flex items-center gap-1.5 w-full px-3 py-1.5 rounded-[10px] text-13px text-[var(--el-text-color-primary)] bg-[var(--el-fill-color-light)] cursor-pointer hover:bg-[var(--el-bg-color)]"
|
||||
@click="handleLocate(msg)"
|
||||
>
|
||||
<Icon icon="ant-design:pushpin-outlined" :size="14" class="im-group-pinned-message__icon" />
|
||||
<span class="im-group-pinned-message__sender">{{ getSenderName(msg) }}:</span>
|
||||
<span class="im-group-pinned-message__text">{{ getPreview(msg) }}</span>
|
||||
<Icon icon="ant-design:pushpin-outlined" :size="14" class="flex-shrink-0 text-[var(--el-color-warning)]" />
|
||||
<span class="flex-shrink-0 text-[var(--el-text-color-secondary)]">{{ getSenderName(msg) }}:</span>
|
||||
<span class="flex-1 min-w-0 truncate">{{ getPreview(msg) }}</span>
|
||||
<span
|
||||
v-if="canManage"
|
||||
v-loading="removingId === msg.id"
|
||||
class="im-group-pinned-message__remove"
|
||||
class="flex-shrink-0 text-[var(--el-color-primary)] cursor-pointer text-13px hover:text-[var(--el-color-primary-light-3)]"
|
||||
@click.stop="handleRemove(msg)"
|
||||
>
|
||||
移除
|
||||
|
|
@ -140,35 +147,7 @@ async function handleRemove(msg: Message) {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 容器:左对齐悬浮在 header 下方;不占整行;relative 让展开列表绝对定位贴顶部胶囊下方 */
|
||||
.im-group-pinned-message {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 6px 16px 8px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
/* 列表面板:绝对定位悬浮在顶部胶囊正下方;白色背景 + 强阴影跟 header 浅灰对比,呈现卡片层级感
|
||||
margin-top: -1px 让弹出层往上盖住 header bottom 那 1px 分隔线,避免视觉上有横向空隙 */
|
||||
.im-group-pinned-message__list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
margin-top: -1px;
|
||||
left: 6px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 380px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--el-bg-color);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
/* 弹出层箭头:朝上的三角,颜色跟弹出层 background 一致(白色),跟 header 浅灰强对比 */
|
||||
/* 弹出层朝上的三角箭头;走 ::before + 4 边 border 配色画,颜色跟弹出层 background 一致 */
|
||||
.im-group-pinned-message__list::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
|
@ -181,71 +160,4 @@ async function handleRemove(msg: Message) {
|
|||
border-bottom: 8px solid var(--el-bg-color);
|
||||
filter: drop-shadow(0 -2px 1px rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
|
||||
/* 胶囊基础样式:顶部固定 width;弹出层里的胶囊撑满外框宽度 */
|
||||
.im-group-pinned-message__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 360px;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.im-group-pinned-message__row--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.im-group-pinned-message__row--clickable:hover {
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
}
|
||||
/* 列表里的胶囊:撑满弹出层宽度;浅灰背景跟弹出层白色区分,hover 反相白色 */
|
||||
.im-group-pinned-message__row--list {
|
||||
width: 100%;
|
||||
background-color: var(--el-fill-color-light);
|
||||
box-shadow: none;
|
||||
}
|
||||
.im-group-pinned-message__row--list:hover {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.im-group-pinned-message__icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.im-group-pinned-message__sender {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.im-group-pinned-message__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.im-group-pinned-message__count {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.im-group-pinned-message__chevron {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.im-group-pinned-message__remove {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
.im-group-pinned-message__remove:hover {
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,18 +5,24 @@
|
|||
- count 从 groupRequestStore 派生(全局存);本端处理 / WS 通知到达后 store 自动更新
|
||||
- 点击横幅打开 GroupRequestListDialog(含历史已处理记录),不再就地展开
|
||||
-->
|
||||
<div v-if="canManage && pendingCount > 0" class="im-group-request-pending">
|
||||
<div class="im-group-request-pending__row" @click="handleOpen">
|
||||
<div
|
||||
v-if="canManage && pendingCount > 0"
|
||||
class="flex flex-shrink-0 flex-col items-start px-4 pt-1.5 pb-2 bg-[var(--el-fill-color-light)]"
|
||||
>
|
||||
<div
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[10px] text-13px text-[var(--el-text-color-primary)] bg-[var(--el-bg-color)] cursor-pointer shadow-[0_1px_2px_rgba(0,0,0,0.04)] transition-colors hover:bg-[var(--el-fill-color-lighter)]"
|
||||
@click="handleOpen"
|
||||
>
|
||||
<Icon
|
||||
icon="ant-design:user-add-outlined"
|
||||
:size="14"
|
||||
class="im-group-request-pending__icon"
|
||||
class="im-group-request-pending__icon flex-shrink-0 text-[var(--el-color-success)]"
|
||||
/>
|
||||
<span class="im-group-request-pending__text"> 新进群申请({{ pendingCount }}) </span>
|
||||
<span class="flex-1 min-w-0 truncate"> 新进群申请({{ pendingCount }}) </span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
class="im-group-request-pending__chevron"
|
||||
class="flex-shrink-0 text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -75,53 +81,8 @@ const pendingCount = computed(() => groupRequestStore.getUnhandledCountByGroupId
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 容器:align-items flex-start 让胶囊靠左、不占整行;高度由内容撑开,与置顶消息横幅节奏对齐 */
|
||||
.im-group-request-pending {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 6px 16px 8px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
/* 胶囊本体:内容自适应宽度,padding / 圆角 / 阴影对齐 GroupPinnedMessage 的 __row */
|
||||
.im-group-request-pending__row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.im-group-request-pending__row:hover {
|
||||
background-color: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
/* 绿色「加好友」icon:与置顶消息黄色 pushpin 同节奏,仅换色调;svg 强制 currentColor 应对暗色覆盖 */
|
||||
.im-group-request-pending__icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
/* :deep 穿透 Icon 子组件 DOM;强制 svg 走 currentColor 应对暗色模式 el-icon 全局色覆盖 */
|
||||
.im-group-request-pending__icon :deep(svg) {
|
||||
fill: currentColor !important;
|
||||
}
|
||||
|
||||
.im-group-request-pending__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.im-group-request-pending__chevron {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,30 +2,47 @@
|
|||
<!-- 公众号会话内大卡片:对齐微信公众号单图文卡(封面 9:5 + 下方白底加粗标题条) -->
|
||||
<div
|
||||
v-if="isChannelView"
|
||||
class="material-card channel-card cursor-pointer"
|
||||
class="material-card cursor-pointer w-full overflow-hidden rounded-lg bg-[var(--el-bg-color)] border border-solid border-[var(--el-border-color-lighter)]"
|
||||
@click="onClick"
|
||||
>
|
||||
<img v-if="payload.coverUrl" class="channel-cover" :src="payload.coverUrl" />
|
||||
<div class="channel-title">{{ payload.title || '(无标题)' }}</div>
|
||||
<img v-if="payload.coverUrl" class="block w-full h-[200px] object-cover" :src="payload.coverUrl" />
|
||||
<div class="px-3.5 py-3 text-15px font-600 leading-[1.4] text-[var(--el-text-color-primary)] line-clamp-2">
|
||||
{{ payload.title || '(无标题)' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 私聊 / 群聊里被转发的素材紧凑卡片:标题 + 摘要在左、小封面在右、底部频道头像 + 名称(对齐微信公众号转发卡) -->
|
||||
<div v-else class="material-card forward-card cursor-pointer" @click="onClick">
|
||||
<div class="forward-body">
|
||||
<div class="forward-text">
|
||||
<div class="forward-title">{{ payload.title || '(无标题)' }}</div>
|
||||
<div v-if="payload.summary" class="forward-summary">{{ payload.summary }}</div>
|
||||
<div
|
||||
v-else
|
||||
class="material-card cursor-pointer flex flex-col w-[260px] px-3.5 pt-3 pb-2.5 rounded-lg bg-[var(--el-bg-color)] border border-solid border-[var(--el-border-color-lighter)]"
|
||||
@click="onClick"
|
||||
>
|
||||
<div class="flex gap-2.5 items-start">
|
||||
<div class="flex flex-1 flex-col gap-1.5 min-w-0">
|
||||
<div class="text-15px font-600 leading-[1.4] text-[var(--el-text-color-primary)] line-clamp-2 break-all">
|
||||
{{ payload.title || '(无标题)' }}
|
||||
</div>
|
||||
<div
|
||||
v-if="payload.summary"
|
||||
class="text-12px leading-[1.5] text-[var(--el-text-color-secondary)] line-clamp-2 break-all"
|
||||
>
|
||||
{{ payload.summary }}
|
||||
</div>
|
||||
</div>
|
||||
<img v-if="payload.coverUrl" class="forward-cover" :src="payload.coverUrl" />
|
||||
<img
|
||||
v-if="payload.coverUrl"
|
||||
class="flex-shrink-0 w-[60px] h-[60px] object-cover rounded bg-[var(--el-fill-color-light)]"
|
||||
:src="payload.coverUrl"
|
||||
/>
|
||||
</div>
|
||||
<div class="forward-footer">
|
||||
<div class="flex items-center gap-1.5 mt-2.5 pt-2 border-t border-t-solid border-[var(--el-border-color-lighter)] text-12px text-[var(--el-text-color-secondary)]">
|
||||
<img
|
||||
v-if="sourceChannel?.avatar"
|
||||
class="forward-channel-avatar"
|
||||
class="w-4 h-4 rounded-full object-cover flex-shrink-0"
|
||||
:src="sourceChannel.avatar"
|
||||
/>
|
||||
<Icon v-else icon="ep:promotion" :size="14" />
|
||||
<span class="forward-channel-name">{{ sourceChannel?.name || '频道消息' }}</span>
|
||||
<span class="truncate">{{ sourceChannel?.name || '频道消息' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -36,10 +53,21 @@
|
|||
fullscreen
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-loading="detailLoading" class="material-detail-body">
|
||||
<div class="article-title">{{ payload.title || '' }}</div>
|
||||
<div v-if="detailHtml" class="article-content" v-dompurify-html="detailHtml"></div>
|
||||
<div v-else-if="!detailLoading" class="article-empty">暂无正文</div>
|
||||
<div v-loading="detailLoading" class="material-detail-body max-w-[720px] mx-auto px-5 pt-6 pb-20">
|
||||
<div class="text-[22px] font-600 leading-[1.4] text-[var(--el-text-color-primary)] mb-5">
|
||||
{{ payload.title || '' }}
|
||||
</div>
|
||||
<div
|
||||
v-if="detailHtml"
|
||||
class="article-content text-15px leading-[1.75] text-[var(--el-text-color-primary)]"
|
||||
v-dompurify-html="detailHtml"
|
||||
></div>
|
||||
<div
|
||||
v-else-if="!detailLoading"
|
||||
class="text-center text-[var(--el-text-color-secondary)] mt-20"
|
||||
>
|
||||
暂无正文
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
|
@ -104,7 +132,7 @@ const onClick = async () => {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* hover 阴影 + transition 用 SCSS 写更紧凑;unocss 写成 hover: 一行还行,但 transition 缓动还得带类,反而散 */
|
||||
/* hover 阴影 + transition:合写一处,写法更紧凑 */
|
||||
.material-card {
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
|
|
@ -113,156 +141,22 @@ const onClick = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
/* 公众号大卡片:封面 9:5 + 下方加粗标题条;纯 SCSS 写避免 unocss 偶发 arbitrary value 漏生成 */
|
||||
.channel-card {
|
||||
width: 100%;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.channel-cover {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
/* :deep 穿透 v-dompurify-html 渲染的内嵌 DOM;统一控制富文本里的 img / p / hN 排版 */
|
||||
.article-content {
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.channel-title {
|
||||
padding: 12px 14px;
|
||||
font-size: 15px;
|
||||
:deep(p) {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
:deep(h3) {
|
||||
margin: 20px 0 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-primary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* 私聊 / 群聊转发卡片:标题 + 摘要左、封面右、底部频道头像 + 名称 */
|
||||
.forward-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 260px;
|
||||
padding: 12px 14px 10px;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
|
||||
.forward-body {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
|
||||
.forward-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.forward-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-primary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.forward-summary {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.forward-cover {
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.forward-footer {
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.forward-channel-avatar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.forward-channel-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 富文本详情:article-content 内置 img / p / hN 用 :deep 全局生效;unocss 无法穿透 scoped 边界 */
|
||||
.material-detail-body {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 20px 80px;
|
||||
|
||||
.article-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
:deep(h3) {
|
||||
margin: 20px 0 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.article-empty {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
<!-- 文件:文件名 + 大小靠左、彩色大图标贴右;上传中插一条进度条 -->
|
||||
<div
|
||||
v-else-if="isFile && filePayload"
|
||||
class="relative flex gap-3 items-center min-w-[260px] max-w-[340px] px-3.5 py-3 border rounded transition-colors"
|
||||
class="relative flex gap-3 items-center min-w-[260px] max-w-[340px] px-3.5 py-3 border border-solid rounded transition-colors"
|
||||
:class="[bubbleClass('file'), isUploading ? 'cursor-default' : 'cursor-pointer']"
|
||||
@click="handleFileClick"
|
||||
>
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
<!-- 合并转发气泡:title + 摘要预览 + 底部「聊天记录」标签 -->
|
||||
<div
|
||||
v-else-if="isMerge && mergePayload"
|
||||
class="flex flex-col w-[260px] rounded-md overflow-hidden cursor-pointer bg-[var(--el-bg-color)] border border-[var(--el-border-color)] hover:border-[#409eff]"
|
||||
class="flex flex-col w-[260px] rounded-md overflow-hidden cursor-pointer bg-[var(--el-bg-color)] border border-solid border-[var(--el-border-color)] hover:border-[#409eff]"
|
||||
@click="emit('open-merge', content)"
|
||||
>
|
||||
<div class="px-3 py-2 text-sm font-medium text-[var(--el-text-color-primary)] truncate">
|
||||
|
|
@ -136,7 +136,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="px-3 py-1 text-12px border-t text-[var(--el-text-color-placeholder)] border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-lighter)]"
|
||||
class="px-3 py-1 text-12px border-t border-t-solid text-[var(--el-text-color-placeholder)] border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
聊天记录
|
||||
</div>
|
||||
|
|
@ -350,8 +350,8 @@ onBeforeUnmount(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 气泡尾巴:小三角伪元素,指向对应头像(对方在左、自己在右)
|
||||
border 4 边色画三角:透明 3 边 + 实色 1 边,省一张图片;颜色与气泡背景对应,留 1px 视觉吃进去 */
|
||||
/* 气泡尾巴小三角指向对应头像(对方在左、自己在右);走 ::before + 4 边 border 配色画:透明 3 边 + 实色 1 边,
|
||||
颜色与气泡背景对应,留 1px 视觉吃进去,省一张图片 */
|
||||
.message-bubble--other::before,
|
||||
.message-bubble--self::before {
|
||||
content: '';
|
||||
|
|
@ -372,7 +372,7 @@ onBeforeUnmount(() => {
|
|||
border-color: transparent transparent transparent #95ec69;
|
||||
}
|
||||
|
||||
/* el-icon 在暗色模式下全局 color 被 .el-icon{color:var(--color)} 干扰,把 voice 图标 fill 锁死 */
|
||||
/* :deep 穿透 scoped 子组件 DOM;el-icon 在暗色模式下全局 color 被 .el-icon{color:var(--color)} 干扰,把 voice 图标 fill 锁死 */
|
||||
.message-bubble__voice-icon :deep(svg) {
|
||||
fill: #606266 !important;
|
||||
}
|
||||
|
|
@ -380,7 +380,7 @@ onBeforeUnmount(() => {
|
|||
fill: #409eff !important;
|
||||
}
|
||||
|
||||
/* 播放中的脉冲动画 */
|
||||
/* @keyframes 需要 SCSS 声明;播放中的脉冲动画 */
|
||||
.im-voice-playing {
|
||||
animation: im-voice-icon-pulse 0.8s infinite;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,10 +42,9 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Tab 行:文件 / 图片 / 语音 / 日期(popover) / 群成员(popover, 仅群聊)
|
||||
底部一条分割线把 tab 区跟消息列表分开,对齐微信观感(border 走 scoped CSS 走主题变量) -->
|
||||
<!-- Tab 行:文件 / 图片 / 语音 / 日期(popover) / 群成员(popover, 仅群聊);底部一条分割线把 tab 区跟消息列表分开,对齐微信观感 -->
|
||||
<div
|
||||
class="im-message-history__tabs flex gap-5 px-2 pb-2 text-sm flex-shrink-0 text-[#1989fa]"
|
||||
class="flex gap-5 px-2 pb-2 text-sm flex-shrink-0 text-[#1989fa] border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<span
|
||||
class="im-message-history__tab cursor-pointer"
|
||||
|
|
@ -144,7 +143,7 @@
|
|||
跟主聊天面板里 MessageItem 的渲染语义对齐 -->
|
||||
<div
|
||||
v-if="isFriendChatTip(message.type)"
|
||||
class="px-4 py-3 text-12px text-center italic text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||
class="px-4 py-3 text-12px text-center italic text-[var(--el-text-color-secondary)] border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<TipSegments :segments="resolveFriendNotificationSegments(message)" />
|
||||
</div>
|
||||
|
|
@ -152,7 +151,7 @@
|
|||
<!-- 群广播事件:跟好友事件同灰色样式,mention 段挂点击弹 UserInfoCard -->
|
||||
<div
|
||||
v-else-if="isGroupNotification(message.type)"
|
||||
class="px-4 py-3 text-12px text-center italic text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||
class="px-4 py-3 text-12px text-center italic text-[var(--el-text-color-secondary)] border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<TipSegments :segments="resolveGroupNotificationSegments(message, resolveGroupMemberName(message))" />
|
||||
</div>
|
||||
|
|
@ -160,7 +159,7 @@
|
|||
<!-- 普通消息行 -->
|
||||
<div
|
||||
v-else
|
||||
class="im-message-history__row flex gap-3 items-start px-1 py-3 border-b border-[var(--el-border-color-lighter)]"
|
||||
class="im-message-history__row flex gap-3 items-start px-1 py-3 border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<UserAvatar
|
||||
:url="getAvatar(message)"
|
||||
|
|
@ -210,7 +209,10 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="currentList.length === 0" class="im-message-history__empty">
|
||||
<div
|
||||
v-if="currentList.length === 0"
|
||||
class="py-10 text-center text-13px text-[var(--el-text-color-disabled)]"
|
||||
>
|
||||
{{ keyword || activeFilter ? '没有匹配的消息' : '暂无消息' }}
|
||||
</div>
|
||||
|
||||
|
|
@ -218,7 +220,7 @@
|
|||
filter 命中 0 条时仍保留 —— 加载更早可能带回匹配内容 -->
|
||||
<div
|
||||
v-if="hasMore && allMessages.length > 0"
|
||||
class="py-3 text-center border-t border-[var(--el-border-color-lighter)]"
|
||||
class="py-3 text-center border-t border-t-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<el-button :loading="loadingMore" link type="primary" @click="loadEarlier">
|
||||
加载更早消息
|
||||
|
|
@ -669,24 +671,14 @@ function locateMessage(messageId: number) {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 空态文案 */
|
||||
.im-message-history__empty {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-disabled);
|
||||
}
|
||||
|
||||
/* 搜索区 chip:禁掉 el-tag 默认的 hover 颜色过渡 / × 图标动效,避免在搜索区里有抖动感 */
|
||||
/* :deep 穿透 el-tag 子组件 DOM;搜索区 chip 禁掉 hover 颜色过渡 / × 图标动效,避免在搜索区里有抖动感 */
|
||||
.im-message-history__chip,
|
||||
.im-message-history__chip :deep(.el-tag__close) {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
/* "定位到聊天位置" 链接:
|
||||
- absolute 定位浮在时间下方,不参与右侧栏 flex 排版(隐藏时不占位 → "我"和内容之间不留空隙)
|
||||
- 默认 display:none,行 hover 才 block;颜色对齐 tab 同款蓝,按钮自身 hover 再深一档 */
|
||||
/* 「定位到聊天位置」按钮:父行 hover 才显示,走 .parent:hover .child 跨元素状态联动 */
|
||||
.im-message-history__locate {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
|
|
@ -705,13 +697,7 @@ function locateMessage(messageId: number) {
|
|||
color: #146fc7;
|
||||
}
|
||||
|
||||
/* tab 行:底部一条灰线把 tab 区跟消息列表分开(UnoCSS 的 border-[var(--*)] 在这里偶发不生效,
|
||||
直接用 scoped CSS 引主题变量更稳) */
|
||||
.im-message-history__tabs {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
/* tab 默认 -> 蓝色行内文字;激活态加下划线 */
|
||||
/* ::after 伪元素画激活下划线;tab 默认蓝色行内文字,激活态加下划线 */
|
||||
.im-message-history__tab {
|
||||
position: relative;
|
||||
padding: 4px 2px;
|
||||
|
|
@ -732,7 +718,7 @@ function locateMessage(messageId: number) {
|
|||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* el-calendar 默认偏大,压一压让它能塞进 320 popover */
|
||||
/* :deep 穿透 el-calendar 子组件 DOM;默认偏大压一压让它能塞进 320 popover */
|
||||
.im-message-history__calendar :deep(.el-calendar) {
|
||||
--el-calendar-cell-width: 36px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
:class="
|
||||
isMessageChecked
|
||||
? 'bg-[#07c160]'
|
||||
: 'border border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
: 'border border-solid border-[var(--el-border-color)] bg-[var(--el-bg-color)]'
|
||||
"
|
||||
>
|
||||
<Icon v-if="isMessageChecked" icon="ant-design:check-outlined" :size="12" color="#fff" />
|
||||
|
|
@ -1049,7 +1049,7 @@ function handleDelete() {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* SENDING 状态的转圈动画 */
|
||||
/* @keyframes 需要 SCSS 声明;SENDING 状态的转圈动画 */
|
||||
.im-loading-spin {
|
||||
animation: im-loading-spin 1s linear infinite;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="flex flex-1 flex-col min-w-0 bg-[var(--el-fill-color-light)]">
|
||||
<template v-if="conversationStore.activeConversation">
|
||||
<!-- 顶部 header:第一行群名 + 右侧图标,第二行嵌入置顶气泡(仅群聊 + 有置顶);border 走 scoped CSS -->
|
||||
<div class="message-panel__header flex flex-col bg-[var(--el-fill-color-light)]">
|
||||
<!-- 顶部 header:第一行群名 + 右侧图标,第二行嵌入置顶气泡(仅群聊 + 有置顶) -->
|
||||
<div class="flex flex-shrink-0 flex-col bg-[var(--el-fill-color-light)] border-b border-b-solid border-[var(--el-border-color-light)]">
|
||||
<div class="flex items-center justify-between h-14 px-5">
|
||||
<span class="flex flex-col min-w-0">
|
||||
<span class="flex items-baseline gap-1.5 min-w-0">
|
||||
|
|
@ -52,16 +52,16 @@
|
|||
class="message-panel__header-icon cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
<div class="message-panel__call-menu">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div
|
||||
class="message-panel__call-menu-item"
|
||||
class="flex items-center gap-2.5 px-3 py-2 rounded cursor-pointer text-sm text-[var(--el-text-color-primary)] hover:bg-[var(--el-fill-color-light)]"
|
||||
@click="startPrivateCall(ImRtcCallMediaType.VOICE)"
|
||||
>
|
||||
<Icon icon="ant-design:phone-outlined" :size="16" />
|
||||
<span>语音通话</span>
|
||||
</div>
|
||||
<div
|
||||
class="message-panel__call-menu-item"
|
||||
class="flex items-center gap-2.5 px-3 py-2 rounded cursor-pointer text-sm text-[var(--el-text-color-primary)] hover:bg-[var(--el-fill-color-light)]"
|
||||
@click="startPrivateCall(ImRtcCallMediaType.VIDEO)"
|
||||
>
|
||||
<Icon icon="ant-design:video-camera-outlined" :size="16" />
|
||||
|
|
@ -107,9 +107,15 @@
|
|||
:group-id="conversationStore.activeConversation.targetId"
|
||||
/>
|
||||
<!-- 私聊:对方不再是有效好友(我删了对方 / 从未加过;单边设计下「被对方删除」本端 friendStore 不更新故不会触发);胶囊嵌在 header 内(跟群置顶同级),点击弹 UserInfoCard -->
|
||||
<div v-if="showNotFriendBanner" class="message-panel__not-friend-container">
|
||||
<div class="message-panel__not-friend" @click="handleNotFriendClick">
|
||||
<span class="message-panel__not-friend-icon">
|
||||
<div
|
||||
v-if="showNotFriendBanner"
|
||||
class="flex flex-shrink-0 items-start px-4 pb-2 bg-[var(--el-fill-color-light)]"
|
||||
>
|
||||
<div
|
||||
class="inline-flex items-center gap-2 px-2.5 py-1 rounded-full text-13px cursor-pointer text-[var(--el-text-color-primary)] bg-[var(--el-color-warning-light-9)] transition-colors hover:bg-[var(--el-color-warning-light-8)]"
|
||||
@click="handleNotFriendClick"
|
||||
>
|
||||
<span class="inline-flex items-center justify-center w-4 h-4 rounded-full text-white bg-[var(--el-color-warning)] flex-shrink-0">
|
||||
<Icon icon="ant-design:user-outlined" :size="11" />
|
||||
</span>
|
||||
<span>对方还不是你的朋友</span>
|
||||
|
|
@ -700,14 +706,8 @@ watch(
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 顶部分隔线:UnoCSS 不带 border-style preflight,class 写法只设色 / 宽不出线,走 scoped 显式 shorthand */
|
||||
.message-panel__header {
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
/* el-icon 全局规则 .el-icon{color:var(--color,inherit)} 优先级胜过 UnoCSS,这里用 :deep + !important 兜底;
|
||||
颜色直接引用 Element Plus 主题变量,暗色模式自动切到更亮的灰 */
|
||||
/* :deep 穿透 el-icon 子组件 svg;el-icon 全局规则 .el-icon{color:var(--color,inherit)} 优先级更高,
|
||||
用 :deep + !important 锁色;颜色取 Element Plus 主题变量,暗色模式自动切到更亮的灰 */
|
||||
.message-panel__header-icon,
|
||||
.message-panel__header-icon :deep(svg) {
|
||||
color: var(--el-text-color-regular) !important;
|
||||
|
|
@ -719,44 +719,7 @@ watch(
|
|||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
/* 「对方还不是你的朋友」胶囊:嵌在 header 内(跟群置顶同级),不占整行;padding 跟群置顶 padding 对齐 */
|
||||
.message-panel__not-friend-container {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 0 16px 8px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
.message-panel__not-friend {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-color-warning-light-9);
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.message-panel__not-friend:hover {
|
||||
background-color: var(--el-color-warning-light-8);
|
||||
}
|
||||
/* 圆形小图标,深黄底色配白色 icon —— 跟微信一致的视觉锤 */
|
||||
.message-panel__not-friend-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background-color: var(--el-color-warning);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* sticky + translate 居中:fit-content 宽度不会撑满,transform 做水平 -50% 偏移;
|
||||
UnoCSS 表达 transform+transition 多 value 不太方便,这里用最小的 scoped CSS 承接 */
|
||||
/* sticky + translate 居中:fit-content 宽度不会撑满,transform 水平 -50% 偏移;同时 transition opacity 和 transform 两个属性 */
|
||||
.message-panel__jump-bottom {
|
||||
transform: translateX(-50%);
|
||||
transition:
|
||||
|
|
@ -764,7 +727,7 @@ watch(
|
|||
transform 0.2s;
|
||||
}
|
||||
|
||||
/* MessageHistory "定位" 跳过来时短暂高亮:1.6s 后由 JS 移除 class,配合 transition 缓出黄底 */
|
||||
/* JS 切换 highlight class + background-color transition 联动;MessageHistory「定位」跳过来时短暂高亮 1.6s */
|
||||
.message-panel__message-anchor {
|
||||
transition: background-color 0.6s ease;
|
||||
}
|
||||
|
|
@ -772,7 +735,7 @@ watch(
|
|||
background-color: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
/* 回到底部按钮的 Vue transition 钩子类名 */
|
||||
/* Vue <transition> 钩子类名固定,必须 SCSS 声明;回到底部按钮淡入淡出 */
|
||||
.message-panel__jump-fade-enter-active,
|
||||
.message-panel__jump-fade-leave-active {
|
||||
transition:
|
||||
|
|
@ -785,27 +748,6 @@ watch(
|
|||
opacity: 0;
|
||||
transform: translate(-50%, 20px);
|
||||
}
|
||||
|
||||
.message-panel__call-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.message-panel__call-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.message-panel__call-menu-item:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@
|
|||
- 撤回(命中本地缓存且 type === RECALL):「原消息已撤回」斜体灰字
|
||||
-->
|
||||
<div
|
||||
class="im-reply-preview flex w-fit gap-1.5 items-center min-w-0 py-0.5 text-12px text-[var(--el-text-color-secondary)] rounded transition-colors"
|
||||
class="flex w-fit gap-1.5 items-center min-w-0 py-0.5 text-12px text-[var(--el-text-color-secondary)] rounded transition-colors"
|
||||
:class="[
|
||||
mirrored ? 'im-reply-preview--end pl-1 pr-2' : 'pl-2 pr-1',
|
||||
mirrored ? 'pl-1 pr-2 border-r-2 border-r-solid border-r-[var(--el-border-color)]' : 'pl-2 pr-1 border-l-2 border-l-solid border-l-[var(--el-border-color)]',
|
||||
{
|
||||
'cursor-pointer hover:text-[var(--el-text-color-primary)]': clickable && !isRecalled,
|
||||
'hover:bg-[var(--el-fill-color-light)]': (clickable && !isRecalled) || closable
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<span v-if="isRecalled" class="italic">原消息已撤回</span>
|
||||
|
||||
<!-- 文本 -->
|
||||
<span v-else-if="isText" class="im-reply-preview__text min-w-0">{{ textPreview }}</span>
|
||||
<span v-else-if="isText" class="min-w-0 line-clamp-2 break-words">{{ textPreview }}</span>
|
||||
|
||||
<!-- 文件:icon + 文件名 + 大小 -->
|
||||
<template v-else-if="isFile">
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
:size="14"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span v-if="parsedPayload?.name" class="im-reply-preview__text min-w-0">
|
||||
<span v-if="parsedPayload?.name" class="min-w-0 line-clamp-2 break-words">
|
||||
{{ parsedPayload.name }}
|
||||
</span>
|
||||
<span
|
||||
|
|
@ -58,12 +58,12 @@
|
|||
</template>
|
||||
|
||||
<!-- 名片 -->
|
||||
<CardLineLabel v-else-if="isCard" :card="parsedPayload" class="im-reply-preview__text min-w-0" />
|
||||
<CardLineLabel v-else-if="isCard" :card="parsedPayload" class="min-w-0 line-clamp-2 break-words" />
|
||||
|
||||
<!-- 表情贴图:缩略图 + name(无 name 仅显示 [表情]) -->
|
||||
<template v-else-if="isFace">
|
||||
<span class="flex-shrink-0">[表情]</span>
|
||||
<span v-if="parsedPayload?.name" class="im-reply-preview__text min-w-0">
|
||||
<span v-if="parsedPayload?.name" class="min-w-0 line-clamp-2 break-words">
|
||||
{{ parsedPayload.name }}
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<!-- 频道素材:[频道] + 标题 + 封面缩略图 -->
|
||||
<template v-else-if="isMaterial">
|
||||
<span class="flex-shrink-0">[频道]</span>
|
||||
<span v-if="parsedPayload?.title" class="im-reply-preview__text min-w-0">
|
||||
<span v-if="parsedPayload?.title" class="min-w-0 line-clamp-2 break-words">
|
||||
{{ parsedPayload.title }}
|
||||
</span>
|
||||
</template>
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
<button
|
||||
v-if="closable"
|
||||
type="button"
|
||||
class="im-reply-preview__close flex-shrink-0 inline-flex items-center justify-center w-4 h-4 cursor-pointer rounded-full bg-transparent border-none text-[var(--el-text-color-secondary)] hover:bg-[var(--el-fill-color)] hover:text-[var(--el-text-color-primary)]"
|
||||
class="flex-shrink-0 inline-flex items-center justify-center w-4 h-4 cursor-pointer rounded-full bg-transparent border-none text-[var(--el-text-color-secondary)] hover:bg-[var(--el-fill-color)] hover:text-[var(--el-text-color-primary)]"
|
||||
@click.stop="emit('close')"
|
||||
>
|
||||
<Icon icon="ant-design:close-outlined" :size="10" />
|
||||
|
|
@ -228,22 +228,3 @@ function onClick() {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 默认左侧 2px 竖线作为「引用」视觉标识;mirrored 时镜像到右侧 */
|
||||
.im-reply-preview {
|
||||
border-left: 2px solid var(--el-border-color);
|
||||
}
|
||||
.im-reply-preview--end {
|
||||
border-left: 0;
|
||||
border-right: 2px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
/* 文字超过 2 行截断,避免长引用把输入条 / 气泡撑高;UnoCSS 的 line-clamp 工具类在本项目未启用,走 scoped CSS */
|
||||
.im-reply-preview__text {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
<!-- 合并模式预览:「[聊天记录] 标题 + 摘要列表」预览卡 -->
|
||||
<div
|
||||
v-if="state.mode === ImForwardMode.MERGE && mergePreview"
|
||||
class="flex flex-col w-full overflow-hidden rounded-md bg-[var(--el-bg-color)] border border-[var(--el-border-color-lighter)]"
|
||||
class="flex flex-col w-full overflow-hidden rounded-md bg-[var(--el-bg-color)] border border-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<div
|
||||
class="px-3 py-2 text-sm font-medium truncate text-[var(--el-text-color-primary)]"
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="px-3 py-1 text-12px border-t text-[var(--el-text-color-placeholder)] border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-lighter)]"
|
||||
class="px-3 py-1 text-12px border-t border-t-solid text-[var(--el-text-color-placeholder)] border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
聊天记录
|
||||
</div>
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
<!-- 逐条模式预览:消息数 + 首条摘要 -->
|
||||
<div
|
||||
v-else-if="state.mode === ImForwardMode.SINGLE && singlePreviewLines.length > 0"
|
||||
class="flex flex-col w-full overflow-hidden rounded-md bg-[var(--el-bg-color)] border border-[var(--el-border-color-lighter)]"
|
||||
class="flex flex-col w-full overflow-hidden rounded-md bg-[var(--el-bg-color)] border border-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<div class="flex flex-col px-3 py-2 gap-0.5">
|
||||
<div
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="px-3 py-1 text-12px border-t text-[var(--el-text-color-placeholder)] border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-lighter)]"
|
||||
class="px-3 py-1 text-12px border-t border-t-solid text-[var(--el-text-color-placeholder)] border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-lighter)]"
|
||||
>
|
||||
共 {{ state.messages.length }} 条消息
|
||||
</div>
|
||||
|
|
@ -429,6 +429,7 @@ async function handleCreateGroupAndSend() {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 复用选择类弹窗的公共 mixin; 内部是 :deep 穿透 el-dialog 内部 header / body 的样式,无法替换为工具类 */
|
||||
@use '@/views/im/home/components/picker/picker-dialog' as picker;
|
||||
|
||||
.im-picker-dialog {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<div v-if="currentPayload" class="flex flex-col h-[480px]">
|
||||
<div
|
||||
class="px-4 py-2 text-12px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||
class="px-4 py-2 text-12px text-[var(--el-text-color-secondary)] border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
以下是 {{ currentPayload.messages.length }} 条消息
|
||||
</div>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<div
|
||||
v-for="(item, idx) in currentPayload.messages"
|
||||
:key="idx"
|
||||
class="flex gap-3 items-start px-4 py-3 border-b border-[var(--el-border-color-lighter)]"
|
||||
class="flex gap-3 items-start px-4 py-3 border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<UserAvatar
|
||||
:url="item.senderAvatar"
|
||||
|
|
@ -117,6 +117,7 @@ function handleClose() {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* :deep 穿透 el-dialog 内部 body,去掉默认内边距,让滚动区贴边 */
|
||||
.im-merge-detail-dialog :deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<ResizableAside :default-width="260" :storage-key="StorageKeys.asideWidth">
|
||||
<!-- 顶部:搜索框 + "+" 号下拉(对齐微信 PC:发起群聊 / 添加朋友);h-14 与右侧 MessagePanel 头部对齐 -->
|
||||
<div
|
||||
class="flex flex-shrink-0 gap-2 items-center h-14 px-4 border-b border-[var(--el-border-color-lighter)]"
|
||||
class="flex flex-shrink-0 gap-2 items-center h-14 px-4 border-b border-b-solid border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<el-input v-model="keyword" placeholder="搜索" clearable class="flex-1">
|
||||
<template #prefix>
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
<!-- 折叠头:放在置顶区底部对齐 WeChat mac;展开 / 折叠态共用,仅在还有"可折叠"内容、或当前已展开时出现 -->
|
||||
<div
|
||||
v-if="pinnedGroups.foldable.length > 0 || pinnedExpanded"
|
||||
class="flex items-center justify-between px-4 py-2.5 cursor-pointer transition-colors text-13px text-[var(--el-text-color-regular)] border-y border-[var(--el-border-color-lighter)] hover:bg-[var(--el-fill-color)]"
|
||||
class="flex items-center justify-between px-4 py-2.5 cursor-pointer transition-colors text-13px text-[var(--el-text-color-regular)] border-y border-y-solid border-[var(--el-border-color-lighter)] hover:bg-[var(--el-fill-color)]"
|
||||
@click="togglePinnedExpanded"
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ const handleSelected = (rows: ManagerGroupApi.ImManagerGroupVO[]) => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* :deep 用于穿透 el-input 内部元素的 cursor 样式,UnoCSS 无法直接处理组件内部 DOM */
|
||||
/* :deep 穿透 el-input 内部 wrapper / inner 元素改 cursor */
|
||||
.is-select-clickable {
|
||||
:deep(.el-input__wrapper),
|
||||
:deep(.el-input__inner) {
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ defineExpose({ open })
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 隐藏 radio 的 label 文字,只保留圆圈 */
|
||||
/* :deep 穿透 el-radio 内部 label 元素,让单选只渲染圆圈不渲染文字 */
|
||||
.radio-no-label {
|
||||
:deep(.el-radio__label) {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原始 JSON" :span="2">
|
||||
<pre class="content-pre">{{ formatJson(detail.content) }}</pre>
|
||||
<pre class="m-0 whitespace-pre-wrap break-all font-mono text-12px bg-[#f5f5f5] p-8px rounded-4px">{{ formatJson(detail.content) }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
|
@ -68,16 +68,3 @@ const open = (row: ManagerGroupMessageApi.ImManagerGroupMessageVO) => {
|
|||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原始 JSON" :span="2">
|
||||
<pre class="content-pre">{{ formatJson(detail.content) }}</pre>
|
||||
<pre class="m-0 whitespace-pre-wrap break-all font-mono text-12px bg-[#f5f5f5] p-8px rounded-4px">{{ formatJson(detail.content) }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
|
@ -56,16 +56,3 @@ const open = (row: ManagerPrivateMessageApi.ImManagerPrivateMessageVO) => {
|
|||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<el-card shadow="never" class="!rounded-8px mb-16px">
|
||||
<template #header>群规模分布</template>
|
||||
<div ref="chartRef" v-loading="loading" style="width: 100%; height: 320px"></div>
|
||||
</el-card>
|
||||
|
|
@ -53,10 +53,3 @@ onMounted(async () => {
|
|||
})
|
||||
onUnmounted(() => chart?.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<el-card shadow="never" class="!rounded-8px mb-16px">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>消息趋势(私聊 + 群聊)</span>
|
||||
<el-select v-model="days" @change="loadData" style="width: 100px" size="small">
|
||||
<el-option label="近 7 天" :value="7" />
|
||||
|
|
@ -84,8 +84,3 @@ onMounted(async () => {
|
|||
})
|
||||
onUnmounted(() => chart?.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-card { border-radius: 8px; margin-bottom: 16px; }
|
||||
.chart-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<el-card shadow="never" class="!rounded-8px mb-16px">
|
||||
<template #header>消息类型分布</template>
|
||||
<div ref="chartRef" v-loading="loading" style="width: 100%; height: 320px"></div>
|
||||
</el-card>
|
||||
|
|
@ -58,10 +58,3 @@ onMounted(async () => {
|
|||
})
|
||||
onUnmounted(() => chart?.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
<template>
|
||||
<el-row :gutter="16">
|
||||
<el-col v-for="card in cards" :key="card.title" :xl="6" :lg="6" :md="12" :sm="24" :xs="24">
|
||||
<el-card shadow="never" class="kpi-card">
|
||||
<div class="kpi-row">
|
||||
<div class="kpi-icon" :style="{ backgroundColor: card.color }">
|
||||
<el-card shadow="never" class="!rounded-8px mb-16px">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-48px h-48px rounded-8px flex items-center justify-center mr-12px flex-shrink-0"
|
||||
:style="{ backgroundColor: card.color }"
|
||||
>
|
||||
<Icon :icon="card.icon" :size="24" color="#fff" />
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-title">{{ card.title }}</div>
|
||||
<div class="kpi-value">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-13px text-[var(--el-text-color-secondary)] mb-4px">{{ card.title }}</div>
|
||||
<div class="text-22px font-600 text-[var(--el-text-color-primary)] leading-none">
|
||||
<CountTo :start-val="0" :end-val="card.value" :duration="1500" />
|
||||
<span v-if="card.suffix" class="kpi-suffix">{{ card.suffix }}</span>
|
||||
<span v-if="card.suffix" class="text-12px text-[var(--el-text-color-placeholder)] ml-6px font-normal">{{ card.suffix }}</span>
|
||||
</div>
|
||||
<div class="kpi-meta">{{ card.metaLabel }}:
|
||||
<div class="text-12px text-[var(--el-text-color-placeholder)] mt-6px">{{ card.metaLabel }}:
|
||||
<span :class="card.metaClass">{{ card.metaValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -85,50 +88,3 @@ const cards = computed(() => {
|
|||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kpi-card {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.kpi-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.kpi-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.kpi-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.kpi-title {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.kpi-value {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
line-height: 1;
|
||||
}
|
||||
.kpi-suffix {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.kpi-meta {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<el-card shadow="never" class="!rounded-8px mb-16px">
|
||||
<template #header>消息发送 TOP 10</template>
|
||||
<div ref="chartRef" v-loading="loading" style="width: 100%; height: 320px"></div>
|
||||
</el-card>
|
||||
|
|
@ -58,10 +58,3 @@ onMounted(async () => {
|
|||
})
|
||||
onUnmounted(() => chart?.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<el-card shadow="never" class="!rounded-8px mb-16px">
|
||||
<template #header>
|
||||
<div class="chart-header">
|
||||
<div class="flex justify-between items-center">
|
||||
<span>用户趋势(新增注册 + 日活)</span>
|
||||
<el-select v-model="days" @change="loadData" style="width: 100px" size="small">
|
||||
<el-option label="近 7 天" :value="7" />
|
||||
|
|
@ -63,8 +63,3 @@ onMounted(async () => {
|
|||
})
|
||||
onUnmounted(() => chart?.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-card { border-radius: 8px; margin-bottom: 16px; }
|
||||
.chart-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="dashboard">
|
||||
<div class="p-16px">
|
||||
<!-- 概览卡片 -->
|
||||
<OverviewCards v-if="overview" :overview="overview" />
|
||||
|
||||
|
|
@ -46,9 +46,3 @@ onMounted(async () => {
|
|||
overview.value = await StatisticsApi.getStatisticsOverview()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue