diff --git a/apps/app-frontend/src/components/ui/skin-site/WikiView.vue b/apps/app-frontend/src/components/ui/skin-site/WikiView.vue
index 4ad2544..7265f76 100644
--- a/apps/app-frontend/src/components/ui/skin-site/WikiView.vue
+++ b/apps/app-frontend/src/components/ui/skin-site/WikiView.vue
@@ -6,12 +6,20 @@ import { computed, onMounted, ref } from 'vue'
import {
type SkinSiteWikiPageDetail,
skinSiteWikiPage,
+ skinSiteWikiDiff,
+ skinSiteWikiHistory,
+ skinSiteWikiRevert,
+ skinSiteWikiRevision,
+ type SkinSiteWikiDiffLine,
+ type SkinSiteWikiNode,
+ type SkinSiteWikiRevisionInfo,
} from '@/helpers/skin-site-api'
import WikiNodeRenderer from './wiki/WikiNodeRenderer.vue'
+import WikiBrowser from './wiki/WikiBrowser.vue'
const { formatMessage } = useVIntl()
-const { handleError } = injectNotificationManager()
+const { handleError, addNotification } = injectNotificationManager()
const props = withDefaults(defineProps<{ initialTitle?: string }>(), {
initialTitle: '',
@@ -40,13 +48,50 @@ const messages = defineMessages({
revision: { id: 'app.skin-site.wiki.revision', defaultMessage: 'Revision #{id}' },
categories: { id: 'app.skin-site.wiki.categories', defaultMessage: 'Categories' },
back: { id: 'app.skin-site.wiki.back', defaultMessage: 'Back' },
+ history: { id: 'app.skin-site.wiki.history', defaultMessage: 'History' },
+ read: { id: 'app.skin-site.wiki.read', defaultMessage: 'Read' },
+ historyEmpty: {
+ id: 'app.skin-site.wiki.history-empty',
+ defaultMessage: 'No revisions yet.',
+ },
+ noSummary: {
+ id: 'app.skin-site.wiki.no-summary',
+ defaultMessage: 'No edit summary',
+ },
+ minor: { id: 'app.skin-site.wiki.minor', defaultMessage: 'Minor' },
+ viewRevision: { id: 'app.skin-site.wiki.view-revision', defaultMessage: 'View' },
+ revertTo: {
+ id: 'app.skin-site.wiki.revert-to',
+ defaultMessage: 'Revert to this',
+ },
+ confirmRevert: {
+ id: 'app.skin-site.wiki.confirm-revert',
+ defaultMessage: 'Confirm revert',
+ },
+ revertDone: {
+ id: 'app.skin-site.wiki.revert-done',
+ defaultMessage: 'Page reverted to the selected revision.',
+ },
+ revisionPreview: {
+ id: 'app.skin-site.wiki.revision-preview',
+ defaultMessage: 'Preview of revision #{id}',
+ },
+ closePreview: {
+ id: 'app.skin-site.wiki.close-preview',
+ defaultMessage: 'Close preview',
+ },
+ diffTitle: {
+ id: 'app.skin-site.wiki.diff-title',
+ defaultMessage: 'Line diff against the current revision',
+ },
+ cancel: { id: 'app.skin-site.wiki.cancel', defaultMessage: 'Cancel' },
})
const titleInput = ref(props.initialTitle)
const page = ref
(null)
const loading = ref(false)
const notFound = ref(false)
-const history = ref([])
+const navStack = ref([])
const currentTitle = computed(() => page.value?.title ?? '')
@@ -64,8 +109,8 @@ async function openPage(title: string) {
return
}
page.value = res.payload
- if (currentTitle.value && history.value[history.value.length - 1] !== currentTitle.value) {
- history.value.push(currentTitle.value)
+ if (currentTitle.value && navStack.value[navStack.value.length - 1] !== currentTitle.value) {
+ navStack.value.push(currentTitle.value)
}
} catch (error) {
handleError(error)
@@ -77,7 +122,7 @@ async function openPage(title: string) {
}
function goBack() {
- const prev = history.value.pop()
+ const prev = navStack.value.pop()
if (prev) void openPage(prev)
}
@@ -89,6 +134,77 @@ function formatDate(ts: number): string {
}
}
+const mode = ref<'read' | 'history'>('read')
+const history = ref([])
+const historyLoading = ref(false)
+
+const selectedRevisionId = ref(null)
+const selectedRevisionNodes = ref(null)
+const diffLines = ref([])
+const pendingRevertId = ref(null)
+const reverting = ref(false)
+
+async function showHistory() {
+ if (!page.value) return
+ mode.value = 'history'
+ historyLoading.value = true
+ try {
+ const res = await skinSiteWikiHistory(page.value.title)
+ if (!res.success) throw new Error(res.errorMessage)
+ history.value = res.payload.items ?? []
+ } catch (error) {
+ handleError(error)
+ history.value = []
+ } finally {
+ historyLoading.value = false
+ }
+}
+
+function backToRead() {
+ mode.value = 'read'
+ selectedRevisionNodes.value = null
+ diffLines.value = []
+ pendingRevertId.value = null
+}
+
+async function viewRevision(revisionId: number) {
+ if (!page.value) return
+ try {
+ const res = await skinSiteWikiRevision(page.value.title, revisionId)
+ if (!res.success) throw new Error(res.errorMessage)
+ selectedRevisionId.value = revisionId
+ selectedRevisionNodes.value = res.payload.nodes ?? []
+ diffLines.value = []
+ if (revisionId !== page.value.revisionId) {
+ const diff = await skinSiteWikiDiff(page.value.title, revisionId, page.value.revisionId)
+ if (diff.success) diffLines.value = diff.payload ?? []
+ }
+ } catch (error) {
+ handleError(error)
+ }
+}
+
+async function revertToRevision(revisionId: number) {
+ if (!page.value || reverting.value) return
+ reverting.value = true
+ try {
+ const res = await skinSiteWikiRevert({
+ title: page.value.title,
+ revisionId,
+ baseRevisionId: page.value.revisionId,
+ })
+ if (!res.success) throw new Error(res.errorMessage)
+ page.value = res.payload
+ pendingRevertId.value = null
+ backToRead()
+ addNotification({ type: 'success', title: formatMessage(messages.revertDone) })
+ } catch (error) {
+ handleError(error)
+ } finally {
+ reverting.value = false
+ }
+}
+
function onNavigate(title: string) {
if (title) void openPage(title)
}
@@ -102,7 +218,7 @@ onMounted(() => {
+
+
+
+
+
+
{{ formatMessage(messages.loading) }}
@@ -142,12 +278,59 @@ onMounted(() => {
>
{{ formatMessage(messages.empty) }}
-
+
{{ page.title }}
-
- {{ formatMessage(messages.revision, { id: page.revisionId }) }}
-
+
+
+ {{ formatMessage(messages.revision, { id: page.revisionId }) }}
+
+
+
+
+
+
+
+
+
+
@@ -188,5 +371,123 @@ onMounted(() => {
+
+
+
+
+ {{ formatMessage(messages.loading) }}
+
+
+
+
+
+ {{ formatMessage(messages.revisionPreview, { id: selectedRevisionId }) }}
+
+
+
+
+
+
+
+
+ {{ formatMessage(messages.diffTitle) }}
+
+
+
+ {{ line.type === 'ADDED' ? '+' : line.type === 'REMOVED' ? '-' : ' ' }}{{ line.text }}
+
+
+
+
+
+
+ {{ formatMessage(messages.historyEmpty) }}
+
+
+
+
+
+ {{ revision.authorName }}
+
+ {{ formatMessage(messages.minor) }}
+
+ #{{ revision.id }}
+
+
+ {{ revision.summary || formatMessage(messages.noSummary) }}
+
+
{{ formatDate(revision.createdAt) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/app-frontend/src/components/ui/skin-site/admin/AdminBroadcastPage.vue b/apps/app-frontend/src/components/ui/skin-site/admin/AdminBroadcastPage.vue
index 0a486c9..6e0ce91 100644
--- a/apps/app-frontend/src/components/ui/skin-site/admin/AdminBroadcastPage.vue
+++ b/apps/app-frontend/src/components/ui/skin-site/admin/AdminBroadcastPage.vue
@@ -4,8 +4,10 @@ import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/u
import { computed, onMounted, ref } from 'vue'
import {
+ type SkinSiteAdminUser,
type SkinSiteBroadcast,
type SkinSiteBroadcastInput,
+ skinSiteAdminUserList,
skinSiteBroadcasts,
skinSiteCreateBroadcast,
skinSiteDeleteBroadcast,
@@ -44,6 +46,7 @@ const messages = defineMessages({
})
const items = ref([])
+const uploaderNames = ref>({})
const loading = ref(true)
const failed = ref(false)
const saving = ref(false)
@@ -154,11 +157,28 @@ const canSave = computed(
() => form.value.title.trim().length > 0 && form.value.content.length > 0 && !saving.value,
)
+function uploaderName(uuid: string): string {
+ return uploaderNames.value[uuid] ?? uuid
+}
+
+async function loadUploaderNames() {
+ try {
+ const res = await skinSiteAdminUserList({ limit: 200, offset: 0 })
+ if (!res.success) return
+ const users: SkinSiteAdminUser[] = res.payload ?? []
+ const map: Record = {}
+ for (const u of users) map[u.uuid] = u.username
+ uploaderNames.value = map
+ } catch {
+ // 名称映射失败不影响公告列表展示
+ }
+}
+
async function load() {
loading.value = true
failed.value = false
try {
- const result = await skinSiteBroadcasts()
+ const [result] = await Promise.all([skinSiteBroadcasts(), loadUploaderNames()])
if (!result.success) throw new Error(result.errorMessage)
items.value = result.payload ?? []
} catch (error) {
@@ -248,7 +268,7 @@ onMounted(() => void load())