1 parent
57ff974d79
commit
b0178c046f
5 files changed
+505
-3
No files matched your search
@@ -6,6 +6,7 @@ import { skinSiteUser } from '@/composables/skin-site-session'
|
||||
|
||||
import AdminAccessAuditPage from './admin/AdminAccessAuditPage.vue'
|
||||
import AdminBroadcastPage from './admin/AdminBroadcastPage.vue'
|
||||
import AdminDonationsPage from './admin/AdminDonationsPage.vue'
|
||||
import AdminDownloadsPage from './admin/AdminDownloadsPage.vue'
|
||||
import AdminLauncherPage from './admin/AdminLauncherPage.vue'
|
||||
import AdminPlayersPage from './admin/AdminPlayersPage.vue'
|
||||
@@ -98,7 +99,9 @@ const navGroups = computed<NavGroup[]>(() => [
|
||||
{ key: 'broadcast', label: formatMessage(messages.itemBroadcast) },
|
||||
{ key: 'chat', label: formatMessage(messages.itemChat) },
|
||||
{ key: 'wiki', label: formatMessage(messages.itemWiki) },
|
||||
{ key: 'donations', label: formatMessage(messages.itemDonations) },
|
||||
...(hasWebAdmin.value
|
||||
? [{ key: 'donations' as AdminKey, label: formatMessage(messages.itemDonations) }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -177,6 +180,7 @@ const current = ref<AdminKey>('summary')
|
||||
<AdminReviewPage v-else-if="current === 'review'" />
|
||||
<AdminLauncherPage v-else-if="current === 'launcher'" />
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<script setup lang="ts">
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
skinSiteAdminDonations,
|
||||
skinSiteCreateDonation,
|
||||
skinSiteDeleteDonation,
|
||||
type SkinSiteDonationAdminEntry,
|
||||
type SkinSiteDonationSummary,
|
||||
skinSiteUpdateDonation,
|
||||
} from '@/helpers/skin-site-api'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError, addNotification } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'app.skin-site.admin.donations.title', defaultMessage: 'Donations' },
|
||||
loading: { id: 'app.skin-site.admin.donations.loading', defaultMessage: 'Loading…' },
|
||||
failed: {
|
||||
id: 'app.skin-site.admin.donations.failed',
|
||||
defaultMessage: 'Could not load donations.',
|
||||
},
|
||||
empty: { id: 'app.skin-site.admin.donations.empty', defaultMessage: 'No donations yet.' },
|
||||
totalAmount: { id: 'app.skin-site.admin.donations.total-amount', defaultMessage: 'Total' },
|
||||
donorCount: { id: 'app.skin-site.admin.donations.donor-count', defaultMessage: 'Donors' },
|
||||
donor: { id: 'app.skin-site.admin.donations.donor', defaultMessage: 'Donor' },
|
||||
amount: { id: 'app.skin-site.admin.donations.amount', defaultMessage: 'Amount' },
|
||||
updatedAt: { id: 'app.skin-site.admin.donations.updated-at', defaultMessage: 'Updated' },
|
||||
external: { id: 'app.skin-site.admin.donations.external', defaultMessage: 'External' },
|
||||
linked: { id: 'app.skin-site.admin.donations.linked', defaultMessage: 'Linked' },
|
||||
create: { id: 'app.skin-site.admin.donations.create', defaultMessage: 'Add donation' },
|
||||
save: { id: 'app.skin-site.admin.donations.save', defaultMessage: 'Save' },
|
||||
cancel: { id: 'app.skin-site.admin.donations.cancel', defaultMessage: 'Cancel' },
|
||||
edit: { id: 'app.skin-site.admin.donations.edit', defaultMessage: 'Edit' },
|
||||
remove: { id: 'app.skin-site.admin.donations.remove', defaultMessage: 'Delete' },
|
||||
amountField: { id: 'app.skin-site.admin.donations.amount-field', defaultMessage: 'Amount' },
|
||||
displayName: {
|
||||
id: 'app.skin-site.admin.donations.display-name',
|
||||
defaultMessage: 'Display name',
|
||||
},
|
||||
userUuid: { id: 'app.skin-site.admin.donations.user-uuid', defaultMessage: 'User UUID' },
|
||||
qq: { id: 'app.skin-site.admin.donations.qq', defaultMessage: 'QQ' },
|
||||
appendMode: {
|
||||
id: 'app.skin-site.admin.donations.append-mode',
|
||||
defaultMessage: 'Append to existing amount',
|
||||
},
|
||||
created: { id: 'app.skin-site.admin.donations.created', defaultMessage: 'Donation created.' },
|
||||
updated: { id: 'app.skin-site.admin.donations.updated', defaultMessage: 'Donation updated.' },
|
||||
deleted: { id: 'app.skin-site.admin.donations.deleted', defaultMessage: 'Donation deleted.' },
|
||||
confirmDelete: {
|
||||
id: 'app.skin-site.admin.donations.confirm-delete',
|
||||
defaultMessage: 'Delete the donation from “{name}”? This cannot be undone.',
|
||||
},
|
||||
loadMore: { id: 'app.skin-site.admin.donations.load-more', defaultMessage: 'Load more' },
|
||||
})
|
||||
|
||||
const items = ref<SkinSiteDonationAdminEntry[]>([])
|
||||
const summary = ref<SkinSiteDonationSummary | null>(null)
|
||||
const hasMore = ref(false)
|
||||
const loading = ref(true)
|
||||
const loadingMore = ref(false)
|
||||
const failed = ref(false)
|
||||
const offset = ref(0)
|
||||
|
||||
const editorOpen = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const editingVersion = ref(0)
|
||||
const saving = ref(false)
|
||||
const deletingId = ref<number | null>(null)
|
||||
|
||||
const form = ref({
|
||||
amount: '',
|
||||
displayName: '',
|
||||
userUuid: '',
|
||||
qq: '',
|
||||
appendMode: false,
|
||||
})
|
||||
|
||||
const canSave = computed(() => form.value.amount.trim().length > 0 && !saving.value)
|
||||
|
||||
function formatDateTime(ts: number): string {
|
||||
try {
|
||||
return new Date(ts).toLocaleString()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = { amount: '', displayName: '', userUuid: '', qq: '', appendMode: false }
|
||||
editingId.value = null
|
||||
editingVersion.value = 0
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
resetForm()
|
||||
editorOpen.value = true
|
||||
}
|
||||
|
||||
function openEdit(entry: SkinSiteDonationAdminEntry) {
|
||||
form.value = {
|
||||
amount: entry.donor.amount,
|
||||
displayName: entry.donor.username,
|
||||
userUuid: entry.userUuid ?? '',
|
||||
qq: entry.qq ?? '',
|
||||
appendMode: false,
|
||||
}
|
||||
editingId.value = entry.donor.id
|
||||
editingVersion.value = entry.version
|
||||
editorOpen.value = true
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
editorOpen.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
async function load(reset = true) {
|
||||
if (reset) {
|
||||
loading.value = true
|
||||
items.value = []
|
||||
offset.value = 0
|
||||
} else {
|
||||
loadingMore.value = true
|
||||
}
|
||||
failed.value = false
|
||||
try {
|
||||
const result = await skinSiteAdminDonations(offset.value)
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
const page = result.payload
|
||||
items.value = reset ? page.items : [...items.value, ...page.items]
|
||||
summary.value = page.summary
|
||||
hasMore.value = page.hasMore
|
||||
} catch (error) {
|
||||
failed.value = true
|
||||
handleError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (!hasMore.value) return
|
||||
offset.value += 20
|
||||
await load(false)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!canSave.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingId.value === null) {
|
||||
const payload: Parameters<typeof skinSiteCreateDonation>[0] = {
|
||||
amount: form.value.amount.trim(),
|
||||
}
|
||||
if (form.value.userUuid.trim()) payload.userUuid = form.value.userUuid.trim()
|
||||
if (form.value.qq.trim()) payload.qq = form.value.qq.trim()
|
||||
if (form.value.displayName.trim()) payload.displayName = form.value.displayName.trim()
|
||||
const result = await skinSiteCreateDonation(payload)
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
addNotification({ type: 'success', title: formatMessage(messages.created) })
|
||||
} else {
|
||||
const result = await skinSiteUpdateDonation(editingId.value, {
|
||||
amount: form.value.amount.trim(),
|
||||
version: editingVersion.value,
|
||||
displayName: form.value.displayName.trim() || undefined,
|
||||
mode: form.value.appendMode ? 'APPEND' : 'REPLACE',
|
||||
})
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
addNotification({ type: 'success', title: formatMessage(messages.updated) })
|
||||
}
|
||||
closeEditor()
|
||||
await load(true)
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(entry: SkinSiteDonationAdminEntry) {
|
||||
if (deletingId.value !== null) return
|
||||
if (!window.confirm(formatMessage(messages.confirmDelete, { name: entry.donor.username }))) return
|
||||
deletingId.value = entry.donor.id
|
||||
try {
|
||||
const result = await skinSiteDeleteDonation(entry.donor.id, entry.version)
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
addNotification({ type: 'success', title: formatMessage(messages.deleted) })
|
||||
await load(true)
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
deletingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => void load(true))
|
||||
</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="ml-auto rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110"
|
||||
@click="openCreate"
|
||||
>
|
||||
{{ formatMessage(messages.create) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="summary" class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div class="flex flex-col gap-1 rounded-xl bg-bg-raised p-4">
|
||||
<span class="text-xs text-secondary">{{ formatMessage(messages.totalAmount) }}</span>
|
||||
<span class="text-xl font-bold text-contrast">{{ summary.totalAmount }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 rounded-xl bg-bg-raised p-4">
|
||||
<span class="text-xs text-secondary">{{ formatMessage(messages.donorCount) }}</span>
|
||||
<span class="text-xl font-bold text-contrast">{{ summary.donorCount }}</span>
|
||||
</div>
|
||||
</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="entry in items"
|
||||
:key="entry.donor.id"
|
||||
class="flex flex-wrap items-center gap-3 rounded-xl bg-bg-raised p-3"
|
||||
>
|
||||
<span class="shrink-0 text-xs text-secondary">#{{ entry.donor.rank }}</span>
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<span class="truncate text-sm font-semibold text-contrast">
|
||||
{{ entry.donor.username }}
|
||||
</span>
|
||||
<span class="truncate text-xs text-secondary">
|
||||
{{ formatMessage(messages.updatedAt) }}:
|
||||
{{ formatDateTime(entry.updatedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="shrink-0 text-sm font-bold text-contrast">{{ entry.donor.amount }}</span>
|
||||
<span
|
||||
v-if="entry.external"
|
||||
class="shrink-0 rounded-full bg-button-bg px-2 py-0.5 text-[10px] font-semibold text-secondary"
|
||||
>
|
||||
{{ formatMessage(messages.external) }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="shrink-0 rounded-full bg-green px-2 py-0.5 text-[10px] font-semibold text-contrast"
|
||||
>
|
||||
{{ formatMessage(messages.linked) }}
|
||||
</span>
|
||||
<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="openEdit(entry)"
|
||||
>
|
||||
{{ 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="deletingId !== null"
|
||||
@click="remove(entry)"
|
||||
>
|
||||
{{ formatMessage(messages.remove) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="hasMore"
|
||||
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="loadingMore"
|
||||
@click="loadMore"
|
||||
>
|
||||
{{ formatMessage(messages.loadMore) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="editorOpen"
|
||||
class="flex flex-col gap-3 rounded-xl border border-solid border-surface-5 bg-bg-raised p-4"
|
||||
>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-semibold text-contrast">
|
||||
{{ formatMessage(messages.amountField) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="form.amount"
|
||||
type="text"
|
||||
inputmode="decimal"
|
||||
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-sm font-semibold text-contrast">
|
||||
{{ formatMessage(messages.displayName) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="form.displayName"
|
||||
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>
|
||||
|
||||
<template v-if="editingId === null">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-semibold text-contrast">
|
||||
{{ formatMessage(messages.userUuid) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="form.userUuid"
|
||||
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-sm font-semibold text-contrast">
|
||||
{{ formatMessage(messages.qq) }}
|
||||
</span>
|
||||
<input
|
||||
v-model="form.qq"
|
||||
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>
|
||||
</template>
|
||||
<label v-else class="flex items-center gap-2">
|
||||
<input v-model="form.appendMode" type="checkbox" />
|
||||
<span class="text-sm text-contrast">{{ formatMessage(messages.appendMode) }}</span>
|
||||
</label>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-button-bg px-4 py-2 text-sm font-semibold text-contrast hover:brightness-110"
|
||||
@click="closeEditor"
|
||||
>
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full bg-brand px-4 py-2 text-sm font-semibold text-contrast disabled:opacity-50"
|
||||
:disabled="!canSave"
|
||||
@click="save"
|
||||
>
|
||||
{{ formatMessage(messages.save) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1452,3 +1452,83 @@ export function skinSiteUpdateAccessAuditSettings(
|
||||
body: { retentionDays },
|
||||
})
|
||||
}
|
||||
|
||||
export interface SkinSiteDonationEntry {
|
||||
id: number
|
||||
rank: number
|
||||
username: string
|
||||
avatar: string | null
|
||||
amount: string
|
||||
}
|
||||
|
||||
export interface SkinSiteDonationSummary {
|
||||
totalAmount: string
|
||||
donorCount: number
|
||||
}
|
||||
|
||||
export interface SkinSiteDonationAdminEntry {
|
||||
donor: SkinSiteDonationEntry
|
||||
userUuid: string | null
|
||||
qq: string | null
|
||||
external: boolean
|
||||
version: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface SkinSiteDonationAdminPage {
|
||||
items: SkinSiteDonationAdminEntry[]
|
||||
summary: SkinSiteDonationSummary
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
export function skinSiteAdminDonations(
|
||||
offset = 0,
|
||||
): Promise<SkinSiteResponse<SkinSiteDonationAdminPage>> {
|
||||
return skinSiteRequest<SkinSiteDonationAdminPage>('donation/admin/list?limit=20&offset=' + offset)
|
||||
}
|
||||
|
||||
export function skinSiteFindAdminDonations(identity: {
|
||||
userUuid?: string
|
||||
qq?: string
|
||||
}): Promise<SkinSiteResponse<SkinSiteDonationAdminEntry[]>> {
|
||||
const query = new URLSearchParams()
|
||||
if (identity.userUuid) query.set('userUuid', identity.userUuid)
|
||||
if (identity.qq) query.set('qq', identity.qq)
|
||||
return skinSiteRequest<SkinSiteDonationAdminEntry[]>('donation/admin/find?' + query.toString())
|
||||
}
|
||||
|
||||
export interface SkinSiteDonationCreate {
|
||||
amount: string
|
||||
userUuid?: string
|
||||
qq?: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
export function skinSiteCreateDonation(
|
||||
data: SkinSiteDonationCreate,
|
||||
): Promise<SkinSiteResponse<number>> {
|
||||
return skinSiteRequest<number>('donation/admin', { method: 'POST', body: data })
|
||||
}
|
||||
|
||||
export interface SkinSiteDonationUpdate {
|
||||
amount: string
|
||||
version: number
|
||||
displayName?: string
|
||||
mode?: 'REPLACE' | 'APPEND'
|
||||
}
|
||||
|
||||
export function skinSiteUpdateDonation(
|
||||
id: number,
|
||||
data: SkinSiteDonationUpdate,
|
||||
): Promise<SkinSiteResponse<string>> {
|
||||
return skinSiteRequest<string>('donation/admin/' + id, { method: 'PUT', body: data })
|
||||
}
|
||||
|
||||
export function skinSiteDeleteDonation(
|
||||
id: number,
|
||||
version: number,
|
||||
): Promise<SkinSiteResponse<string>> {
|
||||
return skinSiteRequest<string>('donation/admin/' + id + '?version=' + version, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
@@ -12023,5 +12023,31 @@
|
||||
"app.skin-site.admin.audit.retention": {"message": "Retention (days)"},
|
||||
"app.skin-site.admin.audit.record-limit": {"message": "Record limit"},
|
||||
"app.skin-site.admin.audit.save-retention": {"message": "Save"},
|
||||
"app.skin-site.admin.audit.retention-saved": {"message": "Retention updated."}
|
||||
"app.skin-site.admin.audit.retention-saved": {"message": "Retention updated."},
|
||||
"app.skin-site.admin.donations.title": {"message": "Donations"},
|
||||
"app.skin-site.admin.donations.loading": {"message": "Loading…"},
|
||||
"app.skin-site.admin.donations.failed": {"message": "Could not load donations."},
|
||||
"app.skin-site.admin.donations.empty": {"message": "No donations yet."},
|
||||
"app.skin-site.admin.donations.total-amount": {"message": "Total"},
|
||||
"app.skin-site.admin.donations.donor-count": {"message": "Donors"},
|
||||
"app.skin-site.admin.donations.donor": {"message": "Donor"},
|
||||
"app.skin-site.admin.donations.amount": {"message": "Amount"},
|
||||
"app.skin-site.admin.donations.updated-at": {"message": "Updated"},
|
||||
"app.skin-site.admin.donations.external": {"message": "External"},
|
||||
"app.skin-site.admin.donations.linked": {"message": "Linked"},
|
||||
"app.skin-site.admin.donations.create": {"message": "Add donation"},
|
||||
"app.skin-site.admin.donations.save": {"message": "Save"},
|
||||
"app.skin-site.admin.donations.cancel": {"message": "Cancel"},
|
||||
"app.skin-site.admin.donations.edit": {"message": "Edit"},
|
||||
"app.skin-site.admin.donations.remove": {"message": "Delete"},
|
||||
"app.skin-site.admin.donations.amount-field": {"message": "Amount"},
|
||||
"app.skin-site.admin.donations.display-name": {"message": "Display name"},
|
||||
"app.skin-site.admin.donations.user-uuid": {"message": "User UUID"},
|
||||
"app.skin-site.admin.donations.qq": {"message": "QQ"},
|
||||
"app.skin-site.admin.donations.append-mode": {"message": "Append to existing amount"},
|
||||
"app.skin-site.admin.donations.created": {"message": "Donation created."},
|
||||
"app.skin-site.admin.donations.updated": {"message": "Donation updated."},
|
||||
"app.skin-site.admin.donations.deleted": {"message": "Donation deleted."},
|
||||
"app.skin-site.admin.donations.confirm-delete": {"message": "Delete the donation from “{name}”? This cannot be undone."},
|
||||
"app.skin-site.admin.donations.load-more": {"message": "Load more"}
|
||||
}
|
||||
@@ -11854,5 +11854,31 @@
|
||||
"app.skin-site.admin.audit.retention": {"message": "保留天数"},
|
||||
"app.skin-site.admin.audit.record-limit": {"message": "记录上限"},
|
||||
"app.skin-site.admin.audit.save-retention": {"message": "保存"},
|
||||
"app.skin-site.admin.audit.retention-saved": {"message": "保留天数已更新。"}
|
||||
"app.skin-site.admin.audit.retention-saved": {"message": "保留天数已更新。"},
|
||||
"app.skin-site.admin.donations.title": {"message": "捐赠管理"},
|
||||
"app.skin-site.admin.donations.loading": {"message": "加载中…"},
|
||||
"app.skin-site.admin.donations.failed": {"message": "无法加载捐赠列表。"},
|
||||
"app.skin-site.admin.donations.empty": {"message": "暂无捐赠记录。"},
|
||||
"app.skin-site.admin.donations.total-amount": {"message": "总额"},
|
||||
"app.skin-site.admin.donations.donor-count": {"message": "捐赠人数"},
|
||||
"app.skin-site.admin.donations.donor": {"message": "捐赠者"},
|
||||
"app.skin-site.admin.donations.amount": {"message": "金额"},
|
||||
"app.skin-site.admin.donations.updated-at": {"message": "更新时间"},
|
||||
"app.skin-site.admin.donations.external": {"message": "外部"},
|
||||
"app.skin-site.admin.donations.linked": {"message": "已绑定"},
|
||||
"app.skin-site.admin.donations.create": {"message": "新增捐赠"},
|
||||
"app.skin-site.admin.donations.save": {"message": "保存"},
|
||||
"app.skin-site.admin.donations.cancel": {"message": "取消"},
|
||||
"app.skin-site.admin.donations.edit": {"message": "编辑"},
|
||||
"app.skin-site.admin.donations.remove": {"message": "删除"},
|
||||
"app.skin-site.admin.donations.amount-field": {"message": "金额"},
|
||||
"app.skin-site.admin.donations.display-name": {"message": "显示名称"},
|
||||
"app.skin-site.admin.donations.user-uuid": {"message": "用户 UUID"},
|
||||
"app.skin-site.admin.donations.qq": {"message": "QQ"},
|
||||
"app.skin-site.admin.donations.append-mode": {"message": "在原有金额上累加"},
|
||||
"app.skin-site.admin.donations.created": {"message": "捐赠记录已创建。"},
|
||||
"app.skin-site.admin.donations.updated": {"message": "捐赠记录已更新。"},
|
||||
"app.skin-site.admin.donations.deleted": {"message": "捐赠记录已删除。"},
|
||||
"app.skin-site.admin.donations.confirm-delete": {"message": "确定删除「{name}」的捐赠记录吗?此操作不可撤销。"},
|
||||
"app.skin-site.admin.donations.load-more": {"message": "加载更多"}
|
||||
}
|
||||
Reference in new issue
Block a user