✨ feat(im): 优化群聊的功能界面
parent
368b385267
commit
4b4c4fab11
|
|
@ -11,6 +11,9 @@
|
|||
@click="$emit('click', friend)"
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
>
|
||||
<!-- prefix slot:放在头像前,给选择类弹窗的 checkbox / 圆点用,不传则不渲染 -->
|
||||
<slot name="prefix"></slot>
|
||||
<!-- 头像 -->
|
||||
<UserAvatar
|
||||
:id="friend.id"
|
||||
:url="friend.avatar"
|
||||
|
|
@ -18,8 +21,8 @@
|
|||
:size="42"
|
||||
:clickable="false"
|
||||
/>
|
||||
<!-- 单行展示 displayName 优先;昵称仅在好友详情面板展示,列表里不重复 -->
|
||||
<div class="flex flex-1 min-w-0">
|
||||
<!-- 单行展示 displayName 优先;昵称仅在好友详情面板展示,列表里不重复 -->
|
||||
<div class="overflow-hidden text-sm truncate text-[var(--el-text-color-primary)]">
|
||||
{{ friend.displayName || friend.nickname }}
|
||||
</div>
|
||||
|
|
@ -55,12 +58,13 @@ const emit = defineEmits<{
|
|||
|
||||
const uiStore = useImUiStore()
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
/** 右键菜单:发送消息 / 删除好友 */
|
||||
function handleContextMenu(event: MouseEvent) {
|
||||
if (!props.menu) {
|
||||
return
|
||||
}
|
||||
uiStore.openContextMenu(
|
||||
{ x: e.clientX, y: e.clientY },
|
||||
{ x: event.clientX, y: event.clientY },
|
||||
[
|
||||
{ key: 'chat', name: '发送消息' },
|
||||
{ key: 'delete', name: '删除好友' }
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
<!--
|
||||
新建群聊对话框
|
||||
- 顶部:群名称输入
|
||||
- 左:好友列表(带 checkbox)
|
||||
- 右:已勾选预览
|
||||
- 左:好友列表(checkbox 前置)
|
||||
- 右:已勾选预览(每行可 x 移除,locked 不渲染 x)
|
||||
- 提交:先 createGroup 再 inviteGroupMember,最后让父页 reload
|
||||
- lockedIds:锁定不可取消的好友 id;私聊侧 "+创建群" 入口用来锁定对方
|
||||
-->
|
||||
<el-dialog v-model="visible" title="新建群聊" width="620px" :close-on-click-modal="false">
|
||||
<div class="flex flex-col gap-3">
|
||||
|
|
@ -14,25 +15,31 @@
|
|||
<div
|
||||
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
|
||||
>
|
||||
<el-input v-model="searchText" placeholder="搜索好友" size="small" clearable>
|
||||
<template #suffix>
|
||||
<Icon icon="ant-design:search-outlined" />
|
||||
<el-input v-model="searchText" placeholder="搜索好友" clearable>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
icon="ant-design:search-outlined"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-scrollbar class="h-[400px]">
|
||||
<FriendItem
|
||||
v-for="f in shownFriends"
|
||||
:key="f.id"
|
||||
:friend="f"
|
||||
v-for="friend in shownFriends"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:menu="false"
|
||||
:active="false"
|
||||
@click="handleToggleCheck(f)"
|
||||
@click="handleToggleCheck(friend)"
|
||||
>
|
||||
<el-checkbox
|
||||
:model-value="f.isCheck"
|
||||
@click.stop
|
||||
@change="(v) => (f.isCheck = !!v)"
|
||||
/>
|
||||
<template #prefix>
|
||||
<el-checkbox
|
||||
:model-value="friend.checked"
|
||||
:disabled="friend.disabled"
|
||||
@click.stop
|
||||
@change="(value) => handleCheckChange(friend, !!value)"
|
||||
/>
|
||||
</template>
|
||||
</FriendItem>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
|
@ -44,27 +51,38 @@
|
|||
<div
|
||||
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
|
||||
>
|
||||
<!-- 标题高度对齐左侧 el-input default(32px),保证两侧第一项起点在同一水平 -->
|
||||
<div
|
||||
class="h-10 pl-2.5 leading-10 text-13px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||
class="h-8 pl-2.5 leading-8 text-13px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
已勾选 {{ checkedFriends.length }} 位好友
|
||||
</div>
|
||||
<el-scrollbar class="h-[400px]">
|
||||
<FriendItem
|
||||
v-for="f in checkedFriends"
|
||||
:key="f.id"
|
||||
:friend="f"
|
||||
v-for="friend in checkedFriends"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:menu="false"
|
||||
:active="false"
|
||||
/>
|
||||
>
|
||||
<!-- locked 的好友不渲染 x,避免误以为可移除 -->
|
||||
<Icon
|
||||
v-if="!friend.disabled"
|
||||
icon="ant-design:close-outlined"
|
||||
class="im-group-create-dialog__remove"
|
||||
@click.stop="handleUncheck(friend)"
|
||||
/>
|
||||
</FriendItem>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleOk">创 建</el-button>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleOk">
|
||||
完成
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
|
@ -83,16 +101,19 @@ import type { FriendLite } from '../../types'
|
|||
defineOptions({ name: 'ImGroupCreateDialog' })
|
||||
|
||||
interface FriendCheckable extends FriendLite {
|
||||
isCheck?: boolean
|
||||
checked?: boolean
|
||||
disabled?: boolean // locked 的好友:勾选态由 lockedIds 强制为 true,UI 上 checkbox / x 都不响应
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
friends?: FriendLite[] // 全量好友(由调用方从 friendStore 传入)
|
||||
lockedIds?: number[] // 锁定的好友 id:自动勾选 + 不可取消(私聊侧 "+创建群" 用来锁定对方)
|
||||
}>(),
|
||||
{
|
||||
friends: () => []
|
||||
friends: () => [],
|
||||
lockedIds: () => []
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -113,47 +134,73 @@ const visible = computed({
|
|||
const groupName = ref('')
|
||||
const searchText = ref('')
|
||||
const submitting = ref(false)
|
||||
// TODO @AI:checked 改成这个变量;
|
||||
const workingFriends = ref<FriendCheckable[]>([]) // 工作副本(带 isCheck 标记),与 prop 隔离
|
||||
const workingFriends = ref<FriendCheckable[]>([]) // 工作副本(带 checked / disabled 标记),与 prop 隔离
|
||||
|
||||
watch(
|
||||
visible,
|
||||
(v) => {
|
||||
if (!v) {
|
||||
(open) => {
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
groupName.value = ''
|
||||
searchText.value = ''
|
||||
workingFriends.value = props.friends
|
||||
.filter((f) => !f.deleted)
|
||||
.map((f) => ({ ...f, isCheck: false }))
|
||||
.filter((friend) => !friend.deleted)
|
||||
.map((friend) => {
|
||||
const locked = props.lockedIds.some((id) => id === friend.id)
|
||||
return { ...friend, checked: locked, disabled: locked }
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/** 左侧展示的好友:按搜索关键字过滤 workingFriends */
|
||||
const shownFriends = computed(() =>
|
||||
workingFriends.value.filter((f) => f.nickname.includes(searchText.value))
|
||||
workingFriends.value.filter((friend) => friend.nickname.includes(searchText.value))
|
||||
)
|
||||
|
||||
/** 已勾选的好友:右侧预览 + 提交时取 memberUserIds */
|
||||
const checkedFriends = computed(() => workingFriends.value.filter((f) => f.isCheck))
|
||||
const checkedFriends = computed(() => workingFriends.value.filter((friend) => friend.checked))
|
||||
|
||||
/** 行点击:切换勾选态,让点击整行与点 checkbox 等价 */
|
||||
function handleToggleCheck(f: FriendCheckable) {
|
||||
f.isCheck = !f.isCheck
|
||||
/**
|
||||
* 完成按钮可点:群名非空 + 至少有 1 个非 locked 勾选
|
||||
*
|
||||
* locked 是入口侧自动选的(如私聊对方),不算"用户主动选择"——否则用户什么都没勾就能建 2 人群,体验上等于私聊
|
||||
*/
|
||||
const canSubmit = computed(() => {
|
||||
if (!groupName.value.trim()) {
|
||||
return false
|
||||
}
|
||||
return checkedFriends.value.some((friend) => !friend.disabled)
|
||||
})
|
||||
|
||||
/** 行点击:切换勾选态,locked 的不响应 */
|
||||
function handleToggleCheck(friend: FriendCheckable) {
|
||||
if (friend.disabled) {
|
||||
return
|
||||
}
|
||||
friend.checked = !friend.checked
|
||||
}
|
||||
|
||||
/** checkbox change:直接落 value(locked 已由 :disabled 拦截,这里再守一层) */
|
||||
function handleCheckChange(friend: FriendCheckable, value: boolean) {
|
||||
if (friend.disabled) {
|
||||
return
|
||||
}
|
||||
friend.checked = value
|
||||
}
|
||||
|
||||
/** 右侧 x 点击:取消勾选(locked 不渲染 x,到这里说明非 locked) */
|
||||
function handleUncheck(friend: FriendCheckable) {
|
||||
friend.checked = false
|
||||
}
|
||||
|
||||
/** 创建群聊:建群 → 拉人 → upsert groupStore,最后 emit('created') 让父页跳转新会话 */
|
||||
async function handleOk() {
|
||||
const name = groupName.value.trim()
|
||||
if (!name) {
|
||||
message.warning('请输入群名称')
|
||||
return
|
||||
}
|
||||
const memberUserIds = checkedFriends.value.map((f) => f.id)
|
||||
if (memberUserIds.length === 0) {
|
||||
message.warning('请至少选择一位好友')
|
||||
const memberUserIds = checkedFriends.value.map((friend) => friend.id)
|
||||
// canSubmit 已挡住空状态,这里再守一道防止 disabled 被外部绕过
|
||||
if (!name || memberUserIds.length === 0) {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
|
|
@ -165,6 +212,7 @@ async function handleOk() {
|
|||
}
|
||||
// 1.2 拉好友入群
|
||||
await inviteGroupMember({ groupId: group.id, memberUserIds })
|
||||
|
||||
// 2.1 直接 upsert 进 groupStore,省一次 fetchGroups——服务端返回 VO 已经够建会话了
|
||||
groupStore.upsertGroup({
|
||||
id: group.id,
|
||||
|
|
@ -182,3 +230,16 @@ async function handleOk() {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 右侧已选行的 x:默认浅灰,hover 转危险色,提示"点了就移除" */
|
||||
.im-group-create-dialog__remove {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.im-group-create-dialog__remove:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,40 @@
|
|||
<template>
|
||||
<!--
|
||||
邀请好友入群对话框
|
||||
- 左:好友列表(带 checkbox)
|
||||
- 右:已勾选预览
|
||||
- 已在群内的好友标记为 disabled
|
||||
- TODO 接入 /im/group/invite;TODO 这个是不是已经接入了?
|
||||
- 左:好友列表(checkbox 前置)
|
||||
- 右:已勾选预览(每行可 x 移除)
|
||||
- 已在群内的好友 disabled,复用 GroupCreateDialog 的视觉
|
||||
-->
|
||||
<el-dialog v-model="visible" title="邀请好友" width="620px" :close-on-click-modal="false">
|
||||
<div class="flex gap-2.5">
|
||||
<div
|
||||
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
|
||||
>
|
||||
<el-input v-model="searchText" placeholder="搜索好友" size="small" clearable>
|
||||
<template #suffix>
|
||||
<Icon icon="ant-design:search-outlined" />
|
||||
<el-input v-model="searchText" placeholder="搜索好友" clearable>
|
||||
<template #prefix>
|
||||
<Icon
|
||||
icon="ant-design:search-outlined"
|
||||
class="text-[var(--el-text-color-placeholder)]"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-scrollbar class="h-[400px]">
|
||||
<!-- TODO @ai: friend? -->
|
||||
<FriendItem
|
||||
v-for="f in shownFriends"
|
||||
:key="f.id"
|
||||
:friend="f"
|
||||
v-for="friend in shownFriends"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:menu="false"
|
||||
:active="false"
|
||||
@click="handleToggleCheck(f)"
|
||||
@click="handleToggleCheck(friend)"
|
||||
>
|
||||
<!-- TODO @AI:checked? -->
|
||||
<el-checkbox
|
||||
:model-value="f.isCheck"
|
||||
:disabled="f.disabled"
|
||||
@click.stop
|
||||
@change="(v) => (f.isCheck = !!v)"
|
||||
/>
|
||||
<template #prefix>
|
||||
<el-checkbox
|
||||
:model-value="friend.checked"
|
||||
:disabled="friend.disabled"
|
||||
@click.stop
|
||||
@change="(value) => handleCheckChange(friend, !!value)"
|
||||
/>
|
||||
</template>
|
||||
</FriendItem>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
|
@ -44,26 +46,35 @@
|
|||
<div
|
||||
class="flex flex-col flex-1 overflow-hidden rounded border border-[var(--el-border-color)]"
|
||||
>
|
||||
<!-- 标题高度对齐左侧 el-input default(32px),保证两侧第一项起点在同一水平 -->
|
||||
<div
|
||||
class="h-10 pl-2.5 leading-10 text-13px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||
class="h-8 pl-2.5 leading-8 text-13px text-[var(--el-text-color-secondary)] border-b border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
已勾选 {{ checkCount }} 位好友
|
||||
已勾选 {{ checkedFriends.length }} 位好友
|
||||
</div>
|
||||
<el-scrollbar class="h-[400px]">
|
||||
<FriendItem
|
||||
v-for="f in checkedFriends"
|
||||
:key="f.id"
|
||||
:friend="f"
|
||||
v-for="friend in checkedFriends"
|
||||
:key="friend.id"
|
||||
:friend="friend"
|
||||
:menu="false"
|
||||
:active="false"
|
||||
/>
|
||||
>
|
||||
<Icon
|
||||
icon="ant-design:close-outlined"
|
||||
class="im-group-member-add-dialog__remove"
|
||||
@click.stop="handleUncheck(friend)"
|
||||
/>
|
||||
</FriendItem>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="handleOk">确 定</el-button>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleOk">
|
||||
完成
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
|
@ -74,6 +85,7 @@ import Icon from '@/components/Icon/src/Icon.vue'
|
|||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { inviteGroupMember } from '@/api/im/group/member'
|
||||
import FriendItem from '../friend/FriendItem.vue'
|
||||
import type { FriendLite } from '../../types'
|
||||
import type { GroupMemberLite } from './GroupMember.vue'
|
||||
|
|
@ -81,8 +93,8 @@ import type { GroupMemberLite } from './GroupMember.vue'
|
|||
defineOptions({ name: 'ImGroupMemberAddDialog' })
|
||||
|
||||
interface FriendCheckable extends FriendLite {
|
||||
isCheck?: boolean
|
||||
disabled?: boolean
|
||||
checked?: boolean
|
||||
disabled?: boolean // 已在群里的标记:checkbox 灰态 + 不计入新邀请,不进右侧列表
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
@ -112,15 +124,16 @@ const visible = computed({
|
|||
})
|
||||
|
||||
const searchText = ref('')
|
||||
/** 本地工作副本(带 isCheck / disabled 标记);和 props.friends 区分以避免直接改 prop */
|
||||
const workingFriends = ref<FriendCheckable[]>([])
|
||||
const submitting = ref(false)
|
||||
const workingFriends = ref<FriendCheckable[]>([]) // 工作副本(带 checked / disabled 标记),与 prop 隔离
|
||||
|
||||
watch(
|
||||
visible,
|
||||
(v) => {
|
||||
if (!v) {
|
||||
(open) => {
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
searchText.value = ''
|
||||
workingFriends.value = props.friends
|
||||
.filter((friend) => !friend.deleted)
|
||||
.map((friend) => {
|
||||
|
|
@ -129,8 +142,8 @@ watch(
|
|||
)
|
||||
return {
|
||||
...friend,
|
||||
disabled: inGroup,
|
||||
isCheck: inGroup
|
||||
checked: inGroup,
|
||||
disabled: inGroup
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
@ -139,36 +152,68 @@ watch(
|
|||
|
||||
/** 左侧展示的好友:按搜索关键字过滤 workingFriends */
|
||||
const shownFriends = computed(() =>
|
||||
workingFriends.value.filter((f) => f.nickname.includes(searchText.value))
|
||||
workingFriends.value.filter((friend) => friend.nickname.includes(searchText.value))
|
||||
)
|
||||
|
||||
/** 本次将被邀请的好友:勾选 + 非已在群成员(disabled 不计入) */
|
||||
const checkedFriends = computed(() => workingFriends.value.filter((f) => f.isCheck && !f.disabled))
|
||||
const checkedFriends = computed(() =>
|
||||
workingFriends.value.filter((friend) => friend.checked && !friend.disabled)
|
||||
)
|
||||
|
||||
/** 已勾选数量:右侧标题展示 */
|
||||
const checkCount = computed(() => checkedFriends.value.length)
|
||||
/** 完成按钮可点:至少有 1 个新邀请的好友 */
|
||||
const canSubmit = computed(() => checkedFriends.value.length > 0)
|
||||
|
||||
/** 行点击:切换勾选态,已在群(disabled)的不响应 */
|
||||
function handleToggleCheck(f: FriendCheckable) {
|
||||
if (!f.disabled) {
|
||||
f.isCheck = !f.isCheck
|
||||
}
|
||||
}
|
||||
|
||||
// TODO @AI:这里接入下?
|
||||
/**
|
||||
* 邀请入群:占位实现——抛 reload 让父侧关弹窗 / 刷新
|
||||
*
|
||||
* TODO 接入 /im/group/invite
|
||||
*/
|
||||
async function handleOk() {
|
||||
const ids = checkedFriends.value.map((f) => f.id)
|
||||
if (ids.length === 0) {
|
||||
message.warning('请选择至少一个好友')
|
||||
function handleToggleCheck(friend: FriendCheckable) {
|
||||
if (friend.disabled) {
|
||||
return
|
||||
}
|
||||
message.info('邀请入群接口待接入,当前为占位实现')
|
||||
emit('reload', ids)
|
||||
visible.value = false
|
||||
friend.checked = !friend.checked
|
||||
}
|
||||
|
||||
/** checkbox change:直接落 value(disabled 已由属性拦截,这里再守一层) */
|
||||
function handleCheckChange(friend: FriendCheckable, value: boolean) {
|
||||
if (friend.disabled) {
|
||||
return
|
||||
}
|
||||
friend.checked = value
|
||||
}
|
||||
|
||||
/** 右侧 x 点击:取消勾选(disabled 不会进右侧列表,到这里说明非 disabled) */
|
||||
function handleUncheck(friend: FriendCheckable) {
|
||||
friend.checked = false
|
||||
}
|
||||
|
||||
/** 邀请入群:调 /im/group/invite,成功后 emit reload 让父侧刷新群成员 */
|
||||
async function handleOk() {
|
||||
if (!props.groupId) {
|
||||
return
|
||||
}
|
||||
const memberUserIds = checkedFriends.value.map((friend) => friend.id)
|
||||
if (memberUserIds.length === 0) {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await inviteGroupMember({ groupId: props.groupId, memberUserIds })
|
||||
message.success('邀请成功')
|
||||
emit('reload', memberUserIds)
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 右侧已选行的 x:默认浅灰,hover 转危险色,提示"点了就移除" */
|
||||
.im-group-member-add-dialog__remove {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.im-group-member-add-dialog__remove:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<template>
|
||||
<!--
|
||||
私聊侧边抽屉
|
||||
- 整体结构对齐 ConversationGroupSide:宫格 + 信息行 + 开关;
|
||||
- "添加 / 清空聊天记录" 按钮在 WeChat 里有,但目前后端没建群-from-私聊 / 清空消息能力,先不加避免做半吊子
|
||||
- 整体结构对齐 ConversationGroupSide:宫格 + 信息行 + 开关
|
||||
- 顶部好友宫格 + "+" tile:点 + 调起 GroupCreateDialog 并锁定对方,对齐微信"基于私聊发起群聊"
|
||||
- "清空聊天记录"按钮在 WeChat 里有,但目前后端没建消息清空能力,先不加避免做半吊子
|
||||
-->
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
|
|
@ -14,7 +15,7 @@
|
|||
>
|
||||
<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">
|
||||
<!-- 好友宫格:单个头像 tile,对齐 GroupSide 视觉,让两种抽屉看起来是一家的 -->
|
||||
<!-- 好友宫格:原 tile + "+" tile,对齐 GroupSide 视觉,让两种抽屉看起来是一家的 -->
|
||||
<div class="im-conversation-private-side__section im-conversation-private-side__friend">
|
||||
<div class="im-conversation-private-side__tile-wrap">
|
||||
<UserAvatar
|
||||
|
|
@ -28,6 +29,18 @@
|
|||
{{ displayName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- + tile:点击调起 GroupCreateDialog,把对方 id 作为 lockedIds 传入 -->
|
||||
<div
|
||||
class="im-conversation-private-side__tile-wrap im-conversation-private-side__tile-wrap--clickable"
|
||||
title="发起群聊"
|
||||
@click="createGroupVisible = true"
|
||||
>
|
||||
<div class="im-conversation-private-side__icon-tile">
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
</div>
|
||||
<div class="im-conversation-private-side__tile-label">添加</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im-conversation-private-side__spacer"></div>
|
||||
|
|
@ -105,6 +118,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子对话框:发起群聊(锁定对方为已选) -->
|
||||
<GroupCreateDialog
|
||||
v-model="createGroupVisible"
|
||||
:friends="friends"
|
||||
:locked-ids="lockedIds"
|
||||
@created="handleGroupCreated"
|
||||
/>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
|
|
@ -112,13 +133,15 @@
|
|||
import { computed, ref, watch } from 'vue'
|
||||
import Icon from '@/components/Icon/src/Icon.vue'
|
||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
||||
import GroupCreateDialog from '../../../../components/group/GroupCreateDialog.vue'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
|
||||
import { useConversationStore } from '@/views/im/home/store/conversationStore'
|
||||
import { useFriendStore } from '@/views/im/home/store/friendStore'
|
||||
import { useGroupStore } from '@/views/im/home/store/groupStore'
|
||||
import { getFriendDisplayName } from '@/views/im/utils/user'
|
||||
import { ImConversationType } from '@/views/im/utils/constants'
|
||||
import type { Conversation, Friend } from '../../../../types'
|
||||
import type { Conversation, Friend, FriendLite } from '../../../../types'
|
||||
|
||||
defineOptions({ name: 'ImConversationPrivateSide' })
|
||||
|
||||
|
|
@ -127,9 +150,11 @@ const props = withDefaults(
|
|||
modelValue?: boolean // 抽屉开关(v-model)
|
||||
conversation?: Conversation | null // 当前会话(取置顶 / 免打扰态)
|
||||
friend?: Friend // 对方好友信息(取头像 / 昵称)
|
||||
friends?: FriendLite[] // 全量好友("+创建群"时给 GroupCreateDialog 选人)
|
||||
}>(),
|
||||
{
|
||||
modelValue: false
|
||||
modelValue: false,
|
||||
friends: () => []
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -145,13 +170,20 @@ const visible = computed({
|
|||
|
||||
const conversationStore = useConversationStore()
|
||||
const friendStore = useFriendStore()
|
||||
const groupStore = useGroupStore()
|
||||
const message = useMessage()
|
||||
|
||||
/** tile 标签 / 后续聊天界面用的展示名:备注优先 */
|
||||
const displayName = computed(() => (props.friend ? getFriendDisplayName(props.friend) : ''))
|
||||
|
||||
/** GroupCreateDialog 锁定 id:把对方默认勾上且不可取消,对应微信"基于私聊发起群聊" */
|
||||
const lockedIds = computed<number[]>(() =>
|
||||
props.friend ? [props.friend.friendUserId] : []
|
||||
)
|
||||
|
||||
const displayNamePopoverVisible = ref(false)
|
||||
const editDisplayName = ref('')
|
||||
const createGroupVisible = ref(false)
|
||||
|
||||
// popover 弹出时把当前备注灌进编辑态,避免上次未保存的脏值
|
||||
watch(displayNamePopoverVisible, (open) => {
|
||||
|
|
@ -190,8 +222,8 @@ function handleMutedChange(value: boolean | string | number) {
|
|||
if (type !== ImConversationType.PRIVATE) {
|
||||
return
|
||||
}
|
||||
friendStore.setMuted(targetId, next).catch((e) => {
|
||||
console.error('[IM ConversationPrivateSide] 切换免打扰失败', { targetId }, e)
|
||||
friendStore.setMuted(targetId, next).catch((error) => {
|
||||
console.error('[IM ConversationPrivateSide] 切换免打扰失败', { targetId }, error)
|
||||
message.error('切换免打扰失败')
|
||||
conversationStore.setMuted(type, targetId, !next)
|
||||
})
|
||||
|
|
@ -204,6 +236,22 @@ function handleTopChange(value: boolean | string | number) {
|
|||
}
|
||||
conversationStore.setTop(props.conversation.type, props.conversation.targetId, !!value)
|
||||
}
|
||||
|
||||
/** 群创建成功:跳到新群会话 + 关掉本侧抽屉,让用户专注新群 */
|
||||
function handleGroupCreated(groupId: number) {
|
||||
const group = groupStore.getGroup(groupId)
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
conversationStore.openConversation(
|
||||
groupId,
|
||||
ImConversationType.GROUP,
|
||||
group.name,
|
||||
group.avatar || '',
|
||||
{ muted: !!group.muted }
|
||||
)
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -220,8 +268,11 @@ function handleTopChange(value: boolean | string | number) {
|
|||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
/* 好友宫格区:留白和 GroupSide__members 对齐,单 tile 居左展示 */
|
||||
/* 好友宫格区:留白和 GroupSide__members 对齐,friend tile + "+" tile 横排 */
|
||||
.im-conversation-private-side__friend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 16px 16px 14px;
|
||||
}
|
||||
|
||||
|
|
@ -231,6 +282,9 @@ function handleTopChange(value: boolean | string | number) {
|
|||
align-items: center;
|
||||
width: 66px;
|
||||
}
|
||||
.im-conversation-private-side__tile-wrap--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.im-conversation-private-side__tile-label {
|
||||
width: 100%;
|
||||
|
|
@ -244,6 +298,33 @@ function handleTopChange(value: boolean | string | number) {
|
|||
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 {
|
||||
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 到当前色 */
|
||||
.im-conversation-private-side__icon-tile :deep(svg) {
|
||||
fill: currentColor !important;
|
||||
}
|
||||
|
||||
/* section 间隔条 */
|
||||
.im-conversation-private-side__spacer {
|
||||
flex-shrink: 0;
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
:group="groupInfo"
|
||||
:conversation="conversationStore.activeConversation"
|
||||
:members="groupMembers"
|
||||
:friends="groupFriends"
|
||||
:friends="friends"
|
||||
@reload="reloadGroupData"
|
||||
@open-history="historyVisible = true"
|
||||
/>
|
||||
|
|
@ -93,6 +93,7 @@
|
|||
v-model="sideVisible"
|
||||
:conversation="conversationStore.activeConversation"
|
||||
:friend="privateFriend"
|
||||
:friends="friends"
|
||||
@open-history="historyVisible = true"
|
||||
/>
|
||||
|
||||
|
|
@ -201,8 +202,8 @@ const groupMembers = computed<GroupMemberLite[]>(() => {
|
|||
})
|
||||
})
|
||||
|
||||
/** 好友列表(用于"邀请入群"对话框):把 friendStore 的全量好友 map 成 FriendLite 窄接口 */
|
||||
const groupFriends = computed<FriendLite[]>(() =>
|
||||
/** 好友列表:群侧用于"邀请入群",私聊侧用于"+创建群",统一从 friendStore 映射成 FriendLite 窄接口 */
|
||||
const friends = computed<FriendLite[]>(() =>
|
||||
friendStore.getActiveFriends.map((friend) => ({
|
||||
id: friend.friendUserId,
|
||||
nickname: friend.nickname,
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- TODO @芋艿:这个只是临时的 -->
|
||||
<div class="im-manager-placeholder">我是好友管理测试界面</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'ImManagerFriend' })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-manager-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
font-size: 18px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- TODO @芋艿:这个只是临时的 -->
|
||||
<div class="im-manager-placeholder">我是群管理测试界面</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'ImManagerGroup' })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-manager-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
font-size: 18px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- TODO @芋艿:这个只是临时的 -->
|
||||
<div class="im-manager-placeholder">我是消息管理测试界面</div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'ImManagerMessage' })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.im-manager-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
font-size: 18px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue