Files
Starlight_Launcher/apps/app-frontend/src/components/ui/skin-site/SkinSiteAdminView.vue
T
AxTps ef1435bd31
Sync source to CNB / Sync Git ref (push) Skipped
feat: 新增皮肤站管理台聊天室房间管理
- 房间列表:头像、名称、可见性、类型、语音标记、前置权限、创建时间
- 创建/编辑房间:名称、简介、公开或私有、普通或聚落、前置条件判定与所需权限
- 房间头像:上传、更换、移除,支持 PNG/JPEG/WebP,5 MB 限制
- 删除房间带二次确认
- 新增 RoomAvatar 与 RoomAvatarEditor 组件,头像按附件 id 缓存 Blob URL
- 补充聊天室相关 API:房间列表、创建、更新、删除、头像更新、附件上传、头像获取
- 管理台导航接入聊天室入口
- 中英文各补 37 个文案
2026-10-18 12:13:34 +08:00

195 lines
7.3 KiB
Vue

<script setup lang="ts">
import { defineMessages, useVIntl } from '@modrinth/ui'
import { computed, ref } from 'vue'
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'
import AdminModsPage from './admin/AdminModsPage.vue'
import AdminPlayersPage from './admin/AdminPlayersPage.vue'
import AdminReviewPage from './admin/AdminReviewPage.vue'
import AdminServersPage from './admin/AdminServersPage.vue'
import AdminSummaryPage from './admin/AdminSummaryPage.vue'
import AdminUsersPage from './admin/AdminUsersPage.vue'
import AdminWikiPage from './admin/AdminWikiPage.vue'
const emit = defineEmits<{ (e: 'back'): void }>()
const { formatMessage } = useVIntl()
const messages = defineMessages({
title: { id: 'app.skin-site.admin.title', defaultMessage: 'Admin console' },
back: { id: 'app.skin-site.admin.back', defaultMessage: 'Back to home' },
noAccess: { id: 'app.skin-site.admin.no-access', defaultMessage: 'You do not have access.' },
noAccessHint: {
id: 'app.skin-site.admin.no-access-hint',
defaultMessage: 'Sign in with an administrator account to use this console.',
},
groupSecurity: { id: 'app.skin-site.admin.group.security', defaultMessage: 'Security' },
groupUsers: { id: 'app.skin-site.admin.group.users', defaultMessage: 'Users' },
groupCommunity: { id: 'app.skin-site.admin.group.community', defaultMessage: 'Community' },
groupResources: { id: 'app.skin-site.admin.group.resources', defaultMessage: 'Resources' },
itemServers: { id: 'app.skin-site.admin.item.servers', defaultMessage: 'Servers' },
itemSummary: { id: 'app.skin-site.admin.item.summary', defaultMessage: 'Summary' },
itemAccessAudit: { id: 'app.skin-site.admin.item.access-audit', defaultMessage: 'Access audit' },
itemUsers: { id: 'app.skin-site.admin.item.users', defaultMessage: 'Users' },
itemPlayers: { id: 'app.skin-site.admin.item.players', defaultMessage: 'Players' },
itemReview: { id: 'app.skin-site.admin.item.review', defaultMessage: 'Player review' },
itemBroadcast: { id: 'app.skin-site.admin.item.broadcast', defaultMessage: 'Broadcast' },
itemChat: { id: 'app.skin-site.admin.item.chat', defaultMessage: 'Chat rooms' },
itemWiki: { id: 'app.skin-site.admin.item.wiki', defaultMessage: 'Wiki' },
itemDonations: { id: 'app.skin-site.admin.item.donations', defaultMessage: 'Donations' },
itemMods: { id: 'app.skin-site.admin.item.mods', defaultMessage: 'Mods' },
itemLauncher: { id: 'app.skin-site.admin.item.launcher', defaultMessage: 'Launcher' },
itemDownloads: { id: 'app.skin-site.admin.item.downloads', defaultMessage: 'Downloads' },
})
type AdminKey =
| 'servers'
| 'summary'
| 'access-audit'
| 'users'
| 'players'
| 'review'
| 'broadcast'
| 'chat'
| 'wiki'
| 'donations'
| 'mods'
| 'launcher'
| 'downloads'
interface NavItem {
key: AdminKey
label: string
}
interface NavGroup {
id: string
label: string
items: NavItem[]
}
const navGroups = computed<NavGroup[]>(() => [
{
id: 'security',
label: formatMessage(messages.groupSecurity),
items: [
...(hasWebAdmin.value
? [{ key: 'servers' as AdminKey, label: formatMessage(messages.itemServers) }]
: []),
{ key: 'summary', label: formatMessage(messages.itemSummary) },
{ key: 'access-audit', label: formatMessage(messages.itemAccessAudit) },
],
},
{
id: 'users',
label: formatMessage(messages.groupUsers),
items: [
{ key: 'users', label: formatMessage(messages.itemUsers) },
{ key: 'players', label: formatMessage(messages.itemPlayers) },
{ key: 'review', label: formatMessage(messages.itemReview) },
],
},
{
id: 'community',
label: formatMessage(messages.groupCommunity),
items: [
{ key: 'broadcast', label: formatMessage(messages.itemBroadcast) },
{ key: 'chat', label: formatMessage(messages.itemChat) },
{ key: 'wiki', label: formatMessage(messages.itemWiki) },
...(hasWebAdmin.value
? [{ key: 'donations' as AdminKey, label: formatMessage(messages.itemDonations) }]
: []),
],
},
{
id: 'resources',
label: formatMessage(messages.groupResources),
items: [
{ key: 'mods', label: formatMessage(messages.itemMods) },
{ key: 'launcher', label: formatMessage(messages.itemLauncher) },
{ key: 'downloads', label: formatMessage(messages.itemDownloads) },
],
},
])
const hasAdminAccess = computed(() => {
const permissions = skinSiteUser.value?.permissions ?? []
return permissions.includes('WEB_ADMIN') || permissions.includes('SERVER_ADMIN')
})
const hasWebAdmin = computed(() => {
const permissions = skinSiteUser.value?.permissions ?? []
return permissions.includes('WEB_ADMIN')
})
const current = ref<AdminKey>('summary')
</script>
<template>
<div class="flex h-full min-h-0 flex-col gap-4">
<button
type="button"
class="flex w-fit items-center gap-1 rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@click="emit('back')"
>
← {{ formatMessage(messages.back) }}
</button>
<div
v-if="!hasAdminAccess"
class="flex flex-col gap-1 rounded-xl border border-solid border-surface-5 bg-bg-raised p-6"
>
<h2 class="m-0 text-lg font-bold text-contrast">{{ formatMessage(messages.noAccess) }}</h2>
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.noAccessHint) }}</p>
</div>
<div v-else class="flex min-h-0 flex-1 gap-4">
<nav class="flex w-48 shrink-0 flex-col gap-3 overflow-y-auto">
<div v-for="group in navGroups" :key="group.id" class="flex flex-col gap-1">
<span class="px-2 text-[10px] font-bold tracking-wide text-secondary uppercase">
{{ group.label }}
</span>
<button
v-for="item in group.items"
:key="item.key"
type="button"
class="rounded-lg px-2 py-1.5 text-left text-xs font-semibold transition-colors"
:class="
current === item.key
? 'bg-brand text-contrast'
: 'text-secondary hover:bg-button-bg hover:text-contrast'
"
@click="current = item.key"
>
{{ item.label }}
</button>
</div>
</nav>
<section
class="min-w-0 flex-1 overflow-y-auto rounded-xl border border-solid border-surface-5 bg-bg-raised p-4"
>
<AdminSummaryPage v-if="current === 'summary'" />
<AdminServersPage v-else-if="current === 'servers' && hasWebAdmin" />
<AdminAccessAuditPage v-else-if="current === 'access-audit'" />
<AdminBroadcastPage v-else-if="current === 'broadcast'" />
<AdminChatPage v-else-if="current === 'chat'" />
<AdminUsersPage v-else-if="current === 'users'" />
<AdminPlayersPage v-else-if="current === 'players'" />
<AdminReviewPage v-else-if="current === 'review'" />
<AdminLauncherPage v-else-if="current === 'launcher'" />
<AdminModsPage v-else-if="current === 'mods'" />
<AdminWikiPage v-else-if="current === 'wiki'" />
<AdminDownloadsPage v-else-if="current === 'downloads'" />
<AdminDonationsPage v-else-if="current === 'donations' && hasWebAdmin" />
<p v-else class="m-0 text-sm text-secondary">{{ current }}</p>
</section>
</div>
</div>
</template>