新增 TalkAvatar 组件:优先取 /Query/GetELTUserAvatar(带 server/admin_verify 作用域参数),加载失败或群聊无对应用户时回退首字母色块;会话列表/消息/成员面板统一改用它,移除本地首字母色块逻辑。 成员面板显示群主/管理员标签与「管理员 x/上限」,群主可对成员设为/取消管理员(上限取 Entry.GroupAdminLimit,与后端一致);改群名入口与操作加权限判定(群主/管理员/Talk 管理员)。
282 lines
9.3 KiB
TypeScript
282 lines
9.3 KiB
TypeScript
/**
|
||
* Talk 企业通讯 · REST 客户端与类型定义
|
||
*
|
||
* 接口前缀 /API/Talk/,响应统一 { code, msg, data },code===0 成功。
|
||
* request.ts 的响应拦截器在成功时自动解包 data,故下列函数返回的即为业务数据。
|
||
*
|
||
* Socket.IO 实时通道见 @/utils/talkSocket(本文件仅提供 SocketToken 获取)。
|
||
*/
|
||
import { get, post, upload } from './request'
|
||
import { getServerScopeParams } from '@/utils/serverScope'
|
||
|
||
/** Talk 接口前缀 */
|
||
const TALK_BASE = '/API/Talk'
|
||
|
||
/** 附件下载基址(与 src/views/assets/Manager.vue 的 ASSET_DOWNLOAD_BASE 保持一致) */
|
||
export const ASSET_DOWNLOAD_BASE = 'https://api.gvsds.com/API/Asset/Download'
|
||
|
||
/** 会话类型:0=单聊 1=群聊 */
|
||
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 {
|
||
/** 公司可创建的群聊上限 */
|
||
GroupCompanyLimit: number
|
||
/** 单用户可创建的群聊上限 */
|
||
GroupUserLimit: number
|
||
/** 单个群聊成员上限 */
|
||
GroupMemberLimit: number
|
||
/** 公司成员总量上限 */
|
||
CompanyMemberLimit: number
|
||
}
|
||
|
||
/** Talk 配额占用 */
|
||
export interface TalkUsage {
|
||
GroupCompanyUsed: number
|
||
GroupUserUsed: number
|
||
CompanyMemberUsed: number
|
||
}
|
||
|
||
/** Entry 接口返回(页面准入与配额) */
|
||
export interface TalkEntry {
|
||
/** 是否已被管理员在应用市场授权 */
|
||
CanUse: boolean
|
||
/** 当前用户是否管理员(用于解散群聊等特权) */
|
||
IsAdmin: boolean
|
||
/** 单个群的管理员数量上限(与后端 manager.MAX_GROUP_ADMINS 一致) */
|
||
GroupAdminLimit: number
|
||
CompanyUUID: string
|
||
Quotas: TalkQuotas
|
||
Usage: TalkUsage
|
||
}
|
||
|
||
/** 成员简要信息 */
|
||
export interface TalkMemberBrief {
|
||
UserUUID: string
|
||
UserName: string
|
||
DisplayName: string
|
||
/** 群内角色:0=成员 1=群主 2=管理员 */
|
||
MemberRole?: number
|
||
}
|
||
|
||
/** 会话项 */
|
||
export interface TalkConversation {
|
||
ConversationUUID: string
|
||
/** 0=单聊 1=群聊 */
|
||
ConversationType: number
|
||
/** 会话标题(单聊可能为空,需由前端取对方名) */
|
||
Title: string
|
||
MemberCount: number
|
||
LastMessageTime: string
|
||
LastMessagePreview: string
|
||
UnreadCount: number
|
||
Members: TalkMemberBrief[]
|
||
/** 当前用户是否为该群群主(前端据此显示解散入口) */
|
||
IsOwner?: boolean
|
||
/** 群主(创建者)UserUUID */
|
||
OwnerUUID?: string
|
||
/** 当前用户是否为该群管理员 */
|
||
IsGroupAdmin?: boolean
|
||
/** 当前用户在该群的角色:0=成员 1=群主 2=管理员 */
|
||
MyRole?: number
|
||
/** 该群当前管理员数量(不含群主) */
|
||
AdminCount?: number
|
||
/** 当前用户是否可管理群信息(改群名等):群主或群管理员 */
|
||
CanManage?: boolean
|
||
}
|
||
|
||
/** 会话列表返回 */
|
||
export interface TalkConversationList {
|
||
List: TalkConversation[]
|
||
Total: number
|
||
}
|
||
|
||
/** 附件信息 */
|
||
export interface TalkAttachment {
|
||
SHA1: string
|
||
Name: string
|
||
Size: number
|
||
Mime: string
|
||
/** 形如 /API/Asset/Download/<sha1>,可直接作为 img src */
|
||
Url: string
|
||
IsImage: boolean
|
||
}
|
||
|
||
/** 消息记录 */
|
||
export interface TalkMessage {
|
||
MessageUUID: string
|
||
ConversationUUID: string
|
||
SenderUUID: string
|
||
SenderName: string
|
||
/** 0=文本 1=图片 2=文件 */
|
||
MsgType: number
|
||
Content: string
|
||
Attachment: TalkAttachment | null
|
||
CreateTime: string
|
||
}
|
||
|
||
/** 消息分页返回(Records 升序,BeforeTime 取更早一页) */
|
||
export interface TalkMessagePage {
|
||
Records: TalkMessage[]
|
||
Total: number
|
||
}
|
||
|
||
/** 发送消息参数 */
|
||
export interface TalkSendPayload {
|
||
ConversationUUID: string
|
||
MsgType: number
|
||
Content?: string
|
||
AttachmentSHA1?: string
|
||
AttachmentName?: string
|
||
AttachmentSize?: number
|
||
AttachmentMime?: string
|
||
}
|
||
|
||
/** 上传返回 */
|
||
export interface TalkUploadResult {
|
||
SHA1: string
|
||
Name: string
|
||
Size: number
|
||
Mime: string
|
||
IsImage: boolean
|
||
Url: string
|
||
}
|
||
|
||
/** SocketToken 返回 */
|
||
export interface TalkSocketToken {
|
||
Token: string
|
||
SocketPath: string
|
||
ExpiresIn: number
|
||
}
|
||
|
||
/** Entry:页面准入与配额。silent=true 时未授权/失败不弹全局提示 */
|
||
export function getTalkEntry(silent = false): Promise<TalkEntry> {
|
||
return get<TalkEntry>(`${TALK_BASE}/Entry`, undefined, silent ? { silent: true } : undefined)
|
||
}
|
||
|
||
/** 会话列表(最后消息时间倒序) */
|
||
export function getTalkConversations(): Promise<TalkConversationList> {
|
||
return get<TalkConversationList>(`${TALK_BASE}/Conversations`)
|
||
}
|
||
|
||
/** 创建会话(Type 0 单聊需恰好 1 人,1 群聊) */
|
||
export function createTalkConversation(data: {
|
||
ConversationType: number
|
||
Title: string
|
||
MemberUUIDs: string[]
|
||
}): Promise<{ ConversationUUID: string; MemberCount: number }> {
|
||
return post<{ ConversationUUID: string; MemberCount: number }>(`${TALK_BASE}/Conversation/Create`, data)
|
||
}
|
||
|
||
/** 邀请成员入群 */
|
||
export function inviteTalkMembers(data: { ConversationUUID: string; MemberUUIDs: string[] }): Promise<void> {
|
||
return post<void>(`${TALK_BASE}/Conversation/Invite`, data)
|
||
}
|
||
|
||
/** 退出群聊 */
|
||
export function leaveTalkConversation(data: { ConversationUUID: string }): Promise<void> {
|
||
return post<void>(`${TALK_BASE}/Conversation/Leave`, data)
|
||
}
|
||
|
||
/** 修改群名(群聊仅群主/管理员) */
|
||
export function renameTalkConversation(data: { ConversationUUID: string; Title: string }): Promise<void> {
|
||
return post<void>(`${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<void> {
|
||
return post<void>(`${TALK_BASE}/Conversation/Dissolve`, data)
|
||
}
|
||
|
||
/** 消息分页(BeforeTime 取更早一页) */
|
||
export function getTalkMessages(params: {
|
||
ConversationUUID: string
|
||
PageIndex?: number
|
||
PageSize?: number
|
||
BeforeTime?: string
|
||
}): Promise<TalkMessagePage> {
|
||
return get<TalkMessagePage>(`${TALK_BASE}/Messages`, params)
|
||
}
|
||
|
||
/** 发送消息 */
|
||
export function sendTalkMessage(payload: TalkSendPayload): Promise<{ Message: TalkMessage }> {
|
||
return post<{ Message: TalkMessage }>(`${TALK_BASE}/Send`, payload)
|
||
}
|
||
|
||
/** 推送已读位点 */
|
||
export function readTalkConversation(data: { ConversationUUID: string; LastReadMessageUUID: string }): Promise<void> {
|
||
return post<void>(`${TALK_BASE}/Read`, data)
|
||
}
|
||
|
||
/** 搜索本公司成员 */
|
||
export function searchTalkMembers(keyword: string): Promise<{ Users: TalkMemberBrief[] }> {
|
||
return get<{ Users: TalkMemberBrief[] }>(`${TALK_BASE}/Members/Search`, { Keyword: keyword })
|
||
}
|
||
|
||
/** 上传附件(multipart,字段名 file,≤10MB) */
|
||
export function uploadTalkAttachment(file: File): Promise<TalkUploadResult> {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
return upload<TalkUploadResult>(`${TALK_BASE}/Upload`, formData)
|
||
}
|
||
|
||
/** 获取 SocketToken(连接实时通道用) */
|
||
export function getTalkSocketToken(): Promise<TalkSocketToken> {
|
||
return get<TalkSocketToken>(`${TALK_BASE}/SocketToken`)
|
||
}
|
||
|
||
/**
|
||
* 将附件解析为可直接用于 <img>/下载 的 URL。
|
||
* 后端返回相对路径 /API/Asset/Download/<sha1>:
|
||
* - 开发环境保留相对路径,经 Vite 代理(/API)直连本地后端;
|
||
* - 生产环境按 assets/Manager.vue 的 ASSET_DOWNLOAD_BASE 绝对化到统一 API 域名。
|
||
*/
|
||
export function resolveTalkAttachmentUrl(attachment: TalkAttachment | null | undefined): string {
|
||
if (!attachment) return ''
|
||
const url = attachment.Url || ''
|
||
if (/^https?:\/\//i.test(url)) return url
|
||
if (attachment.SHA1 && import.meta.env.PROD) return `${ASSET_DOWNLOAD_BASE}/${attachment.SHA1}`
|
||
return url || (attachment.SHA1 ? `${ASSET_DOWNLOAD_BASE}/${attachment.SHA1}` : '')
|
||
}
|
||
|
||
/** 附件下载 URL(追加 download=1 触发浏览器下载) */
|
||
export function resolveTalkAttachmentDownloadUrl(attachment: TalkAttachment | null | undefined): string {
|
||
const base = resolveTalkAttachmentUrl(attachment)
|
||
if (!base) return ''
|
||
return base.includes('?') ? `${base}&download=1` : `${base}?download=1`
|
||
}
|
||
|
||
/**
|
||
* GERP 系统头像 URL —— Talk 的用户头像统一取系统头像
|
||
*
|
||
* /Query/GetELTUserAvatar 由 <img> 直接请求(不走 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()}`
|
||
}
|