Merge from vben
This commit is contained in:
@@ -1,59 +1,59 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ImGroupRequestApi } from '#/api/im/group/request'
|
||||
import type { ImGroupRequestApi } from '#/api/im/group/request';
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { prompt } from '@vben/common-ui'
|
||||
import { prompt } from '@vben/common-ui';
|
||||
|
||||
import { ElDialog, ElEmpty, ElInput, ElMessage } from 'element-plus'
|
||||
import { ElDialog, ElEmpty, ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { getGroupRequestListByGroupId } from '#/api/im/group/request'
|
||||
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
|
||||
import { getGroupRequestListByGroupId } from '#/api/im/group/request';
|
||||
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants';
|
||||
|
||||
import { useGroupRequestStore } from '../../store/groupRequestStore'
|
||||
import { UserAvatar } from '../user'
|
||||
import { useGroupRequestStore } from '../../store/groupRequestStore';
|
||||
import { UserAvatar } from '../user';
|
||||
|
||||
defineOptions({ name: 'ImGroupRequestListDialog' })
|
||||
defineOptions({ name: 'ImGroupRequestListDialog' });
|
||||
|
||||
const groupRequestStore = useGroupRequestStore()
|
||||
const groupRequestStore = useGroupRequestStore();
|
||||
|
||||
const visible = ref(false)
|
||||
const groupId = ref<number | undefined>() // 当前展示的群编号;undefined 时走全局未处理列表(store.unhandledList)
|
||||
const loading = ref(false)
|
||||
const groupList = ref<ImGroupRequestApi.GroupRequestRespVO[]>([])
|
||||
const actingId = ref<null | number>(null)
|
||||
const visible = ref(false);
|
||||
const groupId = ref<number | undefined>(); // 当前展示的群编号;undefined 时走全局未处理列表(store.unhandledList)
|
||||
const loading = ref(false);
|
||||
const groupList = ref<ImGroupRequestApi.GroupRequestRespVO[]>([]);
|
||||
const actingId = ref<null | number>(null);
|
||||
|
||||
defineExpose({
|
||||
/** 打开进群申请弹窗:reset → 灌参 → visible=true;不传 groupId 走全局未处理列表 */
|
||||
open(opts?: { groupId?: number }) {
|
||||
groupId.value = opts?.groupId
|
||||
actingId.value = null
|
||||
visible.value = true
|
||||
}
|
||||
})
|
||||
groupId.value = opts?.groupId;
|
||||
actingId.value = null;
|
||||
visible.value = true;
|
||||
},
|
||||
});
|
||||
|
||||
/** 数据源:单群模式用 fetch 回来的 groupList;全局模式直接读 store.unhandledList,处理后 store 自动 reactive 同步 */
|
||||
const list = computed<ImGroupRequestApi.GroupRequestRespVO[]>(() =>
|
||||
groupId.value ? groupList.value : groupRequestStore.unhandledList
|
||||
)
|
||||
groupId.value ? groupList.value : groupRequestStore.unhandledList,
|
||||
);
|
||||
|
||||
/** 顶部卡片:最新一条;空数组时为 null */
|
||||
const latest = computed(() => list.value[0] || null)
|
||||
const latest = computed(() => list.value[0] || null);
|
||||
/** 历史列表:除最新一条外的其余 */
|
||||
const histories = computed(() => list.value.slice(1))
|
||||
const histories = computed(() => list.value.slice(1));
|
||||
|
||||
/** 打开 dialog 时拉数据:单群拉 API;全局直接读 store;关闭时清掉单群缓存 */
|
||||
watch(
|
||||
[visible, groupId],
|
||||
([isVisible, currentGroupId]) => {
|
||||
if (isVisible && currentGroupId) {
|
||||
void fetchList(currentGroupId)
|
||||
void fetchList(currentGroupId);
|
||||
} else if (!isVisible) {
|
||||
groupList.value = []
|
||||
groupList.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* 单群模式下订阅 store 中归属本群的未处理列表变化:远端事件(WS 1503 新申请 / 其他管理员处理)触发时 refetch
|
||||
@@ -68,89 +68,92 @@ watch(
|
||||
.filter((request) => request.groupId === groupId.value)
|
||||
.map(
|
||||
(request) =>
|
||||
`${request.id}:${request.inviterUserId ?? ''}:${request.applyContent ?? ''}`
|
||||
`${request.id}:${request.inviterUserId ?? ''}:${request.applyContent ?? ''}`,
|
||||
)
|
||||
.join(',')
|
||||
: null,
|
||||
(current, previous) => {
|
||||
if (current === null || previous === undefined || current === previous) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (actingId.value !== null) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (groupId.value) {
|
||||
void fetchList(groupId.value)
|
||||
void fetchList(groupId.value);
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let fetchSeq = 0 // 单调递增请求序号;同群也会因为 WS 1503 推送触发额外 fetch,乱序返回时旧响应不能覆盖新数据
|
||||
let fetchSeq = 0; // 单调递增请求序号;同群也会因为 WS 1503 推送触发额外 fetch,乱序返回时旧响应不能覆盖新数据
|
||||
async function fetchList(targetGroupId: number) {
|
||||
const seq = ++fetchSeq
|
||||
loading.value = true
|
||||
const seq = ++fetchSeq;
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = (await getGroupRequestListByGroupId(targetGroupId)) || []
|
||||
const data = (await getGroupRequestListByGroupId(targetGroupId)) || [];
|
||||
// 期间切群 / 关弹窗 / 又触发更新 fetch:丢响应
|
||||
if (seq !== fetchSeq || !visible.value || groupId.value !== targetGroupId) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
groupList.value = data
|
||||
groupList.value = data;
|
||||
} finally {
|
||||
// 旧请求 finally 命中时新请求仍在跑,跳过避免提前关 loading
|
||||
if (seq === fetchSeq) {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 同意:走 store 同步全局未处理列表 + 本地更新 handleResult 让按钮变灰 */
|
||||
async function handleAgree(item: ImGroupRequestApi.GroupRequestRespVO) {
|
||||
if (actingId.value !== null) return
|
||||
actingId.value = item.id
|
||||
if (actingId.value !== null) return;
|
||||
actingId.value = item.id;
|
||||
try {
|
||||
await groupRequestStore.agreeGroupRequest(item.id)
|
||||
updateLocalResult(item.id, ImGroupRequestHandleResult.AGREED)
|
||||
ElMessage.success('已同意')
|
||||
await groupRequestStore.agreeGroupRequest(item.id);
|
||||
updateLocalResult(item.id, ImGroupRequestHandleResult.AGREED);
|
||||
ElMessage.success('已同意');
|
||||
} finally {
|
||||
actingId.value = null
|
||||
actingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 拒绝:弹理由输入框;为空则不带 handleContent */
|
||||
async function handleRefuse(item: ImGroupRequestApi.GroupRequestRespVO) {
|
||||
if (actingId.value !== null) return
|
||||
let handleContent: string
|
||||
if (actingId.value !== null) return;
|
||||
let handleContent: string;
|
||||
try {
|
||||
const result = await prompt<string>({
|
||||
component: ElInput,
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入拒绝理由(可选)'
|
||||
placeholder: '请输入拒绝理由(可选)',
|
||||
},
|
||||
content: '',
|
||||
modelPropName: 'value',
|
||||
title: '拒绝申请'
|
||||
})
|
||||
handleContent = result || ''
|
||||
title: '拒绝申请',
|
||||
});
|
||||
handleContent = result || '';
|
||||
} catch {
|
||||
return
|
||||
return;
|
||||
}
|
||||
actingId.value = item.id
|
||||
actingId.value = item.id;
|
||||
try {
|
||||
await groupRequestStore.refuseGroupRequest(item.id, handleContent || undefined)
|
||||
updateLocalResult(item.id, ImGroupRequestHandleResult.REFUSED)
|
||||
ElMessage.success('已拒绝')
|
||||
await groupRequestStore.refuseGroupRequest(
|
||||
item.id,
|
||||
handleContent || undefined,
|
||||
);
|
||||
updateLocalResult(item.id, ImGroupRequestHandleResult.REFUSED);
|
||||
ElMessage.success('已拒绝');
|
||||
} finally {
|
||||
actingId.value = null
|
||||
actingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 单群模式下处理后更新 groupList 里的 handleResult,按钮转「已同意 / 已拒绝」灰态;全局模式 store 直接移除该项无需更新 */
|
||||
function updateLocalResult(id: number, handleResult: number) {
|
||||
const target = groupList.value.find((r) => r.id === id)
|
||||
const target = groupList.value.find((r) => r.id === id);
|
||||
if (target) {
|
||||
target.handleResult = handleResult
|
||||
target.handleResult = handleResult;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -166,92 +169,98 @@ function updateLocalResult(id: number, handleResult: number) {
|
||||
v-model="visible"
|
||||
title="进群申请"
|
||||
width="560px"
|
||||
|
||||
:close-on-click-modal="false"
|
||||
class="im-group-request-list__dialog"
|
||||
>
|
||||
<div v-loading="loading" class="w-full">
|
||||
<div class="flex flex-col gap-3 max-h-[60vh] overflow-y-auto pr-1">
|
||||
<!-- 空态 -->
|
||||
<ElEmpty v-if="!loading && list.length === 0" description="暂无进群申请" />
|
||||
<ElEmpty
|
||||
v-if="!loading && list.length === 0"
|
||||
description="暂无进群申请"
|
||||
/>
|
||||
|
||||
<!-- 顶部卡片:最新一条 -->
|
||||
<!-- 顶部卡片:最新一条 -->
|
||||
<div
|
||||
v-if="latest"
|
||||
class="flex flex-col gap-2.5 p-3.5 rounded-[10px] border border-solid border-[var(--ant-color-border-secondary)] bg-[var(--ant-color-bg-container)] 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="flex-1 min-w-0">
|
||||
<div
|
||||
class="truncate text-sm font-medium leading-[1.4] text-[var(--ant-color-text)]"
|
||||
>
|
||||
{{ latest.userNickname || `用户 ${latest.userId}` }}
|
||||
<div class="flex items-center gap-3">
|
||||
<UserAvatar
|
||||
:url="latest.userAvatar"
|
||||
:name="latest.userNickname"
|
||||
:size="44"
|
||||
:clickable="false"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div
|
||||
class="truncate text-sm font-medium leading-[1.4] text-[var(--ant-color-text)]"
|
||||
>
|
||||
{{ latest.userNickname || `用户 ${latest.userId}` }}
|
||||
</div>
|
||||
<div
|
||||
class="truncate mt-[2px] text-12px leading-[1.5] text-[var(--ant-color-text-secondary)]"
|
||||
>
|
||||
<template v-if="latest.inviterUserId">
|
||||
通过
|
||||
<span class="text-[var(--ant-color-primary)]">
|
||||
{{
|
||||
latest.inviterNickname || `用户 ${latest.inviterUserId}`
|
||||
}}
|
||||
</span>
|
||||
的邀请进群
|
||||
</template>
|
||||
<template v-else>申请加入</template>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="truncate mt-[2px] text-12px leading-[1.5] text-[var(--ant-color-text-secondary)]"
|
||||
<span
|
||||
v-if="latest.handleResult === ImGroupRequestHandleResult.AGREED"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
|
||||
>
|
||||
<template v-if="latest.inviterUserId">
|
||||
通过
|
||||
<span class="text-[var(--ant-color-primary)]">
|
||||
{{ latest.inviterNickname || `用户 ${latest.inviterUserId}` }}
|
||||
</span>
|
||||
的邀请进群
|
||||
</template>
|
||||
<template v-else>申请加入</template>
|
||||
已同意
|
||||
</span>
|
||||
<span
|
||||
v-else-if="
|
||||
latest.handleResult === ImGroupRequestHandleResult.REFUSED
|
||||
"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
|
||||
>
|
||||
已拒绝
|
||||
</span>
|
||||
<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"
|
||||
@click="handleAgree(latest)"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
<button
|
||||
class="im-group-request-list__btn im-group-request-list__btn--ghost"
|
||||
:disabled="actingId === latest.id"
|
||||
@click="handleRefuse(latest)"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="latest.handleResult === ImGroupRequestHandleResult.AGREED"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
|
||||
<!-- 申请理由:邀请场景显示邀请人 + 留言;主动申请显示申请人 + 留言 -->
|
||||
<div
|
||||
v-if="latest.applyContent"
|
||||
class="px-3 py-2 rounded-md text-[13px] leading-[1.5] break-all bg-[var(--ant-color-fill-secondary)] text-[var(--ant-color-text)]"
|
||||
>
|
||||
已同意
|
||||
</span>
|
||||
<span
|
||||
v-else-if="latest.handleResult === ImGroupRequestHandleResult.REFUSED"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
|
||||
>
|
||||
已拒绝
|
||||
</span>
|
||||
<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"
|
||||
@click="handleAgree(latest)"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
<button
|
||||
class="im-group-request-list__btn im-group-request-list__btn--ghost"
|
||||
:disabled="actingId === latest.id"
|
||||
@click="handleRefuse(latest)"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
<span class="text-[var(--ant-color-primary)]">
|
||||
{{
|
||||
latest.inviterUserId
|
||||
? latest.inviterNickname || `用户 ${latest.inviterUserId}`
|
||||
: latest.userNickname || `用户 ${latest.userId}`
|
||||
}}:
|
||||
</span>
|
||||
{{ latest.applyContent }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 申请理由:邀请场景显示邀请人 + 留言;主动申请显示申请人 + 留言 -->
|
||||
<div
|
||||
v-if="latest.applyContent"
|
||||
class="px-3 py-2 rounded-md text-[13px] leading-[1.5] break-all bg-[var(--ant-color-fill-secondary)] text-[var(--ant-color-text)]"
|
||||
>
|
||||
<span class="text-[var(--ant-color-primary)]">
|
||||
{{
|
||||
latest.inviterUserId
|
||||
? latest.inviterNickname || `用户 ${latest.inviterUserId}`
|
||||
: latest.userNickname || `用户 ${latest.userId}`
|
||||
}}:
|
||||
</span>
|
||||
{{ latest.applyContent }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线:仅在有更早申请时出现 -->
|
||||
<!-- 分割线:仅在有更早申请时出现 -->
|
||||
<div
|
||||
v-if="histories.length > 0"
|
||||
class="flex items-center justify-center mt-1.5 -mb-0.5 text-12px text-[var(--ant-color-text-placeholder)]"
|
||||
@@ -265,61 +274,63 @@ function updateLocalResult(id: number, handleResult: number) {
|
||||
:key="item.id"
|
||||
class="flex flex-col gap-2.5 px-3.5 py-2.5 rounded-[10px] border border-solid border-[var(--ant-color-border-secondary)] bg-[var(--ant-color-bg-container)] shadow-[0_1px_3px_rgba(0,0,0,0.04)]"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<UserAvatar
|
||||
:url="item.userAvatar"
|
||||
:name="item.userNickname"
|
||||
:size="40"
|
||||
:clickable="false"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div
|
||||
class="truncate text-sm font-medium leading-[1.4] text-[var(--ant-color-text)]"
|
||||
>
|
||||
{{ item.userNickname || `用户 ${item.userId}` }}
|
||||
<div class="flex items-center gap-3">
|
||||
<UserAvatar
|
||||
:url="item.userAvatar"
|
||||
:name="item.userNickname"
|
||||
:size="40"
|
||||
:clickable="false"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div
|
||||
class="truncate text-sm font-medium leading-[1.4] text-[var(--ant-color-text)]"
|
||||
>
|
||||
{{ item.userNickname || `用户 ${item.userId}` }}
|
||||
</div>
|
||||
<div
|
||||
class="truncate mt-[2px] text-12px leading-[1.5] text-[var(--ant-color-text-secondary)]"
|
||||
>
|
||||
<template v-if="item.inviterUserId">
|
||||
通过
|
||||
<span class="text-[var(--ant-color-primary)]">
|
||||
{{ item.inviterNickname || `用户 ${item.inviterUserId}` }}
|
||||
</span>
|
||||
的邀请进群
|
||||
</template>
|
||||
<template v-else>申请加入</template>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="truncate mt-[2px] text-12px leading-[1.5] text-[var(--ant-color-text-secondary)]"
|
||||
<span
|
||||
v-if="item.handleResult === ImGroupRequestHandleResult.AGREED"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
|
||||
>
|
||||
<template v-if="item.inviterUserId">
|
||||
通过
|
||||
<span class="text-[var(--ant-color-primary)]">
|
||||
{{ item.inviterNickname || `用户 ${item.inviterUserId}` }}
|
||||
</span>
|
||||
的邀请进群
|
||||
</template>
|
||||
<template v-else>申请加入</template>
|
||||
已同意
|
||||
</span>
|
||||
<span
|
||||
v-else-if="
|
||||
item.handleResult === ImGroupRequestHandleResult.REFUSED
|
||||
"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
|
||||
>
|
||||
已拒绝
|
||||
</span>
|
||||
<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"
|
||||
@click="handleAgree(item)"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
<button
|
||||
class="im-group-request-list__btn im-group-request-list__btn--ghost"
|
||||
:disabled="actingId === item.id"
|
||||
@click="handleRefuse(item)"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="item.handleResult === ImGroupRequestHandleResult.AGREED"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
|
||||
>
|
||||
已同意
|
||||
</span>
|
||||
<span
|
||||
v-else-if="item.handleResult === ImGroupRequestHandleResult.REFUSED"
|
||||
class="flex-shrink-0 text-[13px] text-[var(--ant-color-text-placeholder)]"
|
||||
>
|
||||
已拒绝
|
||||
</span>
|
||||
<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"
|
||||
@click="handleAgree(item)"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
<button
|
||||
class="im-group-request-list__btn im-group-request-list__btn--ghost"
|
||||
:disabled="actingId === item.id"
|
||||
@click="handleRefuse(item)"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -334,17 +345,18 @@ function updateLocalResult(id: number, handleResult: number) {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
transition:
|
||||
background-color 0.15s,
|
||||
border-color 0.15s,
|
||||
color 0.15s;
|
||||
}
|
||||
|
||||
.im-group-request-list__btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.im-group-request-list__btn--primary {
|
||||
@@ -352,6 +364,7 @@ function updateLocalResult(id: number, handleResult: number) {
|
||||
background-color: var(--ant-color-primary);
|
||||
border-color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
.im-group-request-list__btn--primary:hover:not(:disabled) {
|
||||
background-color: var(--ant-color-primary-hover);
|
||||
border-color: var(--ant-color-primary-hover);
|
||||
@@ -362,6 +375,7 @@ function updateLocalResult(id: number, handleResult: number) {
|
||||
background-color: var(--ant-color-bg-container);
|
||||
border-color: var(--ant-color-border);
|
||||
}
|
||||
|
||||
.im-group-request-list__btn--ghost:hover:not(:disabled) {
|
||||
color: var(--ant-color-primary);
|
||||
border-color: var(--ant-color-primary);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// IM 选择类弹窗的公共样式 mixin
|
||||
// 每个业务壳在自己的 <style scoped lang="scss"> 内 @use + @include 即可,避免全局污染
|
||||
/**
|
||||
* IM 选择类弹窗的公共样式 mixin
|
||||
* 每个业务壳在自己的 <style scoped lang="scss"> 内 @use + @include 即可,避免全局污染
|
||||
*/
|
||||
@mixin styles {
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
@@ -7,7 +9,7 @@
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
margin-right: 0;
|
||||
padding-bottom: 16px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Conversation } from '../../../../types'
|
||||
import type { Conversation } from '../../../../types';
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { confirm } from '@vben/common-ui'
|
||||
import { IconifyIcon as Icon } from '@vben/icons'
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import { IconifyIcon as Icon } from '@vben/icons';
|
||||
|
||||
import { ElTag } from 'element-plus'
|
||||
import { ElTag } from 'element-plus';
|
||||
|
||||
import { buildRecallTip } from '#/views/im/utils/conversation'
|
||||
import { formatConversationTime } from '#/views/im/utils/time'
|
||||
import { getSenderDisplayName } from '#/views/im/utils/user'
|
||||
import { buildRecallTip } from '#/views/im/utils/conversation';
|
||||
import { formatConversationTime } from '#/views/im/utils/time';
|
||||
import { getSenderDisplayName } from '#/views/im/utils/user';
|
||||
|
||||
import { ImContentType, ImConversationType, isNormalMessage } from '../../../../../utils/constants'
|
||||
import { GroupAvatar } from '../../../../components/group'
|
||||
import { UserAvatar } from '../../../../components/user'
|
||||
import { useConversationStore } from '../../../../store/conversationStore'
|
||||
import { useFriendStore } from '../../../../store/friendStore'
|
||||
import { useGroupRequestStore } from '../../../../store/groupRequestStore'
|
||||
import { useGroupStore } from '../../../../store/groupStore'
|
||||
import { useImUiStore } from '../../../../store/uiStore'
|
||||
import {
|
||||
ImContentType,
|
||||
ImConversationType,
|
||||
isNormalMessage,
|
||||
} from '../../../../../utils/constants';
|
||||
import { GroupAvatar } from '../../../../components/group';
|
||||
import { UserAvatar } from '../../../../components/user';
|
||||
import { useConversationStore } from '../../../../store/conversationStore';
|
||||
import { useFriendStore } from '../../../../store/friendStore';
|
||||
import { useGroupRequestStore } from '../../../../store/groupRequestStore';
|
||||
import { useGroupStore } from '../../../../store/groupStore';
|
||||
import { useImUiStore } from '../../../../store/uiStore';
|
||||
|
||||
defineOptions({ name: 'ImConversationItem' })
|
||||
defineOptions({ name: 'ImConversationItem' });
|
||||
|
||||
// 周中文名(dayjs 的 day() 返回 0-6,0=周日);项目没全局装 dayjs/locale/zh-cn,本地映射避免引副作用
|
||||
const props = defineProps<{
|
||||
conversation: Conversation
|
||||
}>()
|
||||
conversation: Conversation;
|
||||
}>();
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const friendStore = useFriendStore()
|
||||
const groupStore = useGroupStore()
|
||||
const groupRequestStore = useGroupRequestStore()
|
||||
const uiStore = useImUiStore()
|
||||
const conversationStore = useConversationStore();
|
||||
const friendStore = useFriendStore();
|
||||
const groupStore = useGroupStore();
|
||||
const groupRequestStore = useGroupRequestStore();
|
||||
const uiStore = useImUiStore();
|
||||
|
||||
const isActive = computed(
|
||||
() =>
|
||||
conversationStore.activeConversation?.targetId === props.conversation.targetId &&
|
||||
conversationStore.activeConversation?.type === props.conversation.type
|
||||
)
|
||||
conversationStore.activeConversation?.targetId ===
|
||||
props.conversation.targetId &&
|
||||
conversationStore.activeConversation?.type === props.conversation.type,
|
||||
);
|
||||
|
||||
/**
|
||||
* 当前会话的草稿快照:存在时列表显示 [草稿] 前缀 + 文本,盖掉 sender 前缀和 @我 红字
|
||||
@@ -46,97 +51,105 @@ const isActive = computed(
|
||||
*/
|
||||
const draft = computed(() => {
|
||||
if (isActive.value) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
return conversationStore.getConversationDraft(props.conversation)
|
||||
})
|
||||
return conversationStore.getConversationDraft(props.conversation);
|
||||
});
|
||||
|
||||
const isGroup = computed(() => props.conversation.type === ImConversationType.GROUP)
|
||||
const isGroup = computed(
|
||||
() => props.conversation.type === ImConversationType.GROUP,
|
||||
);
|
||||
|
||||
/** 最后一条消息发送者的展示名:实时算 + 快照 fallback(getSenderDisplayName 算不出时兜底) */
|
||||
const lastSenderDisplayName = computed(() => {
|
||||
const senderId = props.conversation.lastSenderId
|
||||
const senderId = props.conversation.lastSenderId;
|
||||
if (!senderId) {
|
||||
return ''
|
||||
return '';
|
||||
}
|
||||
return getSenderDisplayName(
|
||||
senderId,
|
||||
props.conversation.type,
|
||||
props.conversation.targetId,
|
||||
props.conversation.lastSenderDisplayName
|
||||
)
|
||||
})
|
||||
props.conversation.lastSenderDisplayName,
|
||||
);
|
||||
});
|
||||
|
||||
/** 群聊 + 有最后发送者 + 最后一条是普通消息时,显示发送者前缀(FRIEND_* / GROUP_* / RECALL / 草稿态不带前缀) */
|
||||
const showSendName = computed(() => {
|
||||
if (draft.value) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
if (!isGroup.value) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
if (!props.conversation.lastSenderId) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
const lastType = props.conversation.lastMessageType
|
||||
return lastType != null && isNormalMessage(lastType)
|
||||
})
|
||||
const lastType = props.conversation.lastMessageType;
|
||||
return lastType != null && isNormalMessage(lastType);
|
||||
});
|
||||
|
||||
/** 列表展示文案:草稿优先(对齐微信 PC:有草稿时盖掉最后一条预览)→ 撤回实时算 → lastContent 兜底 */
|
||||
const lastContentDisplay = computed(() => {
|
||||
if (draft.value) {
|
||||
return draft.value.plain
|
||||
return draft.value.plain;
|
||||
}
|
||||
if (
|
||||
props.conversation.lastMessageType === ImContentType.RECALL &&
|
||||
props.conversation.lastSenderId != null
|
||||
props.conversation.lastSenderId !== null
|
||||
) {
|
||||
return buildRecallTip(
|
||||
props.conversation.lastSenderId,
|
||||
!!props.conversation.lastSelfSend,
|
||||
props.conversation.type,
|
||||
props.conversation.targetId,
|
||||
props.conversation.lastSenderDisplayName
|
||||
)
|
||||
props.conversation.lastSenderDisplayName,
|
||||
);
|
||||
}
|
||||
return props.conversation.lastContent
|
||||
})
|
||||
return props.conversation.lastContent;
|
||||
});
|
||||
|
||||
/** 会话列表 "[草稿]" / "@ 我" / "@ 全体成员" 红字提示;草稿优先(对齐微信 PC) */
|
||||
const atText = computed(() => {
|
||||
if (draft.value) {
|
||||
return '[草稿]'
|
||||
return '[草稿]';
|
||||
}
|
||||
if (props.conversation.atMe) {
|
||||
return '[有人@我]'
|
||||
return '[有人@我]';
|
||||
}
|
||||
if (props.conversation.atAll) {
|
||||
return '[@全体成员]'
|
||||
return '[@全体成员]';
|
||||
}
|
||||
return ''
|
||||
})
|
||||
return '';
|
||||
});
|
||||
|
||||
/** 免打扰会话未读条数文案 */
|
||||
const mutedUnreadText = computed(() => {
|
||||
if (!props.conversation.silent || props.conversation.unreadCount <= 0) {
|
||||
return ''
|
||||
return '';
|
||||
}
|
||||
const count = props.conversation.unreadCount > 99 ? '99+' : props.conversation.unreadCount
|
||||
return `[${count}条]`
|
||||
})
|
||||
const count =
|
||||
props.conversation.unreadCount > 99
|
||||
? '99+'
|
||||
: props.conversation.unreadCount;
|
||||
return `[${count}条]`;
|
||||
});
|
||||
|
||||
/** 群聊未处理加群申请红字前缀;store 已经按「我管理的群」过滤过,count > 0 即可显示 */
|
||||
const requestText = computed(() => {
|
||||
if (!isGroup.value) {
|
||||
return ''
|
||||
return '';
|
||||
}
|
||||
const count = groupRequestStore.getUnhandledGroupRequestCountMap.get(props.conversation.targetId) ?? 0
|
||||
return count > 0 ? `[${count}条进群申请]` : ''
|
||||
})
|
||||
const count =
|
||||
groupRequestStore.getUnhandledGroupRequestCountMap.get(
|
||||
props.conversation.targetId,
|
||||
) ?? 0;
|
||||
return count > 0 ? `[${count}条进群申请]` : '';
|
||||
});
|
||||
|
||||
/** 点击切会话 */
|
||||
function handleClick() {
|
||||
conversationStore.setActiveConversation(props.conversation)
|
||||
conversationStore.setActiveConversation(props.conversation);
|
||||
}
|
||||
|
||||
/** 切换置顶 */
|
||||
@@ -144,30 +157,36 @@ function handleTop() {
|
||||
conversationStore.setConversationTop(
|
||||
props.conversation.type,
|
||||
props.conversation.targetId,
|
||||
!props.conversation.top
|
||||
)
|
||||
!props.conversation.top,
|
||||
);
|
||||
}
|
||||
|
||||
/** 切换免打扰:乐观 UI(先本地切换,菜单立即关;后端失败回滚 conversation 状态) */
|
||||
function handleMuted() {
|
||||
const next = !props.conversation.silent
|
||||
const { type, targetId } = props.conversation
|
||||
conversationStore.setConversationSilent(type, targetId, next)
|
||||
const next = !props.conversation.silent;
|
||||
const { type, targetId } = props.conversation;
|
||||
conversationStore.setConversationSilent(type, targetId, next);
|
||||
const sync =
|
||||
type === ImConversationType.PRIVATE
|
||||
? friendStore.setFriendSilent(targetId, next)
|
||||
: groupStore.setGroupSilent(targetId, next)
|
||||
: groupStore.setGroupSilent(targetId, next);
|
||||
sync.catch((error) => {
|
||||
console.error('[IM] 切换免打扰失败', error)
|
||||
conversationStore.setConversationSilent(type, targetId, !next)
|
||||
})
|
||||
console.error('[IM] 切换免打扰失败', error);
|
||||
conversationStore.setConversationSilent(type, targetId, !next);
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除会话:二次确认后软删 */
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await confirm(`确定删除与「${props.conversation.name}」的会话吗?`, '删除会话')
|
||||
conversationStore.removeConversation(props.conversation.type, props.conversation.targetId)
|
||||
await confirm(
|
||||
`确定删除与「${props.conversation.name}」的会话吗?`,
|
||||
'删除会话',
|
||||
);
|
||||
conversationStore.removeConversation(
|
||||
props.conversation.type,
|
||||
props.conversation.targetId,
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -177,38 +196,42 @@ function handleContextMenu(e: MouseEvent) {
|
||||
{ x: e.clientX, y: e.clientY },
|
||||
[
|
||||
{ key: 'TOP', name: props.conversation.top ? '取消置顶' : '置顶' },
|
||||
{ key: 'MUTED', name: props.conversation.silent ? '允许消息通知' : '消息免打扰' },
|
||||
{ key: 'DELETE', name: '删除', divided: true, danger: true }
|
||||
{
|
||||
key: 'MUTED',
|
||||
name: props.conversation.silent ? '允许消息通知' : '消息免打扰',
|
||||
},
|
||||
{ key: 'DELETE', name: '删除', divided: true, danger: true },
|
||||
],
|
||||
(item) => {
|
||||
switch (item.key) {
|
||||
case 'DELETE': {
|
||||
void handleDelete()
|
||||
case 'DELETE': {
|
||||
void handleDelete();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'MUTED': {
|
||||
handleMuted()
|
||||
break;
|
||||
}
|
||||
case 'MUTED': {
|
||||
handleMuted();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'TOP': {
|
||||
handleTop()
|
||||
break;
|
||||
}
|
||||
case 'TOP': {
|
||||
handleTop();
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex items-center gap-2.5 px-4 py-3 cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
|
||||
:class="{ '!bg-[#d9ecff] dark:!bg-[var(--ant-color-primary-bg-hover)]': isActive }"
|
||||
:class="{
|
||||
'!bg-[#d9ecff] dark:!bg-[var(--ant-color-primary-bg-hover)]': isActive,
|
||||
}"
|
||||
:data-conversation-key="`${conversation.type}-${conversation.targetId}`"
|
||||
@click="handleClick"
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
@@ -246,7 +269,9 @@ function handleContextMenu(e: MouseEvent) {
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex flex-1 items-center gap-1 min-w-0">
|
||||
<span class="overflow-hidden text-sm truncate text-[var(--ant-color-text)]">
|
||||
<span
|
||||
class="overflow-hidden text-sm truncate text-[var(--ant-color-text)]"
|
||||
>
|
||||
{{ conversation.name }}
|
||||
</span>
|
||||
<ElTag
|
||||
@@ -258,7 +283,9 @@ function handleContextMenu(e: MouseEvent) {
|
||||
群
|
||||
</ElTag>
|
||||
</span>
|
||||
<span class="flex-shrink-0 ml-1 text-12px text-[var(--ant-color-text-secondary)]">
|
||||
<span
|
||||
class="flex-shrink-0 ml-1 text-12px text-[var(--ant-color-text-secondary)]"
|
||||
>
|
||||
{{ formatConversationTime(conversation.lastSendTime) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -317,7 +344,7 @@ function handleContextMenu(e: MouseEvent) {
|
||||
|
||||
/* el-icon 的全局 color:var(--color) 在暗色模式下会渲染成白色,这里用 :deep + !important 锁定 */
|
||||
.conversation-item__silent :deep(svg) {
|
||||
fill: currentColor !important;
|
||||
fill: currentcolor !important;
|
||||
}
|
||||
|
||||
.conversation-item__prefix {
|
||||
|
||||
@@ -1,86 +1,101 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Conversation, Friend } from '../../../../types'
|
||||
import type { Conversation, Friend } from '../../../../types';
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon as Icon } from '@vben/icons'
|
||||
import { IconifyIcon as Icon } from '@vben/icons';
|
||||
|
||||
import { ElButton, ElDrawer, ElInput, ElMessage, ElPopover, ElSwitch } from 'element-plus'
|
||||
import {
|
||||
ElButton,
|
||||
ElDrawer,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElPopover,
|
||||
ElSwitch,
|
||||
} from 'element-plus';
|
||||
|
||||
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 { ImConversationType } from '#/views/im/utils/constants'
|
||||
import { getFriendDisplayName, getGroupDisplayName } from '#/views/im/utils/user'
|
||||
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 { ImConversationType } from '#/views/im/utils/constants';
|
||||
import {
|
||||
getFriendDisplayName,
|
||||
getGroupDisplayName,
|
||||
} from '#/views/im/utils/user';
|
||||
|
||||
import { GroupCreateDialog } from '../../../../components/group'
|
||||
import { UserAvatar } from '../../../../components/user'
|
||||
import { GroupCreateDialog } from '../../../../components/group';
|
||||
import { UserAvatar } from '../../../../components/user';
|
||||
|
||||
defineOptions({ name: 'ImConversationPrivateSide' })
|
||||
defineOptions({ name: 'ImConversationPrivateSide' });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
conversation?: Conversation | null // 当前会话(取置顶 / 免打扰态)
|
||||
friend?: Friend // 对方好友信息(取头像 / 昵称)
|
||||
modelValue?: boolean // 抽屉开关(v-model)
|
||||
conversation?: Conversation | null; // 当前会话(取置顶 / 免打扰态)
|
||||
friend?: Friend; // 对方好友信息(取头像 / 昵称)
|
||||
modelValue?: boolean; // 抽屉开关(v-model)
|
||||
}>(),
|
||||
{
|
||||
conversation: null,
|
||||
friend: undefined,
|
||||
modelValue: false
|
||||
}
|
||||
)
|
||||
modelValue: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
openHistory: [] // 点击 "查找聊天内容" 行 → 父组件打开 MessageHistory 弹窗
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
openHistory: []; // 点击 "查找聊天内容" 行 → 父组件打开 MessageHistory 弹窗
|
||||
'update:modelValue': [value: boolean];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
});
|
||||
|
||||
const conversationStore = useConversationStore()
|
||||
const friendStore = useFriendStore()
|
||||
const groupStore = useGroupStore()
|
||||
const conversationStore = useConversationStore();
|
||||
const friendStore = useFriendStore();
|
||||
const groupStore = useGroupStore();
|
||||
|
||||
/** tile 标签 / 后续聊天界面用的展示名:备注优先 */
|
||||
const displayName = computed(() => (props.friend ? getFriendDisplayName(props.friend) : ''))
|
||||
const displayName = computed(() =>
|
||||
props.friend ? getFriendDisplayName(props.friend) : '',
|
||||
);
|
||||
|
||||
const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>() // 发起群聊弹窗 ref:handleOpenCreateGroup 调 open({ lockedIds }) 锁定对方
|
||||
const createGroupDialogRef = ref<InstanceType<typeof GroupCreateDialog>>(); // 发起群聊弹窗 ref:handleOpenCreateGroup 调 open({ lockedIds }) 锁定对方
|
||||
|
||||
/** 打开发起群聊弹窗:把对方默认勾上且不可取消,对应微信"基于私聊发起群聊" */
|
||||
function handleOpenCreateGroup() {
|
||||
const lockedIds = props.friend ? [props.friend.friendUserId] : []
|
||||
createGroupDialogRef.value?.open({ lockedIds })
|
||||
const lockedIds = props.friend ? [props.friend.friendUserId] : [];
|
||||
createGroupDialogRef.value?.open({ lockedIds });
|
||||
}
|
||||
|
||||
const displayNamePopoverVisible = ref(false)
|
||||
const editDisplayName = ref('')
|
||||
const displayNamePopoverVisible = ref(false);
|
||||
const editDisplayName = ref('');
|
||||
|
||||
// popover 弹出时把当前备注灌进编辑态,避免上次未保存的脏值
|
||||
watch(displayNamePopoverVisible, (open) => {
|
||||
if (open) {
|
||||
editDisplayName.value = props.friend?.displayName || ''
|
||||
editDisplayName.value = props.friend?.displayName || '';
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 抽屉关闭时把还没收掉的 popover 一并清掉,避免下次打开闪一下
|
||||
watch(visible, (open) => {
|
||||
if (!open) {
|
||||
displayNamePopoverVisible.value = false
|
||||
displayNamePopoverVisible.value = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/** 备注 popover 点击保存 */
|
||||
async function handleSaveDisplayName() {
|
||||
if (!props.friend) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
await friendStore.setFriendDisplayName(props.friend.friendUserId, editDisplayName.value)
|
||||
displayNamePopoverVisible.value = false
|
||||
ElMessage.success('保存成功')
|
||||
await friendStore.setFriendDisplayName(
|
||||
props.friend.friendUserId,
|
||||
editDisplayName.value,
|
||||
);
|
||||
displayNamePopoverVisible.value = false;
|
||||
ElMessage.success('保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,42 +103,50 @@ async function handleSaveDisplayName() {
|
||||
*/
|
||||
function handleMutedChange(value: boolean | number | string) {
|
||||
if (!props.conversation) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const next = !!value
|
||||
const { type, targetId } = props.conversation
|
||||
conversationStore.setConversationSilent(type, targetId, next)
|
||||
const next = !!value;
|
||||
const { type, targetId } = props.conversation;
|
||||
conversationStore.setConversationSilent(type, targetId, next);
|
||||
if (type !== ImConversationType.PRIVATE) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
friendStore.setFriendSilent(targetId, next).catch((error) => {
|
||||
console.error('[IM ConversationPrivateSide] 切换免打扰失败', { targetId }, error)
|
||||
conversationStore.setConversationSilent(type, targetId, !next)
|
||||
})
|
||||
console.error(
|
||||
'[IM ConversationPrivateSide] 切换免打扰失败',
|
||||
{ targetId },
|
||||
error,
|
||||
);
|
||||
conversationStore.setConversationSilent(type, targetId, !next);
|
||||
});
|
||||
}
|
||||
|
||||
/** 切置顶:纯本地 conversationStore 排序态(无后端字段) */
|
||||
function handleTopChange(value: boolean | number | string) {
|
||||
if (!props.conversation) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
conversationStore.setConversationTop(props.conversation.type, props.conversation.targetId, !!value)
|
||||
conversationStore.setConversationTop(
|
||||
props.conversation.type,
|
||||
props.conversation.targetId,
|
||||
!!value,
|
||||
);
|
||||
}
|
||||
|
||||
/** 群创建成功:跳到新群会话 + 关掉本侧抽屉,让用户专注新群 */
|
||||
function handleGroupCreated(groupId: number) {
|
||||
const group = groupStore.getGroup(groupId)
|
||||
const group = groupStore.getGroup(groupId);
|
||||
if (!group) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
conversationStore.openConversation(
|
||||
groupId,
|
||||
ImConversationType.GROUP,
|
||||
getGroupDisplayName(group),
|
||||
group.avatar || '',
|
||||
{ silent: !!group.silent }
|
||||
)
|
||||
visible.value = false
|
||||
{ silent: !!group.silent },
|
||||
);
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -152,7 +175,9 @@ function handleGroupCreated(groupId: number) {
|
||||
<div v-else class="flex flex-col h-full bg-[var(--ant-color-bg-container)]">
|
||||
<div class="flex-1 overflow-y-auto bg-[var(--ant-color-fill-secondary)]">
|
||||
<!-- 好友宫格:原 tile + "+" tile,对齐 GroupSide 视觉,让两种抽屉看起来是一家的 -->
|
||||
<div class="flex flex-wrap gap-1 px-4 pt-4 pb-[14px] bg-[var(--ant-color-bg-container)]">
|
||||
<div
|
||||
class="flex flex-wrap gap-1 px-4 pt-4 pb-[14px] bg-[var(--ant-color-bg-container)]"
|
||||
>
|
||||
<div class="flex flex-col items-center w-[66px]">
|
||||
<UserAvatar
|
||||
:id="friend.friendUserId"
|
||||
@@ -160,7 +185,9 @@ function handleGroupCreated(groupId: number) {
|
||||
:name="friend.nickname"
|
||||
:size="50"
|
||||
/>
|
||||
<div class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--ant-color-text)] text-center truncate">
|
||||
<div
|
||||
class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--ant-color-text)] text-center truncate"
|
||||
>
|
||||
{{ displayName }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,10 +198,16 @@ function handleGroupCreated(groupId: number) {
|
||||
title="发起群聊"
|
||||
@click="handleOpenCreateGroup"
|
||||
>
|
||||
<div class="im-conversation-private-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--ant-color-text)] bg-[var(--ant-color-fill-tertiary)] border border-dashed border-[var(--ant-color-border)] rounded-md transition-colors duration-200">
|
||||
<div
|
||||
class="im-conversation-private-side__icon-tile flex items-center justify-center w-[50px] h-[50px] text-20px text-[var(--ant-color-text)] bg-[var(--ant-color-fill-tertiary)] border border-dashed border-[var(--ant-color-border)] rounded-md transition-colors duration-200"
|
||||
>
|
||||
<Icon icon="ant-design:plus-outlined" />
|
||||
</div>
|
||||
<div class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--ant-color-text)] text-center truncate">添加</div>
|
||||
<div
|
||||
class="w-full mt-1.5 overflow-hidden text-12px leading-[1.5] text-[var(--ant-color-text)] text-center truncate"
|
||||
>
|
||||
添加
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -192,14 +225,21 @@ function handleGroupCreated(groupId: number) {
|
||||
<div
|
||||
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(--ant-color-fill-tertiary)]"
|
||||
>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">备注</span>
|
||||
<span
|
||||
class="flex-shrink-0 text-14px text-[var(--ant-color-text)]"
|
||||
>
|
||||
备注
|
||||
</span>
|
||||
<span
|
||||
v-if="friend.displayName"
|
||||
class="text-13px leading-[1.6] text-[var(--ant-color-text)] break-all line-clamp-2"
|
||||
>
|
||||
{{ friend.displayName }}
|
||||
</span>
|
||||
<span v-else class="text-13px leading-[1.6] text-[var(--ant-color-text-placeholder)]">
|
||||
<span
|
||||
v-else
|
||||
class="text-13px leading-[1.6] text-[var(--ant-color-text-placeholder)]"
|
||||
>
|
||||
好友备注仅自己可见
|
||||
</span>
|
||||
</div>
|
||||
@@ -212,8 +252,17 @@ function handleGroupCreated(groupId: number) {
|
||||
placeholder="请输入备注名"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ElButton size="small" @click="displayNamePopoverVisible = false">取消</ElButton>
|
||||
<ElButton size="small" type="primary" @click="handleSaveDisplayName">
|
||||
<ElButton
|
||||
size="small"
|
||||
@click="displayNamePopoverVisible = false"
|
||||
>
|
||||
取消
|
||||
</ElButton>
|
||||
<ElButton
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleSaveDisplayName"
|
||||
>
|
||||
保存
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -229,7 +278,9 @@ function handleGroupCreated(groupId: number) {
|
||||
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(--ant-color-fill-tertiary)]"
|
||||
@click="emit('openHistory')"
|
||||
>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">查找聊天内容</span>
|
||||
<span class="flex-shrink-0 text-14px text-[var(--ant-color-text)]">
|
||||
查找聊天内容
|
||||
</span>
|
||||
<Icon
|
||||
icon="ant-design:right-outlined"
|
||||
:size="11"
|
||||
@@ -242,34 +293,52 @@ function handleGroupCreated(groupId: number) {
|
||||
|
||||
<!-- 开关项 -->
|
||||
<div class="bg-[var(--ant-color-bg-container)]">
|
||||
<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(--ant-color-text)]">消息免打扰</span>
|
||||
<ElSwitch :model-value="!!conversation?.silent" @change="handleMutedChange" />
|
||||
<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(--ant-color-text)]">
|
||||
消息免打扰
|
||||
</span>
|
||||
<ElSwitch
|
||||
:model-value="!!conversation?.silent"
|
||||
@change="handleMutedChange"
|
||||
/>
|
||||
</div>
|
||||
<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(--ant-color-text)]">置顶聊天</span>
|
||||
<ElSwitch :model-value="!!conversation?.top" @change="handleTopChange" />
|
||||
<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(--ant-color-text)]">
|
||||
置顶聊天
|
||||
</span>
|
||||
<ElSwitch
|
||||
:model-value="!!conversation?.top"
|
||||
@change="handleTopChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子对话框:发起群聊(锁定对方为已选) -->
|
||||
<GroupCreateDialog ref="createGroupDialogRef" @created="handleGroupCreated" />
|
||||
<GroupCreateDialog
|
||||
ref="createGroupDialogRef"
|
||||
@created="handleGroupCreated"
|
||||
/>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 「+」 tile: hover 时联动内部 icon-tile 走主色; 跨子元素的 hover 联动无法用单元素工具类表达 */
|
||||
.im-conversation-private-side__tile-wrap-clickable:hover .im-conversation-private-side__icon-tile {
|
||||
.im-conversation-private-side__tile-wrap-clickable:hover
|
||||
.im-conversation-private-side__icon-tile {
|
||||
color: var(--ant-color-primary);
|
||||
border-color: var(--ant-color-primary);
|
||||
background-color: var(--ant-color-primary-bg);
|
||||
border-color: var(--ant-color-primary);
|
||||
}
|
||||
|
||||
/* :deep 穿透 Icon 内部 svg; el-icon 全局 color 在暗色模式下被主题盖过,锁 fill 到当前色 */
|
||||
.im-conversation-private-side__icon-tile :deep(svg) {
|
||||
fill: currentColor !important;
|
||||
fill: currentcolor !important;
|
||||
}
|
||||
|
||||
/* 相邻信息行加分隔线; 相邻兄弟选择器无法用工具类表达 */
|
||||
|
||||
Reference in New Issue
Block a user