From ef1435bd31ba43294b8e410236a83ab6091e2b6c Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Sun, 18 Oct 2026 12:13:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=9A=AE=E8=82=A4?= =?UTF-8?q?=E7=AB=99=E7=AE=A1=E7=90=86=E5=8F=B0=E8=81=8A=E5=A4=A9=E5=AE=A4?= =?UTF-8?q?=E6=88=BF=E9=97=B4=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 房间列表:头像、名称、可见性、类型、语音标记、前置权限、创建时间 - 创建/编辑房间:名称、简介、公开或私有、普通或聚落、前置条件判定与所需权限 - 房间头像:上传、更换、移除,支持 PNG/JPEG/WebP,5 MB 限制 - 删除房间带二次确认 - 新增 RoomAvatar 与 RoomAvatarEditor 组件,头像按附件 id 缓存 Blob URL - 补充聊天室相关 API:房间列表、创建、更新、删除、头像更新、附件上传、头像获取 - 管理台导航接入聊天室入口 - 中英文各补 37 个文案 --- .../components/ui/skin-site/RoomAvatar.vue | 63 +++ .../ui/skin-site/RoomAvatarEditor.vue | 161 ++++++ .../ui/skin-site/SkinSiteAdminView.vue | 2 + .../ui/skin-site/admin/AdminChatPage.vue | 514 ++++++++++++++++++ .../ui/skin-site/room-avatar-cache.ts | 30 + .../app-frontend/src/helpers/skin-site-api.ts | 122 +++++ .../app-frontend/src/locales/en-US/index.json | 39 +- .../app-frontend/src/locales/zh-CN/index.json | 39 +- 8 files changed, 968 insertions(+), 2 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/skin-site/RoomAvatar.vue create mode 100644 apps/app-frontend/src/components/ui/skin-site/RoomAvatarEditor.vue create mode 100644 apps/app-frontend/src/components/ui/skin-site/admin/AdminChatPage.vue create mode 100644 apps/app-frontend/src/components/ui/skin-site/room-avatar-cache.ts diff --git a/apps/app-frontend/src/components/ui/skin-site/RoomAvatar.vue b/apps/app-frontend/src/components/ui/skin-site/RoomAvatar.vue new file mode 100644 index 0000000..c991d57 --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin-site/RoomAvatar.vue @@ -0,0 +1,63 @@ + + + diff --git a/apps/app-frontend/src/components/ui/skin-site/RoomAvatarEditor.vue b/apps/app-frontend/src/components/ui/skin-site/RoomAvatarEditor.vue new file mode 100644 index 0000000..9652dbe --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin-site/RoomAvatarEditor.vue @@ -0,0 +1,161 @@ + + + diff --git a/apps/app-frontend/src/components/ui/skin-site/SkinSiteAdminView.vue b/apps/app-frontend/src/components/ui/skin-site/SkinSiteAdminView.vue index 382374a..76162e3 100644 --- a/apps/app-frontend/src/components/ui/skin-site/SkinSiteAdminView.vue +++ b/apps/app-frontend/src/components/ui/skin-site/SkinSiteAdminView.vue @@ -6,6 +6,7 @@ import { skinSiteUser } from '@/composables/skin-site-session' import AdminAccessAuditPage from './admin/AdminAccessAuditPage.vue' import AdminBroadcastPage from './admin/AdminBroadcastPage.vue' +import AdminChatPage from './admin/AdminChatPage.vue' import AdminDonationsPage from './admin/AdminDonationsPage.vue' import AdminDownloadsPage from './admin/AdminDownloadsPage.vue' import AdminLauncherPage from './admin/AdminLauncherPage.vue' @@ -177,6 +178,7 @@ const current = ref('summary') + diff --git a/apps/app-frontend/src/components/ui/skin-site/admin/AdminChatPage.vue b/apps/app-frontend/src/components/ui/skin-site/admin/AdminChatPage.vue new file mode 100644 index 0000000..2ba8467 --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin-site/admin/AdminChatPage.vue @@ -0,0 +1,514 @@ + + + diff --git a/apps/app-frontend/src/components/ui/skin-site/room-avatar-cache.ts b/apps/app-frontend/src/components/ui/skin-site/room-avatar-cache.ts new file mode 100644 index 0000000..4e0a88c --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin-site/room-avatar-cache.ts @@ -0,0 +1,30 @@ +import { skinSiteChatRoomAvatarFetch } from '@/helpers/skin-site-api' + +const avatarUrls = new Map() +const pending = new Map>() + +export function loadCachedRoomAvatar( + roomId: number, + attachmentId: number, +): Promise { + const cached = avatarUrls.get(attachmentId) + if (cached) return Promise.resolve(cached) + const running = pending.get(attachmentId) + if (running) return running + + const request = skinSiteChatRoomAvatarFetch(roomId).then((blob) => { + pending.delete(attachmentId) + if (!blob) return null + const url = URL.createObjectURL(blob) + avatarUrls.set(attachmentId, url) + return url + }) + pending.set(attachmentId, request) + return request +} + +export function forgetRoomAvatar(attachmentId: number) { + const url = avatarUrls.get(attachmentId) + if (url) URL.revokeObjectURL(url) + avatarUrls.delete(attachmentId) +} diff --git a/apps/app-frontend/src/helpers/skin-site-api.ts b/apps/app-frontend/src/helpers/skin-site-api.ts index 4f3e1ae..b25843e 100644 --- a/apps/app-frontend/src/helpers/skin-site-api.ts +++ b/apps/app-frontend/src/helpers/skin-site-api.ts @@ -2071,3 +2071,125 @@ export function skinSiteUpdateMultiModInfo( body: mods, }) } + +// --------------------------------------------------------------------------- +// Chat +// --------------------------------------------------------------------------- + +export type SkinSiteChatRoomVisibility = 'PUBLIC' | 'PRIVATE' +export type SkinSiteChatRoomKind = 'STANDARD' | 'SETTLEMENT' +export type SkinSiteChatPrerequisiteMode = 'ANY' | 'ALL' + +export interface SkinSiteChatRoomInfo { + id: number + name: string + description: string + visibility: SkinSiteChatRoomVisibility + joined: boolean + invited: boolean + manager: boolean + createdAt: number + kind: SkinSiteChatRoomKind + avatarAttachmentId?: number | null + voiceEnabled: boolean + voiceAvailable: boolean + voiceParticipantCount: number + voicePublisherCount: number + voicePublisherLimit: number + voiceParticipantLimit: number + voiceListenerLimit: number + prerequisiteMode: SkinSiteChatPrerequisiteMode + prerequisitePermissions: string[] + prerequisiteEligible: boolean +} + +export interface SkinSiteChatUploadInfo { + token: string + attachment: { + id: number + kind: 'IMAGE' | 'FILE' + name: string + size: number + } +} + +export interface SkinSiteChatRoomCreateInput { + name: string + description: string + visibility: SkinSiteChatRoomVisibility + kind?: SkinSiteChatRoomKind + prerequisiteMode: SkinSiteChatPrerequisiteMode + prerequisitePermissions: string[] +} + +export interface SkinSiteChatRoomUpdateInput { + name: string + description: string + visibility: SkinSiteChatRoomVisibility + prerequisiteMode: SkinSiteChatPrerequisiteMode + prerequisitePermissions: string[] +} + +export function skinSiteChatRoomList(): Promise> { + return skinSiteRequest('chat/admin/rooms') +} + +export function skinSiteChatRoomCreate( + data: SkinSiteChatRoomCreateInput, +): Promise> { + return skinSiteRequest('chat/admin/rooms', { + method: 'POST', + body: data, + }) +} + +export function skinSiteChatRoomUpdate( + id: number, + data: SkinSiteChatRoomUpdateInput, +): Promise> { + return skinSiteRequest('chat/rooms/' + id, { + method: 'PUT', + body: data, + }) +} + +export function skinSiteChatRoomDelete(id: number): Promise> { + return skinSiteRequest('chat/admin/rooms/' + id, { method: 'DELETE' }) +} + +export function skinSiteChatRoomAvatarUpdate( + roomId: number, + data: { imageToken?: string; removeImage?: boolean }, +): Promise> { + return skinSiteRequest('chat/rooms/' + roomId + '/avatar', { + method: 'PUT', + body: data, + }) +} + +export function skinSiteChatAttachmentUpload( + file: File, +): Promise> { + const form = new FormData() + form.append('file', file, file.name) + return skinSiteRequest('chat/attachments', { + method: 'POST', + form, + }) +} + +export async function skinSiteChatRoomAvatarFetch(roomId: number): Promise { + const url = `${SKIN_SITE_ORIGIN}${REQUEST_TYPE_PATH.starlight}/chat/rooms/${roomId}/avatar` + const headers: Record = { Accept: 'image/*' } + const token = getSkinSiteToken() + if (token) headers['Authorization'] = `Bearer ${token}` + try { + const response = await tauriFetch(url, { method: 'GET', headers }) + if (!response.ok) return null + const type = response.headers.get('content-type') ?? '' + if (type.includes('application/json') || type.includes('text/')) return null + return await response.blob() + } catch { + return null + } +} diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index cc17884..93a09ab 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -12249,5 +12249,42 @@ "app.skin-site.admin.mods.upload-skip": {"message": "Skip"}, "app.skin-site.admin.mods.upload-close": {"message": "Close"}, "app.skin-site.admin.mods.upload-all-done": {"message": "All files processed."}, - "app.skin-site.admin.mods.upload-too-large": {"message": "File too large: {name}"} + "app.skin-site.admin.mods.upload-too-large": {"message": "File too large: {name}"}, + "app.skin-site.chat.avatar.label": {"message": "Room avatar"}, + "app.skin-site.chat.avatar.upload": {"message": "Upload avatar"}, + "app.skin-site.chat.avatar.replace": {"message": "Replace avatar"}, + "app.skin-site.chat.avatar.remove": {"message": "Remove avatar"}, + "app.skin-site.chat.avatar.hint": {"message": "PNG, JPEG or WebP, up to 8192 px per side and 5 MB."}, + "app.skin-site.chat.avatar.bad-format": {"message": "Only PNG, JPEG and WebP are supported."}, + "app.skin-site.chat.avatar.too-large": {"message": "Avatar must not exceed 5 MB."}, + "app.skin-site.chat.avatar.not-image": {"message": "Avatar must be an image."}, + "app.skin-site.chat.admin.title": {"message": "Chat rooms"}, + "app.skin-site.chat.admin.subtitle": {"message": "Create public or private rooms and hand daily moderation to trusted members."}, + "app.skin-site.chat.admin.create-room": {"message": "Create room"}, + "app.skin-site.chat.admin.loading": {"message": "Loading…"}, + "app.skin-site.chat.admin.failed": {"message": "Could not load rooms."}, + "app.skin-site.chat.admin.empty": {"message": "No chat rooms yet."}, + "app.skin-site.chat.admin.edit": {"message": "Edit"}, + "app.skin-site.chat.admin.remove": {"message": "Delete"}, + "app.skin-site.chat.admin.save": {"message": "Save"}, + "app.skin-site.chat.admin.cancel": {"message": "Cancel"}, + "app.skin-site.chat.admin.name": {"message": "Name"}, + "app.skin-site.chat.admin.description": {"message": "Description"}, + "app.skin-site.chat.admin.visibility": {"message": "Visibility"}, + "app.skin-site.chat.admin.visibility-public": {"message": "Public"}, + "app.skin-site.chat.admin.visibility-private": {"message": "Private"}, + "app.skin-site.chat.admin.kind": {"message": "Kind"}, + "app.skin-site.chat.admin.kind-standard": {"message": "Standard"}, + "app.skin-site.chat.admin.kind-settlement": {"message": "Settlement"}, + "app.skin-site.chat.admin.prerequisite-mode": {"message": "Prerequisite mode"}, + "app.skin-site.chat.admin.prerequisite-any": {"message": "Any"}, + "app.skin-site.chat.admin.prerequisite-all": {"message": "All"}, + "app.skin-site.chat.admin.prerequisite-permissions": {"message": "Required permissions"}, + "app.skin-site.chat.admin.prerequisite-hint": {"message": "Comma separated. Leave empty for no restriction."}, + "app.skin-site.chat.admin.created": {"message": "Room created."}, + "app.skin-site.chat.admin.updated": {"message": "Room updated."}, + "app.skin-site.chat.admin.deleted": {"message": "Room deleted."}, + "app.skin-site.chat.admin.confirm-delete": {"message": "Permanently delete room “{name}” and all its messages?"}, + "app.skin-site.chat.admin.manager-badge": {"message": "Manager"}, + "app.skin-site.chat.admin.voice-badge": {"message": "Voice"} } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 13b7baa..60caae6 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -12080,5 +12080,42 @@ "app.skin-site.admin.mods.upload-skip": {"message": "跳过"}, "app.skin-site.admin.mods.upload-close": {"message": "关闭"}, "app.skin-site.admin.mods.upload-all-done": {"message": "全部文件处理完成。"}, - "app.skin-site.admin.mods.upload-too-large": {"message": "文件过大:{name}"} + "app.skin-site.admin.mods.upload-too-large": {"message": "文件过大:{name}"}, + "app.skin-site.chat.avatar.label": {"message": "房间头像"}, + "app.skin-site.chat.avatar.upload": {"message": "上传头像"}, + "app.skin-site.chat.avatar.replace": {"message": "更换头像"}, + "app.skin-site.chat.avatar.remove": {"message": "移除头像"}, + "app.skin-site.chat.avatar.hint": {"message": "支持 PNG、JPEG、WebP,单边不超过 8192 像素,文件不超过 5 MB。"}, + "app.skin-site.chat.avatar.bad-format": {"message": "房间头像仅支持 PNG、JPEG 和 WebP 格式。"}, + "app.skin-site.chat.avatar.too-large": {"message": "房间头像大小不能超过 5 MB。"}, + "app.skin-site.chat.avatar.not-image": {"message": "房间头像必须是图片。"}, + "app.skin-site.chat.admin.title": {"message": "聊天室管理"}, + "app.skin-site.chat.admin.subtitle": {"message": "创建公开或私有房间,并将日常管理权限交给合适的成员。"}, + "app.skin-site.chat.admin.create-room": {"message": "创建房间"}, + "app.skin-site.chat.admin.loading": {"message": "加载中…"}, + "app.skin-site.chat.admin.failed": {"message": "无法加载房间列表。"}, + "app.skin-site.chat.admin.empty": {"message": "还没有聊天室。"}, + "app.skin-site.chat.admin.edit": {"message": "编辑"}, + "app.skin-site.chat.admin.remove": {"message": "删除"}, + "app.skin-site.chat.admin.save": {"message": "保存"}, + "app.skin-site.chat.admin.cancel": {"message": "取消"}, + "app.skin-site.chat.admin.name": {"message": "房间名称"}, + "app.skin-site.chat.admin.description": {"message": "房间简介"}, + "app.skin-site.chat.admin.visibility": {"message": "可见性"}, + "app.skin-site.chat.admin.visibility-public": {"message": "公开"}, + "app.skin-site.chat.admin.visibility-private": {"message": "私有"}, + "app.skin-site.chat.admin.kind": {"message": "房间类型"}, + "app.skin-site.chat.admin.kind-standard": {"message": "普通房间"}, + "app.skin-site.chat.admin.kind-settlement": {"message": "聚落房间"}, + "app.skin-site.chat.admin.prerequisite-mode": {"message": "前置条件判定"}, + "app.skin-site.chat.admin.prerequisite-any": {"message": "满足任一"}, + "app.skin-site.chat.admin.prerequisite-all": {"message": "满足全部"}, + "app.skin-site.chat.admin.prerequisite-permissions": {"message": "所需权限"}, + "app.skin-site.chat.admin.prerequisite-hint": {"message": "以逗号分隔;留空表示无限制。"}, + "app.skin-site.chat.admin.created": {"message": "房间已创建。"}, + "app.skin-site.chat.admin.updated": {"message": "房间资料已更新。"}, + "app.skin-site.chat.admin.deleted": {"message": "房间已删除。"}, + "app.skin-site.chat.admin.confirm-delete": {"message": "确认永久删除房间“{name}”及其中全部消息吗?"}, + "app.skin-site.chat.admin.manager-badge": {"message": "管理"}, + "app.skin-site.chat.admin.voice-badge": {"message": "语音"} }