- API 层补充地图服务器列表接口。 - SkinSiteMapView:内外服切换、选择子服与地图,用内嵌 iframe 展示, 并提供在浏览器中打开的入口。 - 主页导航卡的「地图」改为打开该视图。
This commit is contained in:
1 parent
77b381b8d0
commit
8cc4782497
4 files changed
+165
-3
No files matched your search
@@ -8,6 +8,7 @@ import SkinSiteAnnouncements from './SkinSiteAnnouncements.vue'
|
||||
import SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
||||
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
||||
import SkinSiteLoginModal from './SkinSiteLoginModal.vue'
|
||||
import SkinSiteMapView from './SkinSiteMapView.vue'
|
||||
import SkinSiteNavCards from './SkinSiteNavCards.vue'
|
||||
import SkinSiteProfileModal from './SkinSiteProfileModal.vue'
|
||||
import SkinSiteProfileView from './SkinSiteProfileView.vue'
|
||||
@@ -28,7 +29,7 @@ const messages = defineMessages({
|
||||
const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
|
||||
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
|
||||
const profileModal = ref<InstanceType<typeof SkinSiteProfileModal>>()
|
||||
const view = ref<'home' | 'players' | 'downloads' | 'review' | 'bugs'>('home')
|
||||
const view = ref<'home' | 'players' | 'downloads' | 'map' | 'review' | 'bugs'>('home')
|
||||
|
||||
async function copyAddress() {
|
||||
try {
|
||||
@@ -45,6 +46,7 @@ async function copyAddress() {
|
||||
<div class="mx-auto flex w-full max-w-5xl flex-col gap-6 p-6">
|
||||
<SkinSiteProfileView v-if="view === 'players'" @back="view = 'home'" />
|
||||
<SkinSiteDownloadsView v-else-if="view === 'downloads'" @back="view = 'home'" />
|
||||
<SkinSiteMapView v-else-if="view === 'map'" @back="view = 'home'" />
|
||||
|
||||
<template v-else>
|
||||
<header class="flex flex-col items-center gap-1 text-center">
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { ExternalIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { type SkinSiteMapServer,skinSiteMapServers } from '@/helpers/skin-site-api'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'app.skin-site.map.title', defaultMessage: 'Server map' },
|
||||
back: { id: 'app.skin-site.map.back', defaultMessage: 'Back to home' },
|
||||
internal: { id: 'app.skin-site.map.internal', defaultMessage: 'Internal' },
|
||||
external: { id: 'app.skin-site.map.external', defaultMessage: 'External' },
|
||||
server: { id: 'app.skin-site.map.server', defaultMessage: 'Server' },
|
||||
map: { id: 'app.skin-site.map.map', defaultMessage: 'Map' },
|
||||
openExternal: { id: 'app.skin-site.map.open', defaultMessage: 'Open in browser' },
|
||||
loading: { id: 'app.skin-site.map.loading', defaultMessage: 'Loading maps…' },
|
||||
empty: { id: 'app.skin-site.map.empty', defaultMessage: 'No maps available.' },
|
||||
failed: { id: 'app.skin-site.map.failed', defaultMessage: 'Could not load maps.' },
|
||||
})
|
||||
|
||||
const emit = defineEmits<{ (e: 'back'): void }>()
|
||||
|
||||
const servers = ref<SkinSiteMapServer[]>([])
|
||||
const internal = ref(false)
|
||||
const serverIndex = ref(0)
|
||||
const mapIndex = ref(0)
|
||||
const loading = ref(true)
|
||||
const failed = ref(false)
|
||||
|
||||
const filteredServers = computed(() => servers.value.filter((s) => s.internal === internal.value))
|
||||
const selectedServer = computed(() => filteredServers.value[serverIndex.value])
|
||||
const selectedMap = computed(() => selectedServer.value?.maps[mapIndex.value])
|
||||
|
||||
watch(internal, () => {
|
||||
serverIndex.value = 0
|
||||
mapIndex.value = 0
|
||||
})
|
||||
watch(serverIndex, () => {
|
||||
mapIndex.value = 0
|
||||
})
|
||||
|
||||
async function openExternal() {
|
||||
if (!selectedMap.value) return
|
||||
const { openUrl } = await import('@tauri-apps/plugin-opener')
|
||||
await openUrl(selectedMap.value.url)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
failed.value = false
|
||||
try {
|
||||
const result = await skinSiteMapServers()
|
||||
if (!result.success) throw new Error(result.errorMessage)
|
||||
servers.value = result.payload ?? []
|
||||
if (!servers.value.some((s) => !s.internal)) internal.value = true
|
||||
} catch (error) {
|
||||
failed.value = true
|
||||
handleError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex h-full min-h-0 flex-col gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1 rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="emit('back')"
|
||||
>
|
||||
← {{ formatMessage(messages.back) }}
|
||||
</button>
|
||||
<h1 class="m-0 text-lg font-bold text-contrast">{{ formatMessage(messages.title) }}</h1>
|
||||
|
||||
<select
|
||||
v-model="internal"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||
>
|
||||
<option :value="false">{{ formatMessage(messages.external) }}</option>
|
||||
<option :value="true">{{ formatMessage(messages.internal) }}</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-if="filteredServers.length"
|
||||
v-model.number="serverIndex"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||
>
|
||||
<option v-for="(server, index) in filteredServers" :key="index" :value="index">
|
||||
{{ server.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
v-if="selectedServer?.maps.length"
|
||||
v-model.number="mapIndex"
|
||||
class="rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast"
|
||||
>
|
||||
<option v-for="(map, index) in selectedServer.maps" :key="index" :value="index">
|
||||
{{ map.name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
v-if="selectedMap"
|
||||
type="button"
|
||||
class="ml-auto flex items-center gap-1 rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||
@click="openExternal"
|
||||
>
|
||||
<ExternalIcon class="size-3.5" />
|
||||
{{ formatMessage(messages.openExternal) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex flex-1 items-center justify-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 flex-1 text-center text-sm text-secondary">
|
||||
{{ formatMessage(messages.failed) }}
|
||||
</p>
|
||||
<p v-else-if="!selectedMap" class="m-0 flex-1 text-center text-sm text-secondary">
|
||||
{{ formatMessage(messages.empty) }}
|
||||
</p>
|
||||
<iframe
|
||||
v-else
|
||||
:key="selectedMap.url"
|
||||
:src="selectedMap.url"
|
||||
:title="`${selectedServer?.name} · ${selectedMap.name}`"
|
||||
class="min-h-0 w-full flex-1 rounded-lg border border-solid border-surface-5"
|
||||
allow="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,7 +12,7 @@ import type { Component } from 'vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{ (e: 'navigate', view: 'downloads' | 'review' | 'bugs'): void }>()
|
||||
const emit = defineEmits<{ (e: 'navigate', view: 'downloads' | 'review' | 'bugs' | 'map'): void }>()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'app.skin-site.home.nav.title', defaultMessage: 'Skin site' },
|
||||
@@ -104,7 +104,7 @@ const items: NavItem[] = [
|
||||
]
|
||||
|
||||
function open(item: NavItem) {
|
||||
if (item.id === 'download' || item.id === 'review' || item.id === 'bugs') {
|
||||
if (item.id === 'download' || item.id === 'review' || item.id === 'bugs' || item.id === 'map') {
|
||||
emit('navigate', item.id === 'download' ? 'downloads' : item.id)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -532,3 +532,21 @@ export function skinSiteDownloadCategories(): Promise<
|
||||
> {
|
||||
return skinSiteRequest<SkinSiteDownloadCategory[]>('download/category/list', { auth: false })
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Maps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface SkinSiteMapEntry {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface SkinSiteMapServer {
|
||||
name: string
|
||||
internal: boolean
|
||||
maps: SkinSiteMapEntry[]
|
||||
}
|
||||
|
||||
export function skinSiteMapServers(): Promise<SkinSiteResponse<SkinSiteMapServer[]>> {
|
||||
return skinSiteRequest<SkinSiteMapServer[]>('maps', { auth: false })
|
||||
}
|
||||
Reference in new issue
Block a user