feat: 新增皮肤站管理台维基管理页
Sync source to CNB / Sync Git ref (push) Skipped

This commit is contained in:
AxTps committed 2026-10-18 09:11:58 +08:00
1 parent 4a3b4268b9
commit 8d73689356
5 files changed
+670 -2

No files matched your search

@@ -15,6 +15,7 @@ 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 }>()
@@ -181,6 +182,7 @@ const current = ref<AdminKey>('summary')
<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>
@@ -0,0 +1,512 @@
<script setup lang="ts">
import { SpinnerIcon } from '@modrinth/assets'
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { onMounted, ref } from 'vue'
import {
skinSiteWikiAudit,
type SkinSiteWikiAuditInfo,
type SkinSiteWikiEditPolicy,
skinSiteWikiPage,
type SkinSiteWikiPageDetail,
skinSiteWikiProtectPage,
type SkinSiteWikiReadPolicy,
skinSiteWikiSetHomePage,
skinSiteWikiSetPageDeleted,
type SkinSiteWikiSettings,
skinSiteWikiSettings,
skinSiteWikiUpdateSettings,
} from '@/helpers/skin-site-api'
const { formatMessage } = useVIntl()
const { handleError, addNotification } = injectNotificationManager()
const messages = defineMessages({
title: { id: 'app.skin-site.admin.wiki.title', defaultMessage: 'Wiki' },
tabPage: { id: 'app.skin-site.admin.wiki.tab-page', defaultMessage: 'Page' },
tabAudit: { id: 'app.skin-site.admin.wiki.tab-audit', defaultMessage: 'Audit log' },
tabSettings: { id: 'app.skin-site.admin.wiki.tab-settings', defaultMessage: 'Settings' },
loading: { id: 'app.skin-site.admin.wiki.loading', defaultMessage: 'Loading…' },
pagePath: { id: 'app.skin-site.admin.wiki.page-path', defaultMessage: 'Page path' },
loadPage: { id: 'app.skin-site.admin.wiki.load-page', defaultMessage: 'Load page' },
pageNotFound: {
id: 'app.skin-site.admin.wiki.page-not-found',
defaultMessage: 'Page not found.',
},
revision: { id: 'app.skin-site.admin.wiki.revision', defaultMessage: 'Revision' },
author: { id: 'app.skin-site.admin.wiki.author', defaultMessage: 'Author' },
updatedAt: { id: 'app.skin-site.admin.wiki.updated-at', defaultMessage: 'Updated' },
categories: { id: 'app.skin-site.admin.wiki.categories', defaultMessage: 'Categories' },
readPolicy: { id: 'app.skin-site.admin.wiki.read-policy', defaultMessage: 'Read policy' },
editPolicy: { id: 'app.skin-site.admin.wiki.edit-policy', defaultMessage: 'Edit policy' },
saveProtect: { id: 'app.skin-site.admin.wiki.save-protect', defaultMessage: 'Save policies' },
protectSaved: {
id: 'app.skin-site.admin.wiki.protect-saved',
defaultMessage: 'Page policies updated.',
},
setHome: { id: 'app.skin-site.admin.wiki.set-home', defaultMessage: 'Set as home page' },
homeSet: { id: 'app.skin-site.admin.wiki.home-set', defaultMessage: 'Home page updated.' },
setDeleted: { id: 'app.skin-site.admin.wiki.set-deleted', defaultMessage: 'Mark as deleted' },
setRestored: { id: 'app.skin-site.admin.wiki.set-restored', defaultMessage: 'Restore page' },
deletedUpdated: {
id: 'app.skin-site.admin.wiki.deleted-updated',
defaultMessage: 'Page deletion state updated.',
},
auditEmpty: { id: 'app.skin-site.admin.wiki.audit-empty', defaultMessage: 'No audit entries.' },
auditTarget: { id: 'app.skin-site.admin.wiki.audit-target', defaultMessage: 'Target' },
loadMore: { id: 'app.skin-site.admin.wiki.load-more', defaultMessage: 'Load more' },
settingsHint: {
id: 'app.skin-site.admin.wiki.settings-hint',
defaultMessage: 'Comma separated domains.',
},
domains: { id: 'app.skin-site.admin.wiki.domains', defaultMessage: 'Domains' },
homePage: { id: 'app.skin-site.admin.wiki.home-page', defaultMessage: 'Home page' },
defaultSkin: { id: 'app.skin-site.admin.wiki.default-skin', defaultMessage: 'Default skin' },
maxPageBytes: {
id: 'app.skin-site.admin.wiki.max-page-bytes',
defaultMessage: 'Max page bytes',
},
maxAttachmentBytes: {
id: 'app.skin-site.admin.wiki.max-attachment-bytes',
defaultMessage: 'Max attachment bytes',
},
saveSettings: { id: 'app.skin-site.admin.wiki.save-settings', defaultMessage: 'Save settings' },
settingsSaved: {
id: 'app.skin-site.admin.wiki.settings-saved',
defaultMessage: 'Settings saved.',
},
})
type Tab = 'page' | 'audit' | 'settings'
const tab = ref<Tab>('page')
const pagePath = ref('')
const page = ref<SkinSiteWikiPageDetail | null>(null)
const pageLoading = ref(false)
const pageError = ref(false)
const protectRead = ref<SkinSiteWikiReadPolicy>('PUBLIC')
const protectEdit = ref<SkinSiteWikiEditPolicy>('MEMBER')
const savingProtect = ref(false)
const auditItems = ref<SkinSiteWikiAuditInfo[]>([])
const auditNext = ref<number | null>(null)
const auditLoading = ref(false)
const auditLoadingMore = ref(false)
const settings = ref<SkinSiteWikiSettings | null>(null)
const settingsLoading = ref(false)
const settingsSaving = ref(false)
const settingsForm = ref({
domains: '',
homePage: '',
defaultSkin: '',
maxPageUtf8Bytes: 0,
maxAttachmentBytes: 0,
})
function formatDateTime(ts: number): string {
try {
return new Date(ts).toLocaleString()
} catch {
return ''
}
}
async function loadPage() {
const path = pagePath.value.trim()
if (!path) return
pageLoading.value = true
pageError.value = false
page.value = null
try {
const result = await skinSiteWikiPage(path)
if (!result.success) throw new Error(result.errorMessage)
page.value = result.payload
protectRead.value = result.payload.readPolicy
protectEdit.value = result.payload.editPolicy
} catch (error) {
pageError.value = true
handleError(error)
} finally {
pageLoading.value = false
}
}
async function saveProtect() {
if (!page.value || savingProtect.value) return
savingProtect.value = true
try {
const result = await skinSiteWikiProtectPage(
pagePath.value.trim(),
protectRead.value,
protectEdit.value,
)
if (!result.success) throw new Error(result.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.protectSaved) })
await loadPage()
} catch (error) {
handleError(error)
} finally {
savingProtect.value = false
}
}
async function setHome() {
if (!page.value) return
try {
const result = await skinSiteWikiSetHomePage(pagePath.value.trim())
if (!result.success) throw new Error(result.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.homeSet) })
} catch (error) {
handleError(error)
}
}
async function toggleDeleted() {
if (!page.value) return
try {
const result = await skinSiteWikiSetPageDeleted(pagePath.value.trim(), !page.value.canEdit)
if (!result.success) throw new Error(result.errorMessage)
addNotification({ type: 'success', title: formatMessage(messages.deletedUpdated) })
await loadPage()
} catch (error) {
handleError(error)
}
}
async function loadAudit(reset = true) {
if (reset) {
auditLoading.value = true
auditItems.value = []
auditNext.value = null
} else {
auditLoadingMore.value = true
}
try {
const result = await skinSiteWikiAudit(reset ? undefined : (auditNext.value ?? undefined))
if (!result.success) throw new Error(result.errorMessage)
const data = result.payload
auditItems.value = reset ? data.items : [...auditItems.value, ...data.items]
auditNext.value = data.nextCursor ?? null
} catch (error) {
handleError(error)
} finally {
auditLoading.value = false
auditLoadingMore.value = false
}
}
async function loadSettings() {
settingsLoading.value = true
try {
const result = await skinSiteWikiSettings()
if (!result.success) throw new Error(result.errorMessage)
settings.value = result.payload
settingsForm.value = {
domains: result.payload.domains.join(', '),
homePage: result.payload.homePage,
defaultSkin: result.payload.defaultSkin,
maxPageUtf8Bytes: result.payload.maxPageUtf8Bytes,
maxAttachmentBytes: result.payload.maxAttachmentBytes,
}
} catch (error) {
handleError(error)
} finally {
settingsLoading.value = false
}
}
async function saveSettings() {
if (settingsSaving.value) return
settingsSaving.value = true
try {
const result = await skinSiteWikiUpdateSettings({
domains: settingsForm.value.domains
.split(',')
.map((d) => d.trim())
.filter(Boolean),
homePage: settingsForm.value.homePage.trim(),
defaultSkin: settingsForm.value.defaultSkin.trim(),
maxPageUtf8Bytes: Number(settingsForm.value.maxPageUtf8Bytes) || 0,
maxAttachmentBytes: Number(settingsForm.value.maxAttachmentBytes) || 0,
})
if (!result.success) throw new Error(result.errorMessage)
settings.value = result.payload
addNotification({ type: 'success', title: formatMessage(messages.settingsSaved) })
} catch (error) {
handleError(error)
} finally {
settingsSaving.value = false
}
}
async function switchTab(next: Tab) {
tab.value = next
if (next === 'audit' && !auditItems.value.length) await loadAudit(true)
if (next === 'settings' && !settings.value) await loadSettings()
}
onMounted(() => void loadSettings())
</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>
<button
type="button"
class="rounded-full px-3 py-1 text-xs font-semibold"
:class="tab === 'page' ? 'bg-brand text-contrast' : 'bg-button-bg text-secondary'"
@click="switchTab('page')"
>
{{ formatMessage(messages.tabPage) }}
</button>
<button
type="button"
class="rounded-full px-3 py-1 text-xs font-semibold"
:class="tab === 'audit' ? 'bg-brand text-contrast' : 'bg-button-bg text-secondary'"
@click="switchTab('audit')"
>
{{ formatMessage(messages.tabAudit) }}
</button>
<button
type="button"
class="rounded-full px-3 py-1 text-xs font-semibold"
:class="tab === 'settings' ? 'bg-brand text-contrast' : 'bg-button-bg text-secondary'"
@click="switchTab('settings')"
>
{{ formatMessage(messages.tabSettings) }}
</button>
</div>
<template v-if="tab === 'page'">
<div class="flex flex-wrap items-center gap-2">
<input
v-model="pagePath"
type="text"
:placeholder="formatMessage(messages.pagePath)"
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="loadPage"
/>
<button
type="button"
class="rounded-full bg-brand px-4 py-1 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
:disabled="pageLoading || !pagePath.trim()"
@click="loadPage"
>
{{ formatMessage(messages.loadPage) }}
</button>
</div>
<div v-if="pageLoading" class="flex items-center gap-2 text-sm text-secondary">
<SpinnerIcon class="size-4 animate-spin" />
{{ formatMessage(messages.loading) }}
</div>
<p v-else-if="pageError" class="m-0 text-sm text-secondary">
{{ formatMessage(messages.pageNotFound) }}
</p>
<template v-else-if="page">
<div class="flex flex-col gap-2 rounded-xl bg-bg-raised p-4">
<div class="flex flex-wrap items-center gap-2">
<span class="min-w-0 flex-1 truncate text-lg font-bold text-contrast">
{{ page.title }}
</span>
<span class="shrink-0 text-xs text-secondary">
{{ formatMessage(messages.revision) }} #{{ page.revisionId }}
</span>
</div>
<div class="flex flex-wrap items-center gap-3 text-xs text-secondary">
<span>{{ formatMessage(messages.author) }}: {{ page.authorName }}</span>
<span>
{{ formatMessage(messages.updatedAt) }}:
{{ formatDateTime(page.updatedAt) }}
</span>
</div>
<div v-if="page.categories.length" class="flex flex-wrap items-center gap-1">
<span class="text-xs text-secondary"> {{ formatMessage(messages.categories) }}: </span>
<span
v-for="c in page.categories"
:key="c"
class="rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold text-secondary"
>
{{ c }}
</span>
</div>
<p
v-if="page.summary"
class="m-0 rounded-lg bg-bg p-2 text-xs whitespace-pre-wrap break-words text-secondary"
>
{{ page.summary }}
</p>
</div>
<div class="flex flex-wrap items-end gap-3 rounded-xl bg-bg-raised p-4">
<label class="flex flex-col gap-1">
<span class="text-xs font-semibold text-contrast">
{{ formatMessage(messages.readPolicy) }}
</span>
<select
v-model="protectRead"
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-xs text-contrast"
>
<option value="PUBLIC">PUBLIC</option>
<option value="MEMBER">MEMBER</option>
</select>
</label>
<label class="flex flex-col gap-1">
<span class="text-xs font-semibold text-contrast">
{{ formatMessage(messages.editPolicy) }}
</span>
<select
v-model="protectEdit"
class="rounded-lg border border-solid border-surface-5 bg-bg px-2 py-1 text-xs text-contrast"
>
<option value="MEMBER">MEMBER</option>
<option value="WEB_ADMIN">WEB_ADMIN</option>
</select>
</label>
<button
type="button"
class="rounded-full bg-brand px-4 py-1.5 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
:disabled="savingProtect"
@click="saveProtect"
>
{{ formatMessage(messages.saveProtect) }}
</button>
<button
type="button"
class="rounded-full bg-button-bg px-4 py-1.5 text-xs font-semibold text-contrast hover:brightness-110"
@click="setHome"
>
{{ formatMessage(messages.setHome) }}
</button>
<button
type="button"
class="rounded-full bg-red px-4 py-1.5 text-xs font-semibold text-contrast hover:brightness-110"
@click="toggleDeleted"
>
{{
page.canEdit
? formatMessage(messages.setDeleted)
: formatMessage(messages.setRestored)
}}
</button>
</div>
</template>
</template>
<template v-else-if="tab === 'audit'">
<div v-if="auditLoading" class="flex items-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-xl bg-bg-raised p-4 text-sm text-secondary"
>
{{ formatMessage(messages.auditEmpty) }}
</p>
<div v-else class="flex flex-col gap-2">
<div
v-for="entry in auditItems"
:key="entry.id"
class="flex flex-col gap-1 rounded-xl bg-bg-raised p-3"
>
<div class="flex flex-wrap items-center gap-2">
<span class="shrink-0 text-xs text-secondary">#{{ entry.id }}</span>
<span class="min-w-0 flex-1 truncate text-sm font-semibold text-contrast">
{{ entry.action }}
</span>
<span class="shrink-0 text-xs text-secondary">{{ entry.actorName }}</span>
<span class="shrink-0 text-xs text-secondary">
{{ formatDateTime(entry.createdAt) }}
</span>
</div>
<span v-if="entry.target" class="truncate text-xs text-secondary">
{{ formatMessage(messages.auditTarget) }}: {{ entry.target }}
</span>
<span v-if="entry.detail" class="truncate text-xs text-secondary">
{{ entry.detail }}
</span>
</div>
<button
v-if="auditNext !== null"
type="button"
class="w-fit self-center rounded-full bg-button-bg px-4 py-2 text-xs font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
:disabled="auditLoadingMore"
@click="loadAudit(false)"
>
{{ formatMessage(messages.loadMore) }}
</button>
</div>
</template>
<template v-else>
<div v-if="settingsLoading" class="flex items-center gap-2 text-sm text-secondary">
<SpinnerIcon class="size-4 animate-spin" />
{{ formatMessage(messages.loading) }}
</div>
<template v-else-if="settings">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label class="flex flex-col gap-1 sm:col-span-2">
<span class="text-xs font-semibold text-contrast">
{{ formatMessage(messages.domains) }}
</span>
<input
v-model="settingsForm.domains"
type="text"
:placeholder="formatMessage(messages.settingsHint)"
class="rounded-lg border border-solid border-surface-5 bg-bg px-3 py-2 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.homePage) }}
</span>
<input
v-model="settingsForm.homePage"
type="text"
class="rounded-lg border border-solid border-surface-5 bg-bg px-3 py-2 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.defaultSkin) }}
</span>
<input
v-model="settingsForm.defaultSkin"
type="text"
class="rounded-lg border border-solid border-surface-5 bg-bg px-3 py-2 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.maxPageBytes) }}
</span>
<input
v-model.number="settingsForm.maxPageUtf8Bytes"
type="number"
class="rounded-lg border border-solid border-surface-5 bg-bg px-3 py-2 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.maxAttachmentBytes) }}
</span>
<input
v-model.number="settingsForm.maxAttachmentBytes"
type="number"
class="rounded-lg border border-solid border-surface-5 bg-bg px-3 py-2 text-sm text-contrast outline-none focus:border-brand"
/>
</label>
</div>
<button
type="button"
class="w-fit rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110 disabled:opacity-50"
:disabled="settingsSaving"
@click="saveSettings"
>
{{ formatMessage(messages.saveSettings) }}
</button>
</template>
</template>
</div>
</template>
@@ -1699,3 +1699,93 @@ export function skinSiteActivateModRuntime(
export function skinSiteDeleteModRuntime(id: number): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>('runtime/admin/' + id, { method: 'DELETE', type: 'mod' })
}
export type SkinSiteWikiReadPolicy = 'PUBLIC' | 'MEMBER'
export type SkinSiteWikiEditPolicy = 'MEMBER' | 'WEB_ADMIN'
export interface SkinSiteWikiPageDetail {
title: string
revisionId: number
source: string
nodes: unknown[]
categories: string[]
updatedAt: number
authorName: string
summary: string
readPolicy: SkinSiteWikiReadPolicy
editPolicy: SkinSiteWikiEditPolicy
redirectTarget?: string | null
canEdit: boolean
watched: boolean
}
export interface SkinSiteWikiSettings {
domains: string[]
homePage: string
defaultSkin: string
maxPageUtf8Bytes: number
maxAttachmentBytes: number
}
export interface SkinSiteWikiAuditInfo {
id: number
actorName: string
action: string
target?: string | null
detail?: string | null
createdAt: number
}
export interface SkinSiteWikiAuditPage {
items: SkinSiteWikiAuditInfo[]
nextCursor?: number | null
}
export function skinSiteWikiPage(path: string): Promise<SkinSiteResponse<SkinSiteWikiPageDetail>> {
return skinSiteRequest<SkinSiteWikiPageDetail>('wiki/page?path=' + encodeURIComponent(path))
}
export function skinSiteWikiProtectPage(
path: string,
readPolicy: SkinSiteWikiReadPolicy,
editPolicy: SkinSiteWikiEditPolicy,
): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>('wiki/admin/protect', {
method: 'POST',
body: { path, readPolicy, editPolicy },
})
}
export function skinSiteWikiSetHomePage(path: string): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>('wiki/admin/homepage', { method: 'POST', body: { path } })
}
export function skinSiteWikiSetPageDeleted(
path: string,
deleted: boolean,
): Promise<SkinSiteResponse<string>> {
return skinSiteRequest<string>('wiki/admin/delete', {
method: 'POST',
body: { path, deleted },
})
}
export function skinSiteWikiAudit(
before?: number,
): Promise<SkinSiteResponse<SkinSiteWikiAuditPage>> {
const suffix = before ? '?before=' + before : ''
return skinSiteRequest<SkinSiteWikiAuditPage>('wiki/admin/audit' + suffix)
}
export function skinSiteWikiSettings(): Promise<SkinSiteResponse<SkinSiteWikiSettings>> {
return skinSiteRequest<SkinSiteWikiSettings>('wiki/settings')
}
export function skinSiteWikiUpdateSettings(
settings: Partial<SkinSiteWikiSettings>,
): Promise<SkinSiteResponse<SkinSiteWikiSettings>> {
return skinSiteRequest<SkinSiteWikiSettings>('wiki/admin/settings', {
method: 'POST',
body: settings,
})
}
+33 -1
View File
@@ -12109,5 +12109,37 @@
"app.skin-site.home.nav.map": {"message": "Server map"},
"app.skin-site.home.nav.map-desc": {"message": "Explore the live world map."},
"app.skin-site.home.nav.profile": {"message": "Profile"},
"app.skin-site.home.nav.profile-desc": {"message": "Manage your account and textures."}
"app.skin-site.home.nav.profile-desc": {"message": "Manage your account and textures."},
"app.skin-site.admin.wiki.title": {"message": "Wiki"},
"app.skin-site.admin.wiki.tab-page": {"message": "Page"},
"app.skin-site.admin.wiki.tab-audit": {"message": "Audit log"},
"app.skin-site.admin.wiki.tab-settings": {"message": "Settings"},
"app.skin-site.admin.wiki.loading": {"message": "Loading…"},
"app.skin-site.admin.wiki.page-path": {"message": "Page path"},
"app.skin-site.admin.wiki.load-page": {"message": "Load page"},
"app.skin-site.admin.wiki.page-not-found": {"message": "Page not found."},
"app.skin-site.admin.wiki.revision": {"message": "Revision"},
"app.skin-site.admin.wiki.author": {"message": "Author"},
"app.skin-site.admin.wiki.updated-at": {"message": "Updated"},
"app.skin-site.admin.wiki.categories": {"message": "Categories"},
"app.skin-site.admin.wiki.read-policy": {"message": "Read policy"},
"app.skin-site.admin.wiki.edit-policy": {"message": "Edit policy"},
"app.skin-site.admin.wiki.save-protect": {"message": "Save policies"},
"app.skin-site.admin.wiki.protect-saved": {"message": "Page policies updated."},
"app.skin-site.admin.wiki.set-home": {"message": "Set as home page"},
"app.skin-site.admin.wiki.home-set": {"message": "Home page updated."},
"app.skin-site.admin.wiki.set-deleted": {"message": "Mark as deleted"},
"app.skin-site.admin.wiki.set-restored": {"message": "Restore page"},
"app.skin-site.admin.wiki.deleted-updated": {"message": "Page deletion state updated."},
"app.skin-site.admin.wiki.audit-empty": {"message": "No audit entries."},
"app.skin-site.admin.wiki.audit-target": {"message": "Target"},
"app.skin-site.admin.wiki.load-more": {"message": "Load more"},
"app.skin-site.admin.wiki.settings-hint": {"message": "Comma separated domains."},
"app.skin-site.admin.wiki.domains": {"message": "Domains"},
"app.skin-site.admin.wiki.home-page": {"message": "Home page"},
"app.skin-site.admin.wiki.default-skin": {"message": "Default skin"},
"app.skin-site.admin.wiki.max-page-bytes": {"message": "Max page bytes"},
"app.skin-site.admin.wiki.max-attachment-bytes": {"message": "Max attachment bytes"},
"app.skin-site.admin.wiki.save-settings": {"message": "Save settings"},
"app.skin-site.admin.wiki.settings-saved": {"message": "Settings saved."}
}
+33 -1
View File
@@ -11940,5 +11940,37 @@
"app.skin-site.home.nav.map": {"message": "服务器地图"},
"app.skin-site.home.nav.map-desc": {"message": "探索实时世界地图。"},
"app.skin-site.home.nav.profile": {"message": "个人资料"},
"app.skin-site.home.nav.profile-desc": {"message": "管理你的账号与皮肤。"}
"app.skin-site.home.nav.profile-desc": {"message": "管理你的账号与皮肤。"},
"app.skin-site.admin.wiki.title": {"message": "维基管理"},
"app.skin-site.admin.wiki.tab-page": {"message": "页面"},
"app.skin-site.admin.wiki.tab-audit": {"message": "审计日志"},
"app.skin-site.admin.wiki.tab-settings": {"message": "设置"},
"app.skin-site.admin.wiki.loading": {"message": "加载中…"},
"app.skin-site.admin.wiki.page-path": {"message": "页面路径"},
"app.skin-site.admin.wiki.load-page": {"message": "加载页面"},
"app.skin-site.admin.wiki.page-not-found": {"message": "未找到该页面。"},
"app.skin-site.admin.wiki.revision": {"message": "版本"},
"app.skin-site.admin.wiki.author": {"message": "作者"},
"app.skin-site.admin.wiki.updated-at": {"message": "更新时间"},
"app.skin-site.admin.wiki.categories": {"message": "分类"},
"app.skin-site.admin.wiki.read-policy": {"message": "阅读权限"},
"app.skin-site.admin.wiki.edit-policy": {"message": "编辑权限"},
"app.skin-site.admin.wiki.save-protect": {"message": "保存权限"},
"app.skin-site.admin.wiki.protect-saved": {"message": "页面权限已更新。"},
"app.skin-site.admin.wiki.set-home": {"message": "设为首页"},
"app.skin-site.admin.wiki.home-set": {"message": "首页已更新。"},
"app.skin-site.admin.wiki.set-deleted": {"message": "标记为已删除"},
"app.skin-site.admin.wiki.set-restored": {"message": "恢复页面"},
"app.skin-site.admin.wiki.deleted-updated": {"message": "页面删除状态已更新。"},
"app.skin-site.admin.wiki.audit-empty": {"message": "没有审计记录。"},
"app.skin-site.admin.wiki.audit-target": {"message": "目标"},
"app.skin-site.admin.wiki.load-more": {"message": "加载更多"},
"app.skin-site.admin.wiki.settings-hint": {"message": "以逗号分隔域名。"},
"app.skin-site.admin.wiki.domains": {"message": "域名"},
"app.skin-site.admin.wiki.home-page": {"message": "首页"},
"app.skin-site.admin.wiki.default-skin": {"message": "默认皮肤"},
"app.skin-site.admin.wiki.max-page-bytes": {"message": "页面最大字节数"},
"app.skin-site.admin.wiki.max-attachment-bytes": {"message": "附件最大字节数"},
"app.skin-site.admin.wiki.save-settings": {"message": "保存设置"},
"app.skin-site.admin.wiki.settings-saved": {"message": "设置已保存。"}
}