From ac38c98203635f3412c021c295f5924f4065f3d3 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Sun, 18 Oct 2026 12:20:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=81=8A=E5=A4=A9=E5=AE=A4=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E8=A1=A5=E6=88=BF=E7=AE=A1=E4=BB=BB=E5=91=BD=E3=80=81?= =?UTF-8?q?=E8=81=9A=E8=90=BD=E4=B8=BB=E4=B8=8E=E5=AE=A1=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ChatUserPicker 用户选择器:支持按用户名/邮箱/账号 ID 或玩家名称搜索,封禁与游客不可选,支持排除与白名单 - 房管任命与撤销 - 聚落房间可任命聚落主,展示当前聚落主 - 房间审计记录查看,操作类型本地化映射 - 补充对应 API 与类型定义 - i18n 白名单加入 ID 缩写 - 中英文各补 31 个文案 --- .../ui/skin-site/ChatUserPicker.vue | 290 ++++++++++++++ .../ui/skin-site/admin/AdminChatPage.vue | 358 ++++++++++++++++++ .../app-frontend/src/helpers/skin-site-api.ts | 59 +++ .../app-frontend/src/locales/en-US/index.json | 33 +- .../app-frontend/src/locales/zh-CN/index.json | 33 +- scripts/axolotl/i18n-check.mjs | 1 + 6 files changed, 772 insertions(+), 2 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/skin-site/ChatUserPicker.vue diff --git a/apps/app-frontend/src/components/ui/skin-site/ChatUserPicker.vue b/apps/app-frontend/src/components/ui/skin-site/ChatUserPicker.vue new file mode 100644 index 0000000..ad24d06 --- /dev/null +++ b/apps/app-frontend/src/components/ui/skin-site/ChatUserPicker.vue @@ -0,0 +1,290 @@ + + + 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 index 2ba8467..3293c2e 100644 --- a/apps/app-frontend/src/components/ui/skin-site/admin/AdminChatPage.vue +++ b/apps/app-frontend/src/components/ui/skin-site/admin/AdminChatPage.vue @@ -12,8 +12,15 @@ import { skinSiteChatRoomDelete, skinSiteChatRoomList, skinSiteChatRoomUpdate, + skinSiteChatAuditList, + skinSiteChatManagerSet, + skinSiteChatSettlementOwnerAssign, + type SkinSiteChatAuditInfo, + type SkinSiteChatUserInfo, } from '@/helpers/skin-site-api' +import ChatUserPicker from '../ChatUserPicker.vue' + import RoomAvatar from '../RoomAvatar.vue' import RoomAvatarEditor from '../RoomAvatarEditor.vue' @@ -98,6 +105,59 @@ const messages = defineMessages({ id: 'app.skin-site.chat.admin.voice-badge', defaultMessage: 'Voice', }, + managers: { id: 'app.skin-site.chat.admin.managers', defaultMessage: 'Managers' }, + managerTitle: { + id: 'app.skin-site.chat.admin.manager-title', + defaultMessage: 'Room manager', + }, + managerHint: { + id: 'app.skin-site.chat.admin.manager-hint', + defaultMessage: 'Managers can moderate members, messages and files in this room.', + }, + grant: { id: 'app.skin-site.chat.admin.grant', defaultMessage: 'Grant' }, + revoke: { id: 'app.skin-site.chat.admin.revoke', defaultMessage: 'Revoke' }, + managerGranted: { + id: 'app.skin-site.chat.admin.manager-granted', + defaultMessage: 'Manager granted.', + }, + managerRevoked: { + id: 'app.skin-site.chat.admin.manager-revoked', + defaultMessage: 'Manager revoked.', + }, + ownerTitle: { + id: 'app.skin-site.chat.admin.owner-title', + defaultMessage: 'Settlement owner', + }, + ownerHint: { + id: 'app.skin-site.chat.admin.owner-hint', + defaultMessage: 'Assign the first settlement owner for this room.', + }, + ownerAssigned: { + id: 'app.skin-site.chat.admin.owner-assigned', + defaultMessage: 'Settlement owner assigned.', + }, + ownerCurrent: { + id: 'app.skin-site.chat.admin.owner-current', + defaultMessage: 'Current owner: {name}', + }, + ownerNone: { + id: 'app.skin-site.chat.admin.owner-none', + defaultMessage: 'No settlement owner yet.', + }, + audit: { id: 'app.skin-site.chat.admin.audit', defaultMessage: 'Audit' }, + auditTitle: { + id: 'app.skin-site.chat.admin.audit-title', + defaultMessage: 'Room audit', + }, + auditEmpty: { + id: 'app.skin-site.chat.admin.audit-empty', + defaultMessage: 'No audit records yet.', + }, + auditTarget: { + id: 'app.skin-site.chat.admin.audit-target', + defaultMessage: 'Target: {name}', + }, + close: { id: 'app.skin-site.chat.admin.close', defaultMessage: 'Close' }, }) type FormState = { @@ -268,6 +328,138 @@ function formatDateTime(ts?: number | null): string { } } +const managerRoom = ref(null) +const managerUser = ref(null) +const managerGranting = ref(false) + +const ownerRoom = ref(null) +const ownerUser = ref(null) +const ownerSaving = ref(false) + +const auditRoom = ref(null) +const auditItems = ref([]) +const auditLoading = ref(false) + +const AUDIT_LABELS: Record = { + ROOM_CREATED: '创建房间', + ROOM_UPDATED: '修改房间资料', + ROOM_AVATAR_UPDATED: '更新房间头像', + ROOM_AVATAR_CLEARED: '移除房间头像', + MEMBER_JOINED: '成员加入房间', + MEMBER_LEFT: '成员退出房间', + MEMBER_INVITED: '邀请成员', + MEMBER_REMOVED: '踢出成员', + MEMBER_BANNED: '踢出并拉黑', + MEMBER_UNBANNED: '解除成员封禁', + MEMBER_MUTED: '禁言成员', + MEMBER_UNMUTED: '解除成员禁言', + MANAGER_ADDED: '任命房间管理', + MANAGER_REMOVED: '撤销房间管理', + BOT_ADDED: '添加机器人', + BOT_REMOVED: '移除机器人', + MESSAGE_DELETED: '撤回消息', + MESSAGE_RECALLED: '撤回消息', + MESSAGE_PINNED: '设为精华', + MESSAGE_UNPINNED: '取消精华', + ANNOUNCEMENT_UPDATED: '更新房间公告', + ANNOUNCEMENT_CLEARED: '清空房间公告', + FILE_DELETED: '删除房间文件', + VOICE_ROOM_UPDATED: '修改房间语音设置', + VOICE_CAPACITY_UPDATED: '修改房间语音容量', + VOICE_MEMBER_KICKED: '断开成员语音', + VOICE_MEMBER_BANNED: '临时封禁成员语音', + VOICE_MEMBER_UNBANNED: '解除成员语音封禁', + VOICE_SPEAKER_REVOKED: '关闭成员麦克风', + VOICE_OBSERVER_JOINED: 'Web管理开始旁听语音', + VOICE_OBSERVER_LEFT: 'Web管理结束旁听语音', + SETTLEMENT_OWNER_ASSIGNED: '任命聚落主', + SETTLEMENT_OWNER_TRANSFERRED: '转让聚落', + SETTLEMENT_MANAGER_ADDED: '任命聚落管理', + SETTLEMENT_MANAGER_REMOVED: '撤销聚落管理', +} + +function auditLabel(action: string): string { + return AUDIT_LABELS[action] ?? action +} + +function auditActor(name?: string): string { + return name?.trim() || '已删除用户' +} + +function openManager(room: SkinSiteChatRoomInfo) { + managerRoom.value = room + managerUser.value = null +} + +function closeManager() { + managerRoom.value = null + managerUser.value = null +} + +async function grantManager(manager: boolean) { + if (!managerRoom.value || !managerUser.value || managerGranting.value) return + managerGranting.value = true + try { + const res = await skinSiteChatManagerSet(managerRoom.value.id, managerUser.value.uuid, manager) + if (!res.success) throw new Error(res.errorMessage) + addNotification({ + type: 'success', + title: formatMessage(manager ? messages.managerGranted : messages.managerRevoked), + }) + closeManager() + } catch (error) { + handleError(error) + } finally { + managerGranting.value = false + } +} + +function openOwner(room: SkinSiteChatRoomInfo) { + ownerRoom.value = room + ownerUser.value = null +} + +function closeOwner() { + ownerRoom.value = null + ownerUser.value = null +} + +async function assignOwner() { + if (!ownerRoom.value || !ownerUser.value || ownerSaving.value) return + ownerSaving.value = true + try { + const res = await skinSiteChatSettlementOwnerAssign(ownerRoom.value.id, ownerUser.value.uuid) + if (!res.success) throw new Error(res.errorMessage) + addNotification({ type: 'success', title: formatMessage(messages.ownerAssigned) }) + closeOwner() + await loadRooms() + } catch (error) { + handleError(error) + } finally { + ownerSaving.value = false + } +} + +async function openAudit(room: SkinSiteChatRoomInfo) { + auditRoom.value = room + auditItems.value = [] + auditLoading.value = true + try { + const res = await skinSiteChatAuditList(room.id) + if (!res.success) throw new Error(res.errorMessage) + auditItems.value = res.payload ?? [] + } catch (error) { + handleError(error) + } finally { + auditLoading.value = false + } +} + +function closeAudit() { + auditRoom.value = null + auditItems.value = [] +} + onMounted(() => void loadRooms()) @@ -371,6 +563,28 @@ onMounted(() => void loadRooms()) > {{ perm }} + + + + + + + + + +
+
+

+ {{ formatMessage(messages.ownerTitle) }} · {{ ownerRoom.name }} +

+

{{ formatMessage(messages.ownerHint) }}

+

+ {{ + ownerRoom.settlementOwnerName + ? formatMessage(messages.ownerCurrent, { name: ownerRoom.settlementOwnerName }) + : formatMessage(messages.ownerNone) + }} +

+ +
+ + +
+
+
+ +
+
+

+ {{ formatMessage(messages.auditTitle) }} · {{ auditRoom.name }} +

+
+ + {{ formatMessage(messages.loading) }} +
+

+ {{ formatMessage(messages.auditEmpty) }} +

+
+
+ {{ auditLabel(entry.action) }} + {{ auditActor(entry.actorName) }} + + {{ formatMessage(messages.auditTarget, { name: entry.target }) }} + + + {{ formatDateTime(entry.createdAt) }} + +
+
+
+ +
+
+
diff --git a/apps/app-frontend/src/helpers/skin-site-api.ts b/apps/app-frontend/src/helpers/skin-site-api.ts index b25843e..0464a58 100644 --- a/apps/app-frontend/src/helpers/skin-site-api.ts +++ b/apps/app-frontend/src/helpers/skin-site-api.ts @@ -2193,3 +2193,62 @@ export async function skinSiteChatRoomAvatarFetch(roomId: number): Promise> { + const query = new URLSearchParams({ keyword }) + if (roomId !== undefined) query.set('room_id', String(roomId)) + if (searchBy === 'player') query.set('search_by', 'player') + return skinSiteRequest('chat/users/search?' + query.toString()) +} + +export function skinSiteChatManagerSet( + roomId: number, + userUuid: string, + manager: boolean, +): Promise> { + return skinSiteRequest('chat/admin/rooms/' + roomId + '/manager', { + method: 'POST', + body: { userUuid, manager }, + }) +} + +export function skinSiteChatSettlementOwnerAssign( + roomId: number, + userUuid: string, +): Promise> { + return skinSiteRequest('chat/settlements/' + roomId + '/owner', { + method: 'POST', + body: { userUuid }, + }) +} + +export function skinSiteChatAuditList( + roomId: number, +): Promise> { + return skinSiteRequest('chat/rooms/' + roomId + '/audit') +} diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 93a09ab..a485699 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -12286,5 +12286,36 @@ "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"} + "app.skin-site.chat.admin.voice-badge": {"message": "Voice"}, + "app.skin-site.chat.user-picker.label": {"message": "Pick a user"}, + "app.skin-site.chat.user-picker.placeholder": {"message": "Search username, email or account ID"}, + "app.skin-site.chat.user-picker.by-user": {"message": "By user"}, + "app.skin-site.chat.user-picker.by-player": {"message": "By player"}, + "app.skin-site.chat.user-picker.hint": {"message": "Fuzzy search on username and email, or search by account ID."}, + "app.skin-site.chat.user-picker.player-hint": {"message": "Enter a player name to find the bound user."}, + "app.skin-site.chat.user-picker.empty": {"message": "No matching user."}, + "app.skin-site.chat.user-picker.banned": {"message": "This account is banned."}, + "app.skin-site.chat.user-picker.guest": {"message": "Guests cannot become a manager or settlement owner."}, + "app.skin-site.chat.user-picker.already-in": {"message": "This user is already in the room."}, + "app.skin-site.chat.user-picker.not-allowed": {"message": "This user is outside the allowed range."}, + "app.skin-site.chat.user-picker.select": {"message": "Select"}, + "app.skin-site.chat.user-picker.selected": {"message": "Selected"}, + "app.skin-site.chat.user-picker.id": {"message": "ID"}, + "app.skin-site.chat.admin.managers": {"message": "Managers"}, + "app.skin-site.chat.admin.manager-title": {"message": "Room manager"}, + "app.skin-site.chat.admin.manager-hint": {"message": "Managers can moderate members, messages and files in this room."}, + "app.skin-site.chat.admin.grant": {"message": "Grant"}, + "app.skin-site.chat.admin.revoke": {"message": "Revoke"}, + "app.skin-site.chat.admin.manager-granted": {"message": "Manager granted."}, + "app.skin-site.chat.admin.manager-revoked": {"message": "Manager revoked."}, + "app.skin-site.chat.admin.owner-title": {"message": "Settlement owner"}, + "app.skin-site.chat.admin.owner-hint": {"message": "Assign the first settlement owner for this room."}, + "app.skin-site.chat.admin.owner-assigned": {"message": "Settlement owner assigned."}, + "app.skin-site.chat.admin.owner-current": {"message": "Current owner: {name}"}, + "app.skin-site.chat.admin.owner-none": {"message": "No settlement owner yet."}, + "app.skin-site.chat.admin.audit": {"message": "Audit"}, + "app.skin-site.chat.admin.audit-title": {"message": "Room audit"}, + "app.skin-site.chat.admin.audit-empty": {"message": "No audit records yet."}, + "app.skin-site.chat.admin.audit-target": {"message": "Target: {name}"}, + "app.skin-site.chat.admin.close": {"message": "Close"} } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 60caae6..4af0739 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -12117,5 +12117,36 @@ "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": "语音"} + "app.skin-site.chat.admin.voice-badge": {"message": "语音"}, + "app.skin-site.chat.user-picker.label": {"message": "选择用户"}, + "app.skin-site.chat.user-picker.placeholder": {"message": "搜索用户名、邮箱或账号 ID"}, + "app.skin-site.chat.user-picker.by-user": {"message": "按用户信息"}, + "app.skin-site.chat.user-picker.by-player": {"message": "按玩家 ID"}, + "app.skin-site.chat.user-picker.hint": {"message": "支持按用户名、邮箱模糊搜索,也可输入账号 ID 搜索。"}, + "app.skin-site.chat.user-picker.player-hint": {"message": "输入玩家名称,查找绑定该玩家 ID 的用户。"}, + "app.skin-site.chat.user-picker.empty": {"message": "没有找到匹配的用户"}, + "app.skin-site.chat.user-picker.banned": {"message": "该账号已被封禁,不能执行此操作。"}, + "app.skin-site.chat.user-picker.guest": {"message": "游客不能成为房管、聚落主或聚落成员。"}, + "app.skin-site.chat.user-picker.already-in": {"message": "该用户已经在当前房间中。"}, + "app.skin-site.chat.user-picker.not-allowed": {"message": "该用户不在当前允许选择的范围内"}, + "app.skin-site.chat.user-picker.select": {"message": "选择此用户"}, + "app.skin-site.chat.user-picker.selected": {"message": "已选择此用户"}, + "app.skin-site.chat.user-picker.id": {"message": "ID"}, + "app.skin-site.chat.admin.managers": {"message": "房管"}, + "app.skin-site.chat.admin.manager-title": {"message": "任命房管"}, + "app.skin-site.chat.admin.manager-hint": {"message": "房管可以管理该房间的成员、消息与文件。"}, + "app.skin-site.chat.admin.grant": {"message": "任命"}, + "app.skin-site.chat.admin.revoke": {"message": "撤销"}, + "app.skin-site.chat.admin.manager-granted": {"message": "已任命房管。"}, + "app.skin-site.chat.admin.manager-revoked": {"message": "已撤销房管。"}, + "app.skin-site.chat.admin.owner-title": {"message": "聚落主"}, + "app.skin-site.chat.admin.owner-hint": {"message": "为该房间任命首任聚落主。"}, + "app.skin-site.chat.admin.owner-assigned": {"message": "已任命聚落主。"}, + "app.skin-site.chat.admin.owner-current": {"message": "当前聚落主:{name}"}, + "app.skin-site.chat.admin.owner-none": {"message": "尚未设置聚落主。"}, + "app.skin-site.chat.admin.audit": {"message": "审计"}, + "app.skin-site.chat.admin.audit-title": {"message": "房间审计"}, + "app.skin-site.chat.admin.audit-empty": {"message": "暂无审计记录。"}, + "app.skin-site.chat.admin.audit-target": {"message": "对象:{name}"}, + "app.skin-site.chat.admin.close": {"message": "关闭"} } diff --git a/scripts/axolotl/i18n-check.mjs b/scripts/axolotl/i18n-check.mjs index 1482e1e..1f52425 100644 --- a/scripts/axolotl/i18n-check.mjs +++ b/scripts/axolotl/i18n-check.mjs @@ -31,6 +31,7 @@ const allowedUntranslatedMessages = new Set([ 'LiteLoader', 'LogShare.CN', 'UUID', + 'ID', 'QQ', 'IP', 'URL',