1 parent
799d542227
commit
947a739672
6 files changed
+1029
-2
No files matched your search
@@ -5,7 +5,9 @@ import { computed, ref } from 'vue'
|
||||
import { skinSiteUser } from '@/composables/skin-site-session'
|
||||
|
||||
import AdminBroadcastPage from './admin/AdminBroadcastPage.vue'
|
||||
import AdminPlayersPage from './admin/AdminPlayersPage.vue'
|
||||
import AdminSummaryPage from './admin/AdminSummaryPage.vue'
|
||||
import AdminUsersPage from './admin/AdminUsersPage.vue'
|
||||
|
||||
const emit = defineEmits<{ (e: 'back'): void }>()
|
||||
|
||||
@@ -156,6 +158,8 @@ const current = ref<AdminKey>('summary')
|
||||
>
|
||||
<AdminSummaryPage v-if="current === 'summary'" />
|
||||
<AdminBroadcastPage v-else-if="current === 'broadcast'" />
|
||||
<AdminUsersPage v-else-if="current === 'users'" />
|
||||
<AdminPlayersPage v-else-if="current === 'players'" />
|
||||
<p v-else class="m-0 text-sm text-secondary">{{ current }}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
<script setup lang="ts">
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
skinSiteAdminDeletePlayer,
|
||||
type SkinSiteAdminPlayer,
|
||||
skinSiteAdminPlayerCount,
|
||||
skinSiteAdminPlayerList,
|
||||
skinSiteAdminUpdatePlayer,
|
||||
} from '@/helpers/skin-site-api'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError, addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'app.skin-site.admin.players.title', defaultMessage: 'Players' },
|
||||
loading: { id: 'app.skin-site.admin.players.loading', defaultMessage: 'Loading…' },
|
||||
failed: { id: 'app.skin-site.admin.players.failed', defaultMessage: 'Could not load players.' },
|
||||
empty: { id: 'app.skin-site.admin.players.empty', defaultMessage: 'No players found.' },
|
||||
search: { id: 'app.skin-site.admin.players.search', defaultMessage: 'Search' },
|
||||
searchName: { id: 'app.skin-site.admin.players.search-name', defaultMessage: 'Name' },
|
||||
searchOwner: { id: 'app.skin-site.admin.players.search-owner', defaultMessage: 'Owner' },
|
||||
searchUuid: { id: 'app.skin-site.admin.players.search-uuid', defaultMessage: 'UUID' },
|
||||
total: { id: 'app.skin-site.admin.players.total', defaultMessage: '{count} players' },
|
||||
prev: { id: 'app.skin-site.admin.players.prev', defaultMessage: 'Previous' },
|
||||
next: { id: 'app.skin-site.admin.players.next', defaultMessage: 'Next' },
|
||||
page: { id: 'app.skin-site.admin.players.page', defaultMessage: 'Page {page}' },
|
||||
name: { id: 'app.skin-site.admin.players.name', defaultMessage: 'Name' },
|
||||
owner: { id: 'app.skin-site.admin.players.owner', defaultMessage: 'Owner' },
|
||||
source: { id: 'app.skin-site.admin.players.source', defaultMessage: 'Source' },
|
||||
legacy: { id: 'app.skin-site.admin.players.legacy', defaultMessage: 'Legacy adapted' },
|
||||
actions: { id: 'app.skin-site.admin.players.actions', defaultMessage: 'Actions' },
|
||||
edit: { id: 'app.skin-site.admin.players.edit', defaultMessage: 'Edit' },
|
||||
save: { id: 'app.skin-site.admin.players.save', defaultMessage: 'Save' },
|
||||
cancel: { id: 'app.skin-site.admin.players.cancel', defaultMessage: 'Cancel' },
|
||||
remove: { id: 'app.skin-site.admin.players.remove', defaultMessage: 'Delete' },
|
||||
updated: { id: 'app.skin-site.admin.players.updated', defaultMessage: 'Player updated.' },
|
||||
deleted: { id: 'app.skin-site.admin.players.deleted', defaultMessage: 'Player deleted.' },
|
||||
confirmDelete: {
|
||||
id: 'app.skin-site.admin.players.confirm-delete',
|
||||
defaultMessage: 'Delete player “{name}”? This cannot be undone.',
|
||||
},
|
||||
mojang: { id: 'app.skin-site.admin.players.mojang', defaultMessage: 'Mojang' },
|
||||
custom: { id: 'app.skin-site.admin.players.custom', defaultMessage: 'Skin site' },
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const items = ref<SkinSiteAdminPlayer[]>([])
|
||||
const total = ref(0)
|
||||
const offset = ref(0)
|
||||
const loading = ref(true)
|
||||
const failed = ref(false)
|
||||
|
||||
const keyword = ref('')
|
||||
const searchField = ref<'name' | 'ownerUsername' | 'uuid'>('name')
|
||||
|
||||
const editingUuid = ref<string | null>(null)
|
||||
const editName = ref('')
|
||||
const editIsMojang = ref(false)
|
||||
const saving = ref(false)
|
||||
const deletingUuid = ref<string | null>(null)
|
||||
|
||||
const page = computed(() => Math.floor(offset.value / PAGE_SIZE) + 1)
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)))
|
||||
const hasPrev = computed(() => offset.value > 0)
|
||||
const hasNext = computed(() => offset.value + PAGE_SIZE < total.value)
|
||||
|
||||
const SEARCH_FIELDS: Array<'name' | 'ownerUsername' | 'uuid'> = ['name', 'ownerUsername', 'uuid']
|
||||
|
||||
function searchFieldLabel(field: string): string {
|
||||
if (field === 'ownerUsername') return formatMessage(messages.searchOwner)
|
||||
if (field === 'uuid') return formatMessage(messages.searchUuid)
|
||||
return formatMessage(messages.searchName)
|
||||
}
|
||||
|
||||
function buildQuery() {
|
||||
const kw = keyword.value.trim()
|
||||
return {
|
||||
limit: PAGE_SIZE,
|
||||
offset: offset.value,
|
||||
searchField: kw ? searchField.value : undefined,
|
||||
keyword: kw || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function load(reset = false) {
|
||||
if (reset) offset.value = 0
|
||||
loading.value = true
|
||||
failed.value = false
|
||||
try {
|
||||
const query = buildQuery()
|
||||
const [listResult, countResult] = await Promise.all([
|
||||
skinSiteAdminPlayerList(query),
|
||||
skinSiteAdminPlayerCount(
|
||||
query.keyword ? { searchField: query.searchField, keyword: query.keyword } : undefined,
|
||||
),
|
||||
])
|
||||
if (!listResult.success) throw new Error(listResult.errorMessage)
|
||||
items.value = listResult.payload ?? []
|
||||
if (countResult.success && typeof countResult.payload === 'number') {
|
||||
total.value = countResult.payload
|
||||
}
|
||||
} catch (error) {
|
||||
failed.value = true
|
||||
handleError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function prevPage() {
|
||||
if (!hasPrev.value) return
|
||||
offset.value = Math.max(0, offset.value - PAGE_SIZE)
|
||||
await load()
|
||||
}
|
||||
|
||||
async function nextPage() {
|
||||
if (!hasNext.value) return
|
||||
offset.value += PAGE_SIZE
|
||||
await load()
|
||||
}
|
||||
|
||||
function startEdit(player: SkinSiteAdminPlayer) {
|
||||
editingUuid.value = player.uuid
|
||||
editName.value = player.name
|
||||
editIsMojang.value = player.isMojang
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingUuid.value = null
|
||||
editName.value = ''
|
||||
editIsMojang.value = false
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editingUuid.value || saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
const result = await skinSiteAdminUpdatePlayer({
|
||||
uuid: editingUuid.value,
|
||||
name: editName.value.trim(),
|
||||
isMojang: editIsMojang.value,
|
||||
})
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
addNotification({ type: 'success', title: formatMessage(messages.updated) })
|
||||
cancelEdit()
|
||||
await load()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(player: SkinSiteAdminPlayer) {
|
||||
if (deletingUuid.value !== null) return
|
||||
if (!window.confirm(formatMessage(messages.confirmDelete, { name: player.name }))) return
|
||||
deletingUuid.value = player.uuid
|
||||
try {
|
||||
const result = await skinSiteAdminDeletePlayer(player.uuid)
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
addNotification({ type: 'success', title: formatMessage(messages.deleted) })
|
||||
await load()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
deletingUuid.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => void load())
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">{{ formatMessage(messages.title) }}</h2>
|
||||
<span class="text-xs text-secondary">
|
||||
{{ formatMessage(messages.total, { count: total }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
v-model="searchField"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||
>
|
||||
<option v-for="f in SEARCH_FIELDS" :key="f" :value="f">
|
||||
{{ searchFieldLabel(f) }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.search)"
|
||||
class="min-w-0 flex-1 rounded-full border border-solid border-surface-5 bg-bg px-3 py-1 text-xs text-contrast outline-none focus:border-brand"
|
||||
@keyup.enter="load(true)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-brand px-4 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="load(true)"
|
||||
>
|
||||
{{ formatMessage(messages.search) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex items-center gap-2 text-sm text-secondary">
|
||||
<SpinnerIcon class="size-4 animate-spin" />
|
||||
{{ formatMessage(messages.loading) }}
|
||||
</div>
|
||||
<p v-else-if="failed" class="m-0 text-sm text-secondary">
|
||||
{{ formatMessage(messages.failed) }}
|
||||
</p>
|
||||
<p v-else-if="!items.length" class="m-0 rounded-xl bg-bg-raised p-4 text-sm text-secondary">
|
||||
{{ formatMessage(messages.empty) }}
|
||||
</p>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="player in items"
|
||||
:key="player.uuid"
|
||||
class="flex flex-wrap items-center gap-3 rounded-xl bg-bg-raised p-3"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<input
|
||||
v-if="editingUuid === player.uuid"
|
||||
v-model="editName"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-sm text-contrast outline-none focus:border-brand"
|
||||
/>
|
||||
<span v-else class="truncate text-sm font-semibold text-contrast">
|
||||
{{ player.name }}
|
||||
</span>
|
||||
<span class="truncate font-mono text-[10px] text-secondary">{{ player.uuid }}</span>
|
||||
</div>
|
||||
|
||||
<span class="shrink-0 text-xs text-secondary">{{ player.ownerUsername }}</span>
|
||||
<span
|
||||
class="shrink-0 rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold text-secondary"
|
||||
>
|
||||
{{ player.isMojang ? formatMessage(messages.mojang) : formatMessage(messages.custom) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="player.legacyInternalAdapted"
|
||||
class="shrink-0 rounded-full bg-yellow px-2 py-0.5 text-[10px] font-semibold text-contrast"
|
||||
>
|
||||
{{ formatMessage(messages.legacy) }}
|
||||
</span>
|
||||
|
||||
<template v-if="editingUuid === player.uuid">
|
||||
<label class="flex shrink-0 items-center gap-1 text-xs text-contrast">
|
||||
<input v-model="editIsMojang" type="checkbox" />
|
||||
{{ formatMessage(messages.mojang) }}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-full bg-brand px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||
:disabled="saving"
|
||||
@click="saveEdit"
|
||||
>
|
||||
{{ formatMessage(messages.save) }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="startEdit(player)"
|
||||
>
|
||||
{{ formatMessage(messages.edit) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-full bg-red px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||
:disabled="deletingUuid !== null"
|
||||
@click="remove(player)"
|
||||
>
|
||||
{{ formatMessage(messages.remove) }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||
:disabled="!hasPrev"
|
||||
@click="prevPage"
|
||||
>
|
||||
{{ formatMessage(messages.prev) }}
|
||||
</button>
|
||||
<span class="text-xs text-secondary">
|
||||
{{ formatMessage(messages.page, { page }) }} / {{ pageCount }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||
:disabled="!hasNext"
|
||||
@click="nextPage"
|
||||
>
|
||||
{{ formatMessage(messages.next) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,468 @@
|
||||
<script setup lang="ts">
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
skinSiteAdminDeleteUser,
|
||||
type SkinSiteAdminPlayer,
|
||||
skinSiteAdminUpdateUser,
|
||||
type SkinSiteAdminUser,
|
||||
skinSiteAdminUserCount,
|
||||
skinSiteAdminUserList,
|
||||
skinSiteAdminUserPlayers,
|
||||
} from '@/helpers/skin-site-api'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError, addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'app.skin-site.admin.users.title', defaultMessage: 'Users' },
|
||||
loading: { id: 'app.skin-site.admin.users.loading', defaultMessage: 'Loading…' },
|
||||
failed: { id: 'app.skin-site.admin.users.failed', defaultMessage: 'Could not load users.' },
|
||||
empty: { id: 'app.skin-site.admin.users.empty', defaultMessage: 'No users found.' },
|
||||
search: { id: 'app.skin-site.admin.users.search', defaultMessage: 'Search' },
|
||||
searchName: { id: 'app.skin-site.admin.users.search-name', defaultMessage: 'Username' },
|
||||
searchEmail: { id: 'app.skin-site.admin.users.search-email', defaultMessage: 'Email' },
|
||||
searchUuid: { id: 'app.skin-site.admin.users.search-uuid', defaultMessage: 'UUID' },
|
||||
total: { id: 'app.skin-site.admin.users.total', defaultMessage: '{count} users' },
|
||||
prev: { id: 'app.skin-site.admin.users.prev', defaultMessage: 'Previous' },
|
||||
next: { id: 'app.skin-site.admin.users.next', defaultMessage: 'Next' },
|
||||
page: { id: 'app.skin-site.admin.users.page', defaultMessage: 'Page {page}' },
|
||||
edit: { id: 'app.skin-site.admin.users.edit', defaultMessage: 'Edit' },
|
||||
save: { id: 'app.skin-site.admin.users.save', defaultMessage: 'Save' },
|
||||
cancel: { id: 'app.skin-site.admin.users.cancel', defaultMessage: 'Cancel' },
|
||||
remove: { id: 'app.skin-site.admin.users.remove', defaultMessage: 'Delete' },
|
||||
players: { id: 'app.skin-site.admin.users.players', defaultMessage: 'Players' },
|
||||
updated: { id: 'app.skin-site.admin.users.updated', defaultMessage: 'User updated.' },
|
||||
deleted: { id: 'app.skin-site.admin.users.deleted', defaultMessage: 'User deleted.' },
|
||||
confirmDelete: {
|
||||
id: 'app.skin-site.admin.users.confirm-delete',
|
||||
defaultMessage: 'Delete user “{name}”? This cannot be undone.',
|
||||
},
|
||||
username: { id: 'app.skin-site.admin.users.username', defaultMessage: 'Username' },
|
||||
email: { id: 'app.skin-site.admin.users.email', defaultMessage: 'Email' },
|
||||
qq: { id: 'app.skin-site.admin.users.qq', defaultMessage: 'QQ' },
|
||||
password: { id: 'app.skin-site.admin.users.password', defaultMessage: 'New password' },
|
||||
passwordHint: {
|
||||
id: 'app.skin-site.admin.users.password-hint',
|
||||
defaultMessage: 'Leave empty to keep the current password.',
|
||||
},
|
||||
banned: { id: 'app.skin-site.admin.users.banned', defaultMessage: 'Banned' },
|
||||
permissions: { id: 'app.skin-site.admin.users.permissions', defaultMessage: 'Permissions' },
|
||||
permissionsHint: {
|
||||
id: 'app.skin-site.admin.users.permissions-hint',
|
||||
defaultMessage: 'Comma separated, e.g. WEB_ADMIN, SERVER_ADMIN.',
|
||||
},
|
||||
noPlayers: { id: 'app.skin-site.admin.users.no-players', defaultMessage: 'No linked players.' },
|
||||
registered: { id: 'app.skin-site.admin.users.registered', defaultMessage: 'Registered' },
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const items = ref<SkinSiteAdminUser[]>([])
|
||||
const total = ref(0)
|
||||
const offset = ref(0)
|
||||
const loading = ref(true)
|
||||
const failed = ref(false)
|
||||
|
||||
const keyword = ref('')
|
||||
const searchField = ref<'username' | 'email' | 'uuid'>('username')
|
||||
|
||||
const editingUuid = ref<string | null>(null)
|
||||
const editForm = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
qq: '',
|
||||
password: '',
|
||||
banned: false,
|
||||
permissions: '',
|
||||
})
|
||||
const saving = ref(false)
|
||||
const deletingUuid = ref<string | null>(null)
|
||||
|
||||
const playersFor = ref<SkinSiteAdminPlayer[] | null>(null)
|
||||
const playersLoading = ref(false)
|
||||
const playersUuid = ref<string | null>(null)
|
||||
|
||||
const page = computed(() => Math.floor(offset.value / PAGE_SIZE) + 1)
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)))
|
||||
const hasPrev = computed(() => offset.value > 0)
|
||||
const hasNext = computed(() => offset.value + PAGE_SIZE < total.value)
|
||||
|
||||
const SEARCH_FIELDS: Array<'username' | 'email' | 'uuid'> = ['username', 'email', 'uuid']
|
||||
|
||||
function searchFieldLabel(field: string): string {
|
||||
if (field === 'email') return formatMessage(messages.searchEmail)
|
||||
if (field === 'uuid') return formatMessage(messages.searchUuid)
|
||||
return formatMessage(messages.searchName)
|
||||
}
|
||||
|
||||
function formatDate(ts: number): string {
|
||||
try {
|
||||
return new Date(ts).toLocaleDateString()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function load(reset = false) {
|
||||
if (reset) offset.value = 0
|
||||
loading.value = true
|
||||
failed.value = false
|
||||
try {
|
||||
const kw = keyword.value.trim()
|
||||
const listResult = await skinSiteAdminUserList({
|
||||
limit: PAGE_SIZE,
|
||||
offset: offset.value,
|
||||
searchField: kw ? searchField.value : undefined,
|
||||
keyword: kw || undefined,
|
||||
})
|
||||
if (!listResult.success) throw new Error(listResult.errorMessage)
|
||||
items.value = listResult.payload ?? []
|
||||
const countResult = await skinSiteAdminUserCount(
|
||||
kw ? { searchField: searchField.value, keyword: kw } : undefined,
|
||||
)
|
||||
if (countResult.success && typeof countResult.payload === 'number') {
|
||||
total.value = countResult.payload
|
||||
}
|
||||
} catch (error) {
|
||||
failed.value = true
|
||||
handleError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function prevPage() {
|
||||
if (!hasPrev.value) return
|
||||
offset.value = Math.max(0, offset.value - PAGE_SIZE)
|
||||
await load()
|
||||
}
|
||||
|
||||
async function nextPage() {
|
||||
if (!hasNext.value) return
|
||||
offset.value += PAGE_SIZE
|
||||
await load()
|
||||
}
|
||||
|
||||
function startEdit(user: SkinSiteAdminUser) {
|
||||
editingUuid.value = user.uuid
|
||||
editForm.value = {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
qq: user.qq === undefined || user.qq === null ? '' : String(user.qq),
|
||||
password: '',
|
||||
banned: user.banned,
|
||||
permissions: user.permissions.join(', '),
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingUuid.value = null
|
||||
editForm.value = {
|
||||
username: '',
|
||||
email: '',
|
||||
qq: '',
|
||||
password: '',
|
||||
banned: false,
|
||||
permissions: '',
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editingUuid.value || saving.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
const qqText = editForm.value.qq.trim()
|
||||
const permissions = editForm.value.permissions
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
const result = await skinSiteAdminUpdateUser({
|
||||
uuid: editingUuid.value,
|
||||
username: editForm.value.username.trim(),
|
||||
email: editForm.value.email.trim(),
|
||||
qq: qqText ? Number(qqText) : undefined,
|
||||
password: editForm.value.password ? editForm.value.password : undefined,
|
||||
banned: editForm.value.banned,
|
||||
permissions,
|
||||
})
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
addNotification({ type: 'success', title: formatMessage(messages.updated) })
|
||||
cancelEdit()
|
||||
await load()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(user: SkinSiteAdminUser) {
|
||||
if (deletingUuid.value !== null) return
|
||||
if (!window.confirm(formatMessage(messages.confirmDelete, { name: user.username }))) return
|
||||
deletingUuid.value = user.uuid
|
||||
try {
|
||||
const result = await skinSiteAdminDeleteUser(user.uuid)
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
addNotification({ type: 'success', title: formatMessage(messages.deleted) })
|
||||
await load()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
deletingUuid.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePlayers(user: SkinSiteAdminUser) {
|
||||
if (playersUuid.value === user.uuid) {
|
||||
playersUuid.value = null
|
||||
playersFor.value = null
|
||||
return
|
||||
}
|
||||
playersUuid.value = user.uuid
|
||||
playersLoading.value = true
|
||||
playersFor.value = null
|
||||
try {
|
||||
const result = await skinSiteAdminUserPlayers(user.uuid)
|
||||
if (result.success) {
|
||||
playersFor.value = result.payload ?? []
|
||||
} else {
|
||||
playersFor.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
playersFor.value = []
|
||||
handleError(error)
|
||||
} finally {
|
||||
playersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => void load())
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h2 class="m-0 text-lg font-bold text-contrast">{{ formatMessage(messages.title) }}</h2>
|
||||
<span class="text-xs text-secondary">
|
||||
{{ formatMessage(messages.total, { count: total }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
v-model="searchField"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||
>
|
||||
<option v-for="f in SEARCH_FIELDS" :key="f" :value="f">
|
||||
{{ searchFieldLabel(f) }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.search)"
|
||||
class="min-w-0 flex-1 rounded-full border border-solid border-surface-5 bg-bg px-3 py-1 text-xs text-contrast outline-none focus:border-brand"
|
||||
@keyup.enter="load(true)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-brand px-4 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="load(true)"
|
||||
>
|
||||
{{ formatMessage(messages.search) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex items-center gap-2 text-sm text-secondary">
|
||||
<SpinnerIcon class="size-4 animate-spin" />
|
||||
{{ formatMessage(messages.loading) }}
|
||||
</div>
|
||||
<p v-else-if="failed" class="m-0 text-sm text-secondary">
|
||||
{{ formatMessage(messages.failed) }}
|
||||
</p>
|
||||
<p v-else-if="!items.length" class="m-0 rounded-xl bg-bg-raised p-4 text-sm text-secondary">
|
||||
{{ formatMessage(messages.empty) }}
|
||||
</p>
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<div
|
||||
v-for="user in items"
|
||||
:key="user.uuid"
|
||||
class="flex flex-col gap-2 rounded-xl bg-bg-raised p-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<span class="truncate text-sm font-semibold text-contrast">
|
||||
{{ user.username }}
|
||||
<span class="text-xs font-normal text-secondary">#{{ user.id }}</span>
|
||||
</span>
|
||||
<span class="truncate text-xs text-secondary">{{ user.email }}</span>
|
||||
<span class="truncate font-mono text-[10px] text-secondary">{{ user.uuid }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="user.banned"
|
||||
class="shrink-0 rounded-full bg-red px-2 py-0.5 text-[10px] font-semibold text-contrast"
|
||||
>
|
||||
{{ formatMessage(messages.banned) }}
|
||||
</span>
|
||||
<span
|
||||
v-for="perm in user.permissions"
|
||||
:key="perm"
|
||||
class="shrink-0 rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold text-secondary"
|
||||
>
|
||||
{{ perm }}
|
||||
</span>
|
||||
<span class="shrink-0 text-xs text-secondary">
|
||||
{{ formatDate(user.registerTime) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<template v-if="editingUuid === user.uuid">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs font-semibold text-contrast">
|
||||
{{ formatMessage(messages.username) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="editForm.username"
|
||||
type="text"
|
||||
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-sm text-contrast outline-none focus:border-brand"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs font-semibold text-contrast">
|
||||
{{ formatMessage(messages.email) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="editForm.email"
|
||||
type="email"
|
||||
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-sm text-contrast outline-none focus:border-brand"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs font-semibold text-contrast">
|
||||
{{ formatMessage(messages.qq) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="editForm.qq"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-sm text-contrast outline-none focus:border-brand"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs font-semibold text-contrast">
|
||||
{{ formatMessage(messages.password) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="editForm.password"
|
||||
type="password"
|
||||
:placeholder="formatMessage(messages.passwordHint)"
|
||||
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-sm text-contrast outline-none focus:border-brand"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 sm:col-span-2">
|
||||
<span class="text-xs font-semibold text-contrast">
|
||||
{{ formatMessage(messages.permissions) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="editForm.permissions"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.permissionsHint)"
|
||||
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-sm text-contrast outline-none focus:border-brand"
|
||||
/>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="editForm.banned" type="checkbox" />
|
||||
<span class="text-sm text-contrast">{{ formatMessage(messages.banned) }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-brand px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||
:disabled="saving"
|
||||
@click="saveEdit"
|
||||
>
|
||||
{{ formatMessage(messages.save) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="startEdit(user)"
|
||||
>
|
||||
{{ formatMessage(messages.edit) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="togglePlayers(user)"
|
||||
>
|
||||
{{ formatMessage(messages.players) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-red px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||
:disabled="deletingUuid !== null"
|
||||
@click="remove(user)"
|
||||
>
|
||||
{{ formatMessage(messages.remove) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="playersUuid === user.uuid" class="flex flex-wrap gap-2 pt-1">
|
||||
<div v-if="playersLoading" class="flex items-center gap-2 text-xs text-secondary">
|
||||
<SpinnerIcon class="size-3 animate-spin" />
|
||||
{{ formatMessage(messages.loading) }}
|
||||
</div>
|
||||
<span v-else-if="!playersFor || !playersFor.length" class="text-xs text-secondary">
|
||||
{{ formatMessage(messages.noPlayers) }}
|
||||
</span>
|
||||
<span
|
||||
v-for="p in playersFor ?? []"
|
||||
:key="p.uuid"
|
||||
class="rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold text-contrast"
|
||||
>
|
||||
{{ p.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||
:disabled="!hasPrev"
|
||||
@click="prevPage"
|
||||
>
|
||||
{{ formatMessage(messages.prev) }}
|
||||
</button>
|
||||
<span class="text-xs text-secondary">
|
||||
{{ formatMessage(messages.page, { page }) }} / {{ pageCount }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
|
||||
:disabled="!hasNext"
|
||||
@click="nextPage"
|
||||
>
|
||||
{{ formatMessage(messages.next) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -904,3 +904,131 @@ export function skinSiteUpdateBroadcast(
|
||||
export function skinSiteDeleteBroadcast(id: number): Promise<SkinSiteResponse<string>> {
|
||||
return skinSiteRequest<string>('broadcast/' + id, { method: 'DELETE', type: 'skin' })
|
||||
}
|
||||
|
||||
export interface SkinSiteAdminPlayer {
|
||||
uuid: string
|
||||
name: string
|
||||
isMojang: boolean
|
||||
ownerUsername: string
|
||||
legacyInternalAdapted: boolean
|
||||
}
|
||||
|
||||
export interface SkinSiteAdminPlayerQuery {
|
||||
limit: number
|
||||
offset: number
|
||||
searchField?: string
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export function skinSiteAdminPlayerCount(params?: {
|
||||
searchField?: string
|
||||
keyword?: string
|
||||
}): Promise<SkinSiteResponse<number>> {
|
||||
const query = new URLSearchParams()
|
||||
if (params?.searchField?.trim()) query.set('searchField', params.searchField!.trim())
|
||||
if (params?.keyword?.trim()) query.set('keyword', params.keyword!.trim())
|
||||
const suffix = query.toString() ? '?' + query.toString() : ''
|
||||
return skinSiteRequest<number>('admin/players/number' + suffix, { type: 'skin' })
|
||||
}
|
||||
|
||||
export function skinSiteAdminPlayerList(
|
||||
query: SkinSiteAdminPlayerQuery,
|
||||
): Promise<SkinSiteResponse<SkinSiteAdminPlayer[]>> {
|
||||
return skinSiteRequest<SkinSiteAdminPlayer[]>('admin/players/list', {
|
||||
method: 'POST',
|
||||
type: 'skin',
|
||||
body: query,
|
||||
})
|
||||
}
|
||||
|
||||
export function skinSiteAdminUpdatePlayer(input: {
|
||||
uuid: string
|
||||
name: string
|
||||
isMojang: boolean
|
||||
}): Promise<SkinSiteResponse<string>> {
|
||||
return skinSiteRequest<string>('admin/update', {
|
||||
method: 'POST',
|
||||
type: 'skin',
|
||||
body: input,
|
||||
})
|
||||
}
|
||||
|
||||
export function skinSiteAdminDeletePlayer(uuid: string): Promise<SkinSiteResponse<string>> {
|
||||
return skinSiteRequest<string>('admin/delete/' + encodeURIComponent(uuid), {
|
||||
method: 'POST',
|
||||
type: 'skin',
|
||||
})
|
||||
}
|
||||
|
||||
export interface SkinSiteAdminUser {
|
||||
uuid: string
|
||||
username: string
|
||||
email: string
|
||||
qq?: number
|
||||
banned: boolean
|
||||
id: number
|
||||
permissions: string[]
|
||||
avatar?: string
|
||||
registerTime: number
|
||||
}
|
||||
|
||||
export function skinSiteAdminUserCount(params?: {
|
||||
searchField?: string
|
||||
keyword?: string
|
||||
}): Promise<SkinSiteResponse<number>> {
|
||||
const query = new URLSearchParams()
|
||||
if (params?.searchField?.trim()) query.set('searchField', params.searchField!.trim())
|
||||
if (params?.keyword?.trim()) query.set('keyword', params.keyword!.trim())
|
||||
const suffix = query.toString() ? '?' + query.toString() : ''
|
||||
return skinSiteRequest<number>('admin/users/number' + suffix)
|
||||
}
|
||||
|
||||
export function skinSiteAdminUserList(query: {
|
||||
limit: number
|
||||
offset: number
|
||||
searchField?: string
|
||||
keyword?: string
|
||||
}): Promise<SkinSiteResponse<SkinSiteAdminUser[]>> {
|
||||
return skinSiteRequest<SkinSiteAdminUser[]>('admin/users/list', {
|
||||
method: 'POST',
|
||||
body: query,
|
||||
})
|
||||
}
|
||||
|
||||
export function skinSiteAdminUpdateUser(input: {
|
||||
uuid: string
|
||||
username: string
|
||||
password?: string
|
||||
email: string
|
||||
qq?: number
|
||||
banned: boolean
|
||||
permissions: string[]
|
||||
}): Promise<SkinSiteResponse<string>> {
|
||||
return skinSiteRequest<string>('admin/update', { method: 'POST', body: input })
|
||||
}
|
||||
|
||||
export function skinSiteAdminDeleteUser(uuid: string): Promise<SkinSiteResponse<string>> {
|
||||
return skinSiteRequest<string>('admin/delete/' + encodeURIComponent(uuid), {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export function skinSiteAdminUserPlayers(
|
||||
uuid: string,
|
||||
): Promise<SkinSiteResponse<SkinSiteAdminPlayer[]>> {
|
||||
return skinSiteRequest<SkinSiteAdminPlayer[]>(
|
||||
'admin/users/' + encodeURIComponent(uuid) + '/players',
|
||||
)
|
||||
}
|
||||
|
||||
export interface SkinSiteFrequencyLimit {
|
||||
dropPercent?: number | null
|
||||
}
|
||||
|
||||
export function skinSiteAdminUserFrequencyLimit(
|
||||
uuid: string,
|
||||
): Promise<SkinSiteResponse<SkinSiteFrequencyLimit>> {
|
||||
return skinSiteRequest<SkinSiteFrequencyLimit>(
|
||||
'admin/users/' + encodeURIComponent(uuid) + '/frequency-limit',
|
||||
)
|
||||
}
|
||||
@@ -11830,5 +11830,61 @@
|
||||
"app.skin-site.admin.broadcast.updated": {"message": "Announcement updated."},
|
||||
"app.skin-site.admin.broadcast.deleted": {"message": "Announcement deleted."},
|
||||
"app.skin-site.admin.broadcast.confirm-delete": {"message": "Delete “{title}”? This cannot be undone."},
|
||||
"app.skin-site.admin.broadcast.uploader": {"message": "by {name}"}
|
||||
"app.skin-site.admin.broadcast.uploader": {"message": "by {name}"},
|
||||
"app.skin-site.admin.players.title": {"message": "Players"},
|
||||
"app.skin-site.admin.players.loading": {"message": "Loading…"},
|
||||
"app.skin-site.admin.players.failed": {"message": "Could not load players."},
|
||||
"app.skin-site.admin.players.empty": {"message": "No players found."},
|
||||
"app.skin-site.admin.players.search": {"message": "Search"},
|
||||
"app.skin-site.admin.players.search-name": {"message": "Name"},
|
||||
"app.skin-site.admin.players.search-owner": {"message": "Owner"},
|
||||
"app.skin-site.admin.players.search-uuid": {"message": "UUID"},
|
||||
"app.skin-site.admin.players.total": {"message": "{count} players"},
|
||||
"app.skin-site.admin.players.prev": {"message": "Previous"},
|
||||
"app.skin-site.admin.players.next": {"message": "Next"},
|
||||
"app.skin-site.admin.players.page": {"message": "Page {page}"},
|
||||
"app.skin-site.admin.players.name": {"message": "Name"},
|
||||
"app.skin-site.admin.players.owner": {"message": "Owner"},
|
||||
"app.skin-site.admin.players.source": {"message": "Source"},
|
||||
"app.skin-site.admin.players.legacy": {"message": "Legacy adapted"},
|
||||
"app.skin-site.admin.players.actions": {"message": "Actions"},
|
||||
"app.skin-site.admin.players.edit": {"message": "Edit"},
|
||||
"app.skin-site.admin.players.save": {"message": "Save"},
|
||||
"app.skin-site.admin.players.cancel": {"message": "Cancel"},
|
||||
"app.skin-site.admin.players.remove": {"message": "Delete"},
|
||||
"app.skin-site.admin.players.updated": {"message": "Player updated."},
|
||||
"app.skin-site.admin.players.deleted": {"message": "Player deleted."},
|
||||
"app.skin-site.admin.players.confirm-delete": {"message": "Delete player “{name}”? This cannot be undone."},
|
||||
"app.skin-site.admin.players.mojang": {"message": "Mojang"},
|
||||
"app.skin-site.admin.players.custom": {"message": "Skin site"},
|
||||
"app.skin-site.admin.users.title": {"message": "Users"},
|
||||
"app.skin-site.admin.users.loading": {"message": "Loading…"},
|
||||
"app.skin-site.admin.users.failed": {"message": "Could not load users."},
|
||||
"app.skin-site.admin.users.empty": {"message": "No users found."},
|
||||
"app.skin-site.admin.users.search": {"message": "Search"},
|
||||
"app.skin-site.admin.users.search-name": {"message": "Username"},
|
||||
"app.skin-site.admin.users.search-email": {"message": "Email"},
|
||||
"app.skin-site.admin.users.search-uuid": {"message": "UUID"},
|
||||
"app.skin-site.admin.users.total": {"message": "{count} users"},
|
||||
"app.skin-site.admin.users.prev": {"message": "Previous"},
|
||||
"app.skin-site.admin.users.next": {"message": "Next"},
|
||||
"app.skin-site.admin.users.page": {"message": "Page {page}"},
|
||||
"app.skin-site.admin.users.edit": {"message": "Edit"},
|
||||
"app.skin-site.admin.users.save": {"message": "Save"},
|
||||
"app.skin-site.admin.users.cancel": {"message": "Cancel"},
|
||||
"app.skin-site.admin.users.remove": {"message": "Delete"},
|
||||
"app.skin-site.admin.users.players": {"message": "Players"},
|
||||
"app.skin-site.admin.users.updated": {"message": "User updated."},
|
||||
"app.skin-site.admin.users.deleted": {"message": "User deleted."},
|
||||
"app.skin-site.admin.users.confirm-delete": {"message": "Delete user “{name}”? This cannot be undone."},
|
||||
"app.skin-site.admin.users.username": {"message": "Username"},
|
||||
"app.skin-site.admin.users.email": {"message": "Email"},
|
||||
"app.skin-site.admin.users.qq": {"message": "QQ"},
|
||||
"app.skin-site.admin.users.password": {"message": "New password"},
|
||||
"app.skin-site.admin.users.password-hint": {"message": "Leave empty to keep the current password."},
|
||||
"app.skin-site.admin.users.banned": {"message": "Banned"},
|
||||
"app.skin-site.admin.users.permissions": {"message": "Permissions"},
|
||||
"app.skin-site.admin.users.permissions-hint": {"message": "Comma separated, e.g. WEB_ADMIN, SERVER_ADMIN."},
|
||||
"app.skin-site.admin.users.no-players": {"message": "No linked players."},
|
||||
"app.skin-site.admin.users.registered": {"message": "Registered"}
|
||||
}
|
||||
@@ -11661,5 +11661,61 @@
|
||||
"app.skin-site.admin.broadcast.updated": {"message": "公告已更新。"},
|
||||
"app.skin-site.admin.broadcast.deleted": {"message": "公告已删除。"},
|
||||
"app.skin-site.admin.broadcast.confirm-delete": {"message": "确定删除「{title}」吗?此操作不可撤销。"},
|
||||
"app.skin-site.admin.broadcast.uploader": {"message": "由 {name} 发布"}
|
||||
"app.skin-site.admin.broadcast.uploader": {"message": "由 {name} 发布"},
|
||||
"app.skin-site.admin.players.title": {"message": "玩家"},
|
||||
"app.skin-site.admin.players.loading": {"message": "加载中…"},
|
||||
"app.skin-site.admin.players.failed": {"message": "无法加载玩家列表。"},
|
||||
"app.skin-site.admin.players.empty": {"message": "没有找到玩家。"},
|
||||
"app.skin-site.admin.players.search": {"message": "搜索"},
|
||||
"app.skin-site.admin.players.search-name": {"message": "名称"},
|
||||
"app.skin-site.admin.players.search-owner": {"message": "所有者"},
|
||||
"app.skin-site.admin.players.search-uuid": {"message": "UUID"},
|
||||
"app.skin-site.admin.players.total": {"message": "共 {count} 名玩家"},
|
||||
"app.skin-site.admin.players.prev": {"message": "上一页"},
|
||||
"app.skin-site.admin.players.next": {"message": "下一页"},
|
||||
"app.skin-site.admin.players.page": {"message": "第 {page} 页"},
|
||||
"app.skin-site.admin.players.name": {"message": "名称"},
|
||||
"app.skin-site.admin.players.owner": {"message": "所有者"},
|
||||
"app.skin-site.admin.players.source": {"message": "来源"},
|
||||
"app.skin-site.admin.players.legacy": {"message": "旧版适配"},
|
||||
"app.skin-site.admin.players.actions": {"message": "操作"},
|
||||
"app.skin-site.admin.players.edit": {"message": "编辑"},
|
||||
"app.skin-site.admin.players.save": {"message": "保存"},
|
||||
"app.skin-site.admin.players.cancel": {"message": "取消"},
|
||||
"app.skin-site.admin.players.remove": {"message": "删除"},
|
||||
"app.skin-site.admin.players.updated": {"message": "玩家已更新。"},
|
||||
"app.skin-site.admin.players.deleted": {"message": "玩家已删除。"},
|
||||
"app.skin-site.admin.players.confirm-delete": {"message": "确定删除玩家「{name}」吗?此操作不可撤销。"},
|
||||
"app.skin-site.admin.players.mojang": {"message": "正版"},
|
||||
"app.skin-site.admin.players.custom": {"message": "皮肤站"},
|
||||
"app.skin-site.admin.users.title": {"message": "用户"},
|
||||
"app.skin-site.admin.users.loading": {"message": "加载中…"},
|
||||
"app.skin-site.admin.users.failed": {"message": "无法加载用户列表。"},
|
||||
"app.skin-site.admin.users.empty": {"message": "没有找到用户。"},
|
||||
"app.skin-site.admin.users.search": {"message": "搜索"},
|
||||
"app.skin-site.admin.users.search-name": {"message": "用户名"},
|
||||
"app.skin-site.admin.users.search-email": {"message": "邮箱"},
|
||||
"app.skin-site.admin.users.search-uuid": {"message": "UUID"},
|
||||
"app.skin-site.admin.users.total": {"message": "共 {count} 名用户"},
|
||||
"app.skin-site.admin.users.prev": {"message": "上一页"},
|
||||
"app.skin-site.admin.users.next": {"message": "下一页"},
|
||||
"app.skin-site.admin.users.page": {"message": "第 {page} 页"},
|
||||
"app.skin-site.admin.users.edit": {"message": "编辑"},
|
||||
"app.skin-site.admin.users.save": {"message": "保存"},
|
||||
"app.skin-site.admin.users.cancel": {"message": "取消"},
|
||||
"app.skin-site.admin.users.remove": {"message": "删除"},
|
||||
"app.skin-site.admin.users.players": {"message": "角色"},
|
||||
"app.skin-site.admin.users.updated": {"message": "用户已更新。"},
|
||||
"app.skin-site.admin.users.deleted": {"message": "用户已删除。"},
|
||||
"app.skin-site.admin.users.confirm-delete": {"message": "确定删除用户「{name}」吗?此操作不可撤销。"},
|
||||
"app.skin-site.admin.users.username": {"message": "用户名"},
|
||||
"app.skin-site.admin.users.email": {"message": "邮箱"},
|
||||
"app.skin-site.admin.users.qq": {"message": "QQ"},
|
||||
"app.skin-site.admin.users.password": {"message": "新密码"},
|
||||
"app.skin-site.admin.users.password-hint": {"message": "留空则保持原密码不变。"},
|
||||
"app.skin-site.admin.users.banned": {"message": "封禁"},
|
||||
"app.skin-site.admin.users.permissions": {"message": "权限"},
|
||||
"app.skin-site.admin.users.permissions-hint": {"message": "以逗号分隔,例如 WEB_ADMIN, SERVER_ADMIN。"},
|
||||
"app.skin-site.admin.users.no-players": {"message": "没有绑定的角色。"},
|
||||
"app.skin-site.admin.users.registered": {"message": "注册时间"}
|
||||
}
|
||||
Reference in new issue
Block a user