diff --git a/src/api/talk.ts b/src/api/talk.ts index 1a662ed..c38a8e3 100644 --- a/src/api/talk.ts +++ b/src/api/talk.ts @@ -7,6 +7,7 @@ * Socket.IO 实时通道见 @/utils/talkSocket(本文件仅提供 SocketToken 获取)。 */ import { get, post, upload } from './request' +import { getServerScopeParams } from '@/utils/serverScope' /** Talk 接口前缀 */ const TALK_BASE = '/API/Talk' @@ -20,6 +21,9 @@ export const TalkConversationType = { Single: 0, Group: 1 } as const /** 消息类型:0=文本 1=图片 2=文件(后端契约未显式约定,按此实现,见汇总说明) */ export const TalkMsgType = { Text: 0, Image: 1, File: 2 } as const +/** 群成员角色:0=成员 1=群主 2=管理员(上限由 Entry.GroupAdminLimit 下发) */ +export const TalkMemberRole = { Member: 0, Owner: 1, Admin: 2 } as const + /** Talk 使用配额上限 */ export interface TalkQuotas { /** 公司可创建的群聊上限 */ @@ -45,6 +49,8 @@ export interface TalkEntry { CanUse: boolean /** 当前用户是否管理员(用于解散群聊等特权) */ IsAdmin: boolean + /** 单个群的管理员数量上限(与后端 manager.MAX_GROUP_ADMINS 一致) */ + GroupAdminLimit: number CompanyUUID: string Quotas: TalkQuotas Usage: TalkUsage @@ -55,6 +61,8 @@ export interface TalkMemberBrief { UserUUID: string UserName: string DisplayName: string + /** 群内角色:0=成员 1=群主 2=管理员 */ + MemberRole?: number } /** 会话项 */ @@ -73,6 +81,14 @@ export interface TalkConversation { IsOwner?: boolean /** 群主(创建者)UserUUID */ OwnerUUID?: string + /** 当前用户是否为该群管理员 */ + IsGroupAdmin?: boolean + /** 当前用户在该群的角色:0=成员 1=群主 2=管理员 */ + MyRole?: number + /** 该群当前管理员数量(不含群主) */ + AdminCount?: number + /** 当前用户是否可管理群信息(改群名等):群主或群管理员 */ + CanManage?: boolean } /** 会话列表返回 */ @@ -168,11 +184,23 @@ export function leaveTalkConversation(data: { ConversationUUID: string }): Promi return post(`${TALK_BASE}/Conversation/Leave`, data) } -/** 修改群名 */ +/** 修改群名(群聊仅群主/管理员) */ export function renameTalkConversation(data: { ConversationUUID: string; Title: string }): Promise { return post(`${TALK_BASE}/Conversation/Rename`, data) } +/** 设置/取消群管理员(Role:0=成员 2=管理员;仅群主或 Talk 管理员,上限见 Entry.GroupAdminLimit) */ +export function setTalkMemberRole(data: { + ConversationUUID: string + UserUUID: string + Role: number +}): Promise<{ UserUUID: string; MemberRole: number; AdminCount: number; MaxAdmins: number }> { + return post<{ UserUUID: string; MemberRole: number; AdminCount: number; MaxAdmins: number }>( + `${TALK_BASE}/Conversation/Member/Role`, + data, + ) +} + /** 解散群聊(群主/管理员) */ export function dissolveTalkConversation(data: { ConversationUUID: string }): Promise { return post(`${TALK_BASE}/Conversation/Dissolve`, data) @@ -235,3 +263,19 @@ export function resolveTalkAttachmentDownloadUrl(attachment: TalkAttachment | nu if (!base) return '' return base.includes('?') ? `${base}&download=1` : `${base}?download=1` } + +/** + * GERP 系统头像 URL —— Talk 的用户头像统一取系统头像 + * + * /Query/GetELTUserAvatar 由 直接请求(不走 axios 拦截器), + * 故手动附加 server / admin_verify 作用域参数,与 UserDetailPanel.vue 一致。 + */ +export function buildTalkAvatarUrl(UserUUID: string): string { + if (!UserUUID) return '' + const params = new URLSearchParams() + params.set('UserUUID', UserUUID) + const scope = getServerScopeParams() + if (scope.server) params.set('server', scope.server) + if (scope.admin_verify) params.set('admin_verify', scope.admin_verify) + return `/Query/GetELTUserAvatar?${params.toString()}` +} diff --git a/src/views/talk/Talk.vue b/src/views/talk/Talk.vue index e9c5028..6081aec 100644 --- a/src/views/talk/Talk.vue +++ b/src/views/talk/Talk.vue @@ -27,11 +27,13 @@ import { leaveTalkConversation, renameTalkConversation, dissolveTalkConversation, + setTalkMemberRole, searchTalkMembers, resolveTalkAttachmentUrl, resolveTalkAttachmentDownloadUrl, TalkConversationType, TalkMsgType, + TalkMemberRole, type TalkEntry, type TalkConversation, type TalkMessage, @@ -44,6 +46,7 @@ import { emitTalkEvent, } from '@/utils/talkSocket' import GroupCreateDialog from './GroupCreateDialog.vue' +import TalkAvatar from './TalkAvatar.vue' const route = useRoute() const userStore = useUserStore() @@ -125,6 +128,22 @@ const CanDissolve = computed( () => Entry.value?.IsAdmin === true || ActiveConversation.value?.IsOwner === true, ) +/** 是否有权修改群信息(群名等):Talk 管理员,或该群群主/群管理员(后端 CanManage) */ +const CanManageGroup = computed( + () => Entry.value?.IsAdmin === true || ActiveConversation.value?.CanManage === true, +) + +/** 是否有权设置/取消群管理员:仅群主或 Talk 管理员(与后端一致) */ +const CanManageMembers = computed( + () => Entry.value?.IsAdmin === true || ActiveConversation.value?.IsOwner === true, +) + +/** 单群管理员上限(后端 Entry.GroupAdminLimit 下发,兜底 10) */ +const AdminLimit = computed(() => Entry.value?.GroupAdminLimit || 10) + +/** 该群当前管理员数量 */ +const AdminCount = computed(() => ActiveConversation.value?.AdminCount || 0) + /** 过滤后的会话列表 */ const FilteredConversations = computed(() => { const keyword = ConversationKeyword.value.trim().toLowerCase() @@ -132,6 +151,18 @@ const FilteredConversations = computed(() => { return Conversations.value.filter((c) => ConversationTitle(c).toLowerCase().includes(keyword)) }) +/** 群成员排序:群主 → 管理员 → 成员(同级保持后端顺序) */ +const SortedMembers = computed(() => { + const rank = (role?: number): number => { + if (role === TalkMemberRole.Owner) return 0 + if (role === TalkMemberRole.Admin) return 1 + return 2 + } + return (ActiveConversation.value?.Members || []) + .slice() + .sort((a, b) => rank(a.MemberRole) - rank(b.MemberRole)) +}) + /** 正在输入提示文案 */ const TypingText = computed(() => { const names = Object.values(TypingUsers.value) @@ -154,19 +185,11 @@ function ConversationTitle(conversation: TalkConversation): string { return other ? MemberName(other) : '会话' } -/** 头像首字母 */ -function AvatarText(name: string): string { - const text = (name || '').trim() - return text ? text.slice(0, 1).toUpperCase() : '?' -} - -/** 头像底色:由 UUID 稳定散列到克制色板 */ -function AvatarColor(seed: string): string { - const palette = ['#2563eb', '#0891b2', '#16a34a', '#ca8a04', '#dc2626', '#7c3aed', '#db2777', '#475569'] - let hash = 0 - const text = seed || '' - for (let i = 0; i < text.length; i++) hash = (hash * 31 + text.charCodeAt(i)) >>> 0 - return palette[hash % palette.length] +/** 会话头像对应的用户:单聊取对方(用其 GERP 系统头像),群聊返回空(用群名首字色块) */ +function ConversationAvatarUUID(conversation: TalkConversation): string { + if (conversation.ConversationType === TalkConversationType.Group) return '' + const other = conversation.Members?.find((m) => m.UserUUID !== MyUUID.value) + return other?.UserUUID || '' } /** 时间字符串 → 毫秒(无效返回 0) */ @@ -528,9 +551,36 @@ function HandleGroupCommand(command: string): void { else if (command === 'dissolve') void DissolveGroup() } +/** 成员操作:设为/取消群管理员(仅群主或 Talk 管理员,单群上限由后端下发) */ +async function HandleMemberCommand(command: string, member: TalkMemberBrief): Promise { + const conversation = ActiveConversation.value + if (!conversation) return + const role = command === 'setAdmin' ? TalkMemberRole.Admin : TalkMemberRole.Member + if (role === TalkMemberRole.Admin && AdminCount.value >= AdminLimit.value) { + ElMessage.warning(`每个群最多设置 ${AdminLimit.value} 个管理员`) + return + } + try { + await setTalkMemberRole({ + ConversationUUID: conversation.ConversationUUID, + UserUUID: member.UserUUID, + Role: role, + }) + ElMessage.success(role === TalkMemberRole.Admin ? '已设为管理员' : '已取消管理员') + // 重新拉取会话,同步成员角色与管理员数量 + await LoadConversations() + } catch { + // 业务错误已由响应拦截器统一提示 + } +} + /** 打开改群名 */ function OpenRename(): void { if (!ActiveConversation.value) return + if (!CanManageGroup.value) { + ElMessage.warning('仅群主或管理员可修改群名') + return + } RenameTitle.value = ActiveConversation.value.Title || '' RenameVisible.value = true } @@ -805,9 +855,11 @@ onBeforeUnmount(() => { :class="{ active: conversation.ConversationUUID === ActiveUUID }" @click="OpenConversation(conversation)" > -
- {{ AvatarText(ConversationTitle(conversation)) }} -
+
{{ ConversationTitle(conversation) }} @@ -844,7 +896,7 @@ onBeforeUnmount(() => {