feat: 新增皮肤站管理控制台框架
Sync source to CNB / Sync Git ref (push) Skipped

This commit is contained in:
AxTps committed 2026-10-17 22:21:03 +08:00
1 parent a4615c7b73
commit 77258eee9d
6 files changed
+265 -7

No files matched your search

@@ -30,15 +30,23 @@ const messages = defineMessages({
error: { id: 'app.skin-site.home.error', defaultMessage: 'Could not load players.' },
signIn: { id: 'app.skin-site.home.sign-in', defaultMessage: 'Sign in' },
settings: { id: 'app.skin-site.home.settings', defaultMessage: 'Account' },
admin: { id: 'app.skin-site.home.admin', defaultMessage: 'Admin' },
myPlayers: { id: 'app.skin-site.home.my-players', defaultMessage: 'My players' },
})
const emit = defineEmits<{ (e: 'sign-in' | 'open-settings' | 'open-players'): void }>()
const emit = defineEmits<{
(e: 'sign-in' | 'open-settings' | 'open-players' | 'open-admin'): void
}>()
const luck = ref<number | null>(null)
const luckLoading = ref(false)
const signedIn = computed(() => skinSiteStatus.value === 'signed-in' && !!skinSiteUser.value)
const isAdmin = computed(() => {
const permissions = skinSiteUser.value?.permissions ?? []
return permissions.includes('WEB_ADMIN') || permissions.includes('SERVER_ADMIN')
})
const players = computed(() => skinSitePlayers.value)
const playersLoading = computed(
() => skinSitePlayersStatus.value === 'checking' || skinSitePlayersStatus.value === 'idle',
@@ -98,6 +106,14 @@ watch(signedIn, loadLuck)
>
{{ formatMessage(messages.settings) }}
</button>
<button
v-if="isAdmin"
type="button"
class="shrink-0 rounded-full bg-brand px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
@click="emit('open-admin')"
>
{{ formatMessage(messages.admin) }}
</button>
<div
class="ml-auto flex shrink-0 items-center gap-1 rounded-full bg-button-bg px-3 py-1 text-sm"
:title="formatMessage(messages.luck)"
@@ -0,0 +1,161 @@
<script setup lang="ts">
import { defineMessages, useVIntl } from '@modrinth/ui'
import { computed, ref } from 'vue'
import { skinSiteUser } from '@/composables/skin-site-session'
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: [
{ key: 'servers', 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) },
{ key: 'donations', 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 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"
>
<h1 class="m-0 mb-3 text-xl font-bold text-contrast">
{{ formatMessage(messages.title) }} · {{ current }}
</h1>
<!-- Page content is filled in one admin screen at a time. -->
</section>
</div>
</div>
</template>
@@ -4,6 +4,7 @@ import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/u
import { ref } from 'vue'
import SkinSiteAccountCard from './SkinSiteAccountCard.vue'
import SkinSiteAdminView from './SkinSiteAdminView.vue'
import SkinSiteAnnouncements from './SkinSiteAnnouncements.vue'
import SkinSiteBugCreateModal from './SkinSiteBugCreateModal.vue'
import SkinSiteBugDetailView from './SkinSiteBugDetailView.vue'
@@ -35,7 +36,15 @@ const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
const profileModal = ref<InstanceType<typeof SkinSiteProfileModal>>()
const view = ref<
'home' | 'players' | 'downloads' | 'map' | 'review' | 'bugs' | 'bug-detail' | 'bug-notifications'
| 'home'
| 'players'
| 'downloads'
| 'map'
| 'review'
| 'bugs'
| 'bug-detail'
| 'bug-notifications'
| 'admin'
>('home')
const selectedBugId = ref<number | null>(null)
@@ -88,6 +97,7 @@ async function copyAddress() {
@back="view = 'bugs'"
@open="openBug"
/>
<SkinSiteAdminView v-else-if="view === 'admin'" @back="view = 'home'" />
<SkinSiteBugDetailView
v-else-if="view === 'bug-detail' && selectedBugId !== null"
:issue-id="selectedBugId"
@@ -104,6 +114,7 @@ async function copyAddress() {
@sign-in="loginModal?.show()"
@open-settings="profileModal?.show()"
@open-players="view = 'players'"
@open-admin="view = 'admin'"
/>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
@@ -19,7 +19,7 @@ import { fetchImageAsDataUrl, renderSkinHead } from '@/helpers/skin-site-render'
export const SKIN_SITE_ORIGIN = 'https://skin.starlight.cool'
export type SkinSiteUser = { uuid: string; username: string }
export type SkinSiteUser = { uuid: string; username: string; permissions: string[] }
export type SkinSiteStatus = 'checking' | 'signed-out' | 'signed-in' | 'error'
export type SkinSitePlayer = {
uuid: string
@@ -46,7 +46,11 @@ export const skinSitePlayersStatus = readonly(playersStatus)
export const selectedSkinSitePlayerId = readonly(selectedPlayerId)
function toUser(raw: ApiSkinSiteUser): SkinSiteUser {
return { uuid: raw.uuid, username: raw.username }
return {
uuid: raw.uuid,
username: raw.username,
permissions: Array.isArray(raw.permissions) ? raw.permissions : [],
}
}
function clearSessionState() {
@@ -111,7 +115,7 @@ export async function signInSkinSite(payload: {
status.value = 'signed-in'
void requestSkinSitePlayers().catch(() => {})
} else {
user.value = { uuid: '', username: result.payload.username }
user.value = { uuid: '', username: result.payload.username, permissions: [] }
status.value = 'signed-in'
}
}
+34 -1
View File
@@ -11767,5 +11767,38 @@
"app.skin-site.home.subtitle": {"message": "The wardrobe for StarLight Server players."},
"app.skin-site.home.address": {"message": "Skin site address"},
"app.skin-site.home.copied": {"message": "Address copied."},
"app.skin-site.home.copy": {"message": "Copy"}
"app.skin-site.home.copy": {"message": "Copy"},
"app.skin-site.admin.title": {"message": "Admin console"},
"app.skin-site.admin.back": {"message": "Back to home"},
"app.skin-site.admin.no-access": {"message": "You do not have access."},
"app.skin-site.admin.no-access-hint": {"message": "Sign in with an administrator account to use this console."},
"app.skin-site.admin.group.security": {"message": "Security"},
"app.skin-site.admin.group.users": {"message": "Users"},
"app.skin-site.admin.group.community": {"message": "Community"},
"app.skin-site.admin.group.resources": {"message": "Resources"},
"app.skin-site.admin.item.servers": {"message": "Servers"},
"app.skin-site.admin.item.summary": {"message": "Summary"},
"app.skin-site.admin.item.access-audit": {"message": "Access audit"},
"app.skin-site.admin.item.users": {"message": "Users"},
"app.skin-site.admin.item.players": {"message": "Players"},
"app.skin-site.admin.item.review": {"message": "Player review"},
"app.skin-site.admin.item.broadcast": {"message": "Broadcast"},
"app.skin-site.admin.item.chat": {"message": "Chat rooms"},
"app.skin-site.admin.item.wiki": {"message": "Wiki"},
"app.skin-site.admin.item.donations": {"message": "Donations"},
"app.skin-site.admin.item.mods": {"message": "Mods"},
"app.skin-site.admin.item.launcher": {"message": "Launcher"},
"app.skin-site.admin.item.downloads": {"message": "Downloads"},
"app.skin-site.home.admin": {"message": "Admin"},
"app.skin-site.home.settings": {"message": "Account"},
"app.skin-site.home.sign-in": {"message": "Sign in"},
"app.skin-site.home.players": {"message": "Players"},
"app.skin-site.home.my-players": {"message": "My players"},
"app.skin-site.home.no-players": {"message": "No players linked yet."},
"app.skin-site.home.loading": {"message": "Loading…"},
"app.skin-site.home.error": {"message": "Could not load."},
"app.skin-site.home.luck": {"message": "Luck"},
"app.skin-site.home.luck-unavailable": {"message": "Unavailable"},
"app.skin-site.home.signed-out-title": {"message": "Not signed in"},
"app.skin-site.home.signed-out-body": {"message": "Sign in to manage your account and textures."}
}
+34 -1
View File
@@ -11598,5 +11598,38 @@
"app.skin-site.home.subtitle": {"message": "星光服务器玩家的衣柜。"},
"app.skin-site.home.address": {"message": "皮肤站地址"},
"app.skin-site.home.copied": {"message": "地址已复制。"},
"app.skin-site.home.copy": {"message": "复制"}
"app.skin-site.home.copy": {"message": "复制"},
"app.skin-site.admin.title": {"message": "管理控制台"},
"app.skin-site.admin.back": {"message": "返回主页"},
"app.skin-site.admin.no-access": {"message": "你没有访问权限。"},
"app.skin-site.admin.no-access-hint": {"message": "请使用管理员账号登录后使用本控制台。"},
"app.skin-site.admin.group.security": {"message": "系统安全"},
"app.skin-site.admin.group.users": {"message": "用户管理"},
"app.skin-site.admin.group.community": {"message": "社区管理"},
"app.skin-site.admin.group.resources": {"message": "资源分发"},
"app.skin-site.admin.item.servers": {"message": "服务器管理"},
"app.skin-site.admin.item.summary": {"message": "状态概览"},
"app.skin-site.admin.item.access-audit": {"message": "登录与认证审计"},
"app.skin-site.admin.item.users": {"message": "用户信息"},
"app.skin-site.admin.item.players": {"message": "玩家信息"},
"app.skin-site.admin.item.review": {"message": "玩家审核"},
"app.skin-site.admin.item.broadcast": {"message": "公告操作"},
"app.skin-site.admin.item.chat": {"message": "聊天室管理"},
"app.skin-site.admin.item.wiki": {"message": "维基管理"},
"app.skin-site.admin.item.donations": {"message": "捐赠管理"},
"app.skin-site.admin.item.mods": {"message": "Mod 管理"},
"app.skin-site.admin.item.launcher": {"message": "启动器管理"},
"app.skin-site.admin.item.downloads": {"message": "下载管理"},
"app.skin-site.home.admin": {"message": "管理"},
"app.skin-site.home.settings": {"message": "账号"},
"app.skin-site.home.sign-in": {"message": "登录"},
"app.skin-site.home.players": {"message": "角色"},
"app.skin-site.home.my-players": {"message": "我的角色"},
"app.skin-site.home.no-players": {"message": "尚未绑定角色。"},
"app.skin-site.home.loading": {"message": "加载中…"},
"app.skin-site.home.error": {"message": "加载失败。"},
"app.skin-site.home.luck": {"message": "幸运值"},
"app.skin-site.home.luck-unavailable": {"message": "不可用"},
"app.skin-site.home.signed-out-title": {"message": "未登录"},
"app.skin-site.home.signed-out-body": {"message": "登录后可管理你的账号与皮肤。"}
}