Files
GERP/src/api/talk.ts
T
TermiNexus 899dfbc8e9 feat(Talk): 用户头像改用 GERP 系统头像,成员面板支持设置群管理员
新增 TalkAvatar 组件:优先取 /Query/GetELTUserAvatar(带 server/admin_verify 作用域参数),加载失败或群聊无对应用户时回退首字母色块;会话列表/消息/成员面板统一改用它,移除本地首字母色块逻辑。

成员面板显示群主/管理员标签与「管理员 x/上限」,群主可对成员设为/取消管理员(上限取 Entry.GroupAdminLimit,与后端一致);改群名入口与操作加权限判定(群主/管理员/Talk 管理员)。
2026-10-13 16:08:24 +08:00

282 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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()}`
}