feat: 聊天室管理补房管任命、聚落主与审计
Sync source to CNB / Sync Git ref (push) Skipped

- 新增 ChatUserPicker 用户选择器:支持按用户名/邮箱/账号 ID 或玩家名称搜索,封禁与游客不可选,支持排除与白名单
- 房管任命与撤销
- 聚落房间可任命聚落主,展示当前聚落主
- 房间审计记录查看,操作类型本地化映射
- 补充对应 API 与类型定义
- i18n 白名单加入 ID 缩写
- 中英文各补 31 个文案
This commit is contained in:
AxTps committed 2026-10-18 12:20:45 +08:00
1 parent ef1435bd31
commit ac38c98203
6 files changed
+772 -2

No files matched your search

@@ -0,0 +1,290 @@
<script setup lang="ts">
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { type SkinSiteChatUserInfo, skinSiteChatUserSearch } from '@/helpers/skin-site-api'
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const props = withDefaults(
defineProps<{
roomId?: number
label?: string
placeholder?: string
excludeUuids?: string[]
allowedUuids?: string[]
allowedHint?: string
}>(),
{
roomId: undefined,
label: undefined,
placeholder: undefined,
excludeUuids: () => [],
allowedUuids: undefined,
allowedHint: undefined,
},
)
const model = defineModel<SkinSiteChatUserInfo | null>({ required: true })
const messages = defineMessages({
label: { id: 'app.skin-site.chat.user-picker.label', defaultMessage: 'Pick a user' },
placeholder: {
id: 'app.skin-site.chat.user-picker.placeholder',
defaultMessage: 'Search username, email or account ID',
},
byUser: { id: 'app.skin-site.chat.user-picker.by-user', defaultMessage: 'By user' },
byPlayer: { id: 'app.skin-site.chat.user-picker.by-player', defaultMessage: 'By player' },
hint: {
id: 'app.skin-site.chat.user-picker.hint',
defaultMessage: 'Fuzzy search on username and email, or search by account ID.',
},
playerHint: {
id: 'app.skin-site.chat.user-picker.player-hint',
defaultMessage: 'Enter a player name to find the bound user.',
},
empty: {
id: 'app.skin-site.chat.user-picker.empty',
defaultMessage: 'No matching user.',
},
banned: {
id: 'app.skin-site.chat.user-picker.banned',
defaultMessage: 'This account is banned.',
},
guest: {
id: 'app.skin-site.chat.user-picker.guest',
defaultMessage: 'Guests cannot become a manager or settlement owner.',
},
alreadyIn: {
id: 'app.skin-site.chat.user-picker.already-in',
defaultMessage: 'This user is already in the room.',
},
notAllowed: {
id: 'app.skin-site.chat.user-picker.not-allowed',
defaultMessage: 'This user is outside the allowed range.',
},
select: { id: 'app.skin-site.chat.user-picker.select', defaultMessage: 'Select' },
selected: {
id: 'app.skin-site.chat.user-picker.selected',
defaultMessage: 'Selected',
},
idLabel: { id: 'app.skin-site.chat.user-picker.id', defaultMessage: 'ID' },
})
const keyword = ref('')
const searchMode = ref<'user' | 'player'>('user')
const results = ref<SkinSiteChatUserInfo[]>([])
const preview = ref<SkinSiteChatUserInfo | null>(null)
const loading = ref(false)
let timer: ReturnType<typeof setTimeout> | undefined
let requestId = 0
const currentLabel = computed(() => props.label ?? formatMessage(messages.label))
const currentPlaceholder = computed(
() => props.placeholder ?? formatMessage(messages.placeholder),
)
watch([keyword, searchMode], () => {
clearTimeout(timer)
const query = keyword.value.trim()
if (!query) {
results.value = []
preview.value = null
loading.value = false
return
}
timer = setTimeout(() => void search(query), 300)
})
watch(model, (value) => {
if (value) return
keyword.value = ''
preview.value = null
results.value = []
})
watch(
() => props.roomId,
() => {
model.value = null
},
)
onBeforeUnmount(() => clearTimeout(timer))
async function search(query: string) {
const current = ++requestId
loading.value = true
try {
const res = await skinSiteChatUserSearch(query, props.roomId, searchMode.value)
if (current !== requestId) return
if (!res.success) throw new Error(res.errorMessage)
results.value = res.payload ?? []
preview.value = results.value.length === 1 ? results.value[0] : null
} catch (error) {
if (current === requestId) {
results.value = []
handleError(error)
}
} finally {
if (current === requestId) loading.value = false
}
}
function unavailableReason(user: SkinSiteChatUserInfo): string | null {
if (user.banned) return formatMessage(messages.banned)
if (user.permissions.includes('GUEST')) return formatMessage(messages.guest)
if (props.excludeUuids.includes(user.uuid)) return formatMessage(messages.alreadyIn)
if (props.allowedUuids && !props.allowedUuids.includes(user.uuid)) {
return props.allowedHint ?? formatMessage(messages.notAllowed)
}
return null
}
function selectPreview() {
if (preview.value && !unavailableReason(preview.value)) {
model.value = preview.value
}
}
function formatTime(ts: number): string {
try {
return new Date(ts).toLocaleString()
} catch {
return '—'
}
}
</script>
<template>
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-1.5">
<span class="text-xs font-semibold text-secondary">{{ currentLabel }}</span>
<div class="flex gap-2">
<select
v-model="searchMode"
class="shrink-0 rounded-lg border border-solid border-surface-5 bg-bg-raised px-2 py-1.5 text-xs text-contrast outline-none focus:border-brand"
>
<option value="user">{{ formatMessage(messages.byUser) }}</option>
<option value="player">{{ formatMessage(messages.byPlayer) }}</option>
</select>
<input
v-model.trim="keyword"
type="text"
maxlength="100"
:placeholder="searchMode === 'player' ? formatMessage(messages.playerHint) : currentPlaceholder"
class="min-w-0 flex-1 rounded-lg border border-solid border-surface-5 bg-bg-raised px-3 py-1.5 text-xs text-contrast outline-none focus:border-brand"
autocomplete="off"
/>
</div>
<span class="text-[11px] text-secondary">
{{ searchMode === 'player' ? formatMessage(messages.playerHint) : formatMessage(messages.hint) }}
</span>
</div>
<div
v-if="keyword && !loading"
class="flex max-h-56 flex-col gap-1.5 overflow-y-auto rounded-lg border border-solid border-surface-5 bg-bg-raised p-2"
>
<button
v-for="user in results"
:key="user.uuid"
type="button"
class="flex w-full items-center justify-between gap-3 rounded-lg border border-solid p-2 text-left transition-colors"
:class="
preview?.uuid === user.uuid
? 'border-brand bg-brand/10'
: 'border-transparent bg-bg hover:border-surface-5'
"
@click="preview = user"
>
<span class="flex min-w-0 items-center gap-2">
<span class="grid size-8 shrink-0 place-items-center rounded-lg bg-brand/15 text-xs font-bold text-brand">
{{ user.username.slice(0, 1).toUpperCase() }}
</span>
<span class="min-w-0">
<strong class="block truncate text-xs text-contrast">{{ user.username }}</strong>
<span class="block truncate text-[10px] text-secondary">
{{ formatMessage(messages.idLabel) }} {{ user.id }} · {{ user.email }}
</span>
</span>
</span>
<span
v-if="unavailableReason(user)"
class="shrink-0 rounded-full bg-red/20 px-2 py-0.5 text-[10px] font-semibold text-red"
>
{{ unavailableReason(user) }}
</span>
<span
v-else-if="model?.uuid === user.uuid"
class="shrink-0 rounded-full bg-brand px-2 py-0.5 text-[10px] font-semibold text-contrast"
>
{{ formatMessage(messages.selected) }}
</span>
</button>
<p v-if="!results.length" class="m-0 py-4 text-center text-xs text-secondary">
{{ formatMessage(messages.empty) }}
</p>
</div>
<section
v-if="preview"
class="flex flex-col gap-3 rounded-lg border border-solid border-surface-5 bg-bg-raised p-3"
>
<div class="flex items-start justify-between gap-3">
<div>
<h3 class="m-0 text-sm font-bold text-contrast">{{ preview.username }}</h3>
<p class="m-0 text-[11px] text-secondary">
{{ formatMessage(messages.idLabel) }} {{ preview.id }}
</p>
</div>
<span
class="rounded-full px-2 py-0.5 text-[10px] font-semibold"
:class="preview.banned ? 'bg-red text-contrast' : 'bg-green text-contrast'"
>
{{ preview.banned ? 'Banned' : 'OK' }}
</span>
</div>
<div class="grid gap-2 text-[11px] sm:grid-cols-2">
<div class="min-w-0">
<span class="block text-secondary">UUID</span>
<span class="block break-all font-mono text-contrast">{{ preview.uuid }}</span>
</div>
<div>
<span class="block text-secondary">Email</span>
<span class="block break-all text-contrast">{{ preview.email }}</span>
</div>
<div>
<span class="block text-secondary">QQ</span>
<span class="block text-contrast">{{ preview.qq ?? '—' }}</span>
</div>
<div>
<span class="block text-secondary">Registered</span>
<span class="block text-contrast">{{ formatTime(preview.registerTime) }}</span>
</div>
</div>
<div class="flex flex-wrap gap-1.5">
<span
v-for="perm in preview.permissions"
:key="perm"
class="rounded bg-bg px-2 py-0.5 font-mono text-[10px] text-secondary"
>
{{ perm }}
</span>
</div>
<p v-if="unavailableReason(preview)" class="m-0 text-[11px] text-red">
{{ unavailableReason(preview) }}
</p>
<button
type="button"
class="w-full rounded-lg bg-brand px-3 py-2 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
:disabled="Boolean(unavailableReason(preview))"
@click="selectPreview"
>
{{ model?.uuid === preview.uuid ? formatMessage(messages.selected) : formatMessage(messages.select) }}
</button>
</section>
</div>
</template>
@@ -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<SkinSiteChatRoomInfo | null>(null)
const managerUser = ref<SkinSiteChatUserInfo | null>(null)
const managerGranting = ref(false)
const ownerRoom = ref<SkinSiteChatRoomInfo | null>(null)
const ownerUser = ref<SkinSiteChatUserInfo | null>(null)
const ownerSaving = ref(false)
const auditRoom = ref<SkinSiteChatRoomInfo | null>(null)
const auditItems = ref<SkinSiteChatAuditInfo[]>([])
const auditLoading = ref(false)
const AUDIT_LABELS: Record<string, string> = {
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())
</script>
@@ -371,6 +563,28 @@ onMounted(() => void loadRooms())
>
{{ perm }}
</span>
<button
v-if="room.kind === 'SETTLEMENT'"
type="button"
class="rounded-lg bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@click="openOwner(room)"
>
{{ formatMessage(messages.ownerTitle) }}
</button>
<button
type="button"
class="rounded-lg bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@click="openManager(room)"
>
{{ formatMessage(messages.managers) }}
</button>
<button
type="button"
class="rounded-lg bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@click="openAudit(room)"
>
{{ formatMessage(messages.audit) }}
</button>
<button
type="button"
class="ml-auto rounded-lg bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@@ -510,5 +724,149 @@ onMounted(() => void loadRooms())
</div>
</div>
</div>
<div
v-if="managerRoom"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="closeManager"
>
<div
class="flex max-h-[88vh] w-full max-w-lg flex-col gap-3 overflow-y-auto rounded-xl bg-bg p-6"
>
<h3 class="m-0 text-lg font-bold text-contrast">
{{ formatMessage(messages.managerTitle) }} · {{ managerRoom.name }}
</h3>
<p class="m-0 text-xs text-secondary">{{ formatMessage(messages.managerHint) }}</p>
<ChatUserPicker
v-model="managerUser"
:room-id="managerRoom.id"
:label="formatMessage(messages.managerTitle)"
/>
<div class="flex justify-end gap-2 border-t border-solid border-surface-5 pt-3">
<button
type="button"
class="rounded-lg bg-button-bg px-4 py-2 text-sm font-semibold text-secondary hover:brightness-110"
@click="closeManager"
>
{{ formatMessage(messages.cancel) }}
</button>
<button
type="button"
class="rounded-lg bg-red/20 px-4 py-2 text-sm font-semibold text-red hover:brightness-110 disabled:opacity-50"
:disabled="!managerUser || managerGranting"
@click="grantManager(false)"
>
{{ formatMessage(messages.revoke) }}
</button>
<button
type="button"
class="rounded-lg bg-brand px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
:disabled="!managerUser || managerGranting"
@click="grantManager(true)"
>
{{ formatMessage(messages.grant) }}
</button>
</div>
</div>
</div>
<div
v-if="ownerRoom"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="closeOwner"
>
<div
class="flex max-h-[88vh] w-full max-w-lg flex-col gap-3 overflow-y-auto rounded-xl bg-bg p-6"
>
<h3 class="m-0 text-lg font-bold text-contrast">
{{ formatMessage(messages.ownerTitle) }} · {{ ownerRoom.name }}
</h3>
<p class="m-0 text-xs text-secondary">{{ formatMessage(messages.ownerHint) }}</p>
<p class="m-0 text-xs text-secondary">
{{
ownerRoom.settlementOwnerName
? formatMessage(messages.ownerCurrent, { name: ownerRoom.settlementOwnerName })
: formatMessage(messages.ownerNone)
}}
</p>
<ChatUserPicker
v-model="ownerUser"
:room-id="ownerRoom.id"
:label="formatMessage(messages.ownerTitle)"
/>
<div class="flex justify-end gap-2 border-t border-solid border-surface-5 pt-3">
<button
type="button"
class="rounded-lg bg-button-bg px-4 py-2 text-sm font-semibold text-secondary hover:brightness-110"
@click="closeOwner"
>
{{ formatMessage(messages.cancel) }}
</button>
<button
type="button"
class="rounded-lg bg-brand px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
:disabled="!ownerUser || ownerSaving"
@click="assignOwner"
>
{{ formatMessage(messages.save) }}
</button>
</div>
</div>
</div>
<div
v-if="auditRoom"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
@click.self="closeAudit"
>
<div
class="flex max-h-[88vh] w-full max-w-2xl flex-col gap-3 overflow-y-auto rounded-xl bg-bg p-6"
>
<h3 class="m-0 text-lg font-bold text-contrast">
{{ formatMessage(messages.auditTitle) }} · {{ auditRoom.name }}
</h3>
<div
v-if="auditLoading"
class="flex min-h-32 items-center justify-center gap-2 text-sm text-secondary"
>
<SpinnerIcon class="size-4 animate-spin" />
{{ formatMessage(messages.loading) }}
</div>
<p
v-else-if="!auditItems.length"
class="m-0 rounded-lg bg-bg-raised p-3 text-xs text-secondary"
>
{{ formatMessage(messages.auditEmpty) }}
</p>
<div v-else class="flex flex-col gap-1.5">
<div
v-for="entry in auditItems"
:key="entry.id"
class="flex flex-wrap items-center gap-2 rounded-lg border border-solid border-surface-5 bg-bg-raised px-3 py-2"
>
<span class="text-xs font-semibold text-contrast">{{ auditLabel(entry.action) }}</span>
<span class="text-[11px] text-secondary">{{ auditActor(entry.actorName) }}</span>
<span
v-if="entry.target"
class="text-[11px] text-secondary"
>
{{ formatMessage(messages.auditTarget, { name: entry.target }) }}
</span>
<span class="ml-auto text-[10px] text-secondary">
{{ formatDateTime(entry.createdAt) }}
</span>
</div>
</div>
<div class="flex justify-end border-t border-solid border-surface-5 pt-3">
<button
type="button"
class="rounded-lg bg-button-bg px-4 py-2 text-sm font-semibold text-secondary hover:brightness-110"
@click="closeAudit"
>
{{ formatMessage(messages.close) }}
</button>
</div>
</div>
</div>
</div>
</template>
@@ -2193,3 +2193,62 @@ export async function skinSiteChatRoomAvatarFetch(roomId: number): Promise<Blob
return null
}
}
export interface SkinSiteChatUserInfo {
uuid: string
id: number
username: string
avatar?: string | null
email: string
qq?: number | null
permissions: string[]
banned: boolean
registerTime: number
}
export interface SkinSiteChatAuditInfo {
id: number
actorUuid: string
actorName?: string
action: string
target?: string | null
createdAt: number
}
export function skinSiteChatUserSearch(
keyword: string,
roomId?: number,
searchBy: 'user' | 'player' = 'user',
): Promise<SkinSiteResponse<SkinSiteChatUserInfo[]>> {
const query = new URLSearchParams({ keyword })
if (roomId !== undefined) query.set('room_id', String(roomId))
if (searchBy === 'player') query.set('search_by', 'player')
return skinSiteRequest<SkinSiteChatUserInfo[]>('chat/users/search?' + query.toString())
}
export function skinSiteChatManagerSet(
roomId: number,
userUuid: string,
manager: boolean,
): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>('chat/admin/rooms/' + roomId + '/manager', {
method: 'POST',
body: { userUuid, manager },
})
}
export function skinSiteChatSettlementOwnerAssign(
roomId: number,
userUuid: string,
): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>('chat/settlements/' + roomId + '/owner', {
method: 'POST',
body: { userUuid },
})
}
export function skinSiteChatAuditList(
roomId: number,
): Promise<SkinSiteResponse<SkinSiteChatAuditInfo[]>> {
return skinSiteRequest<SkinSiteChatAuditInfo[]>('chat/rooms/' + roomId + '/audit')
}
+32 -1
View File
@@ -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"}
}
+32 -1
View File
@@ -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": "关闭"}
}