- 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 SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
||||||
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
import SkinSiteDownloadsView from './SkinSiteDownloadsView.vue'
|
||||||
import SkinSiteLoginModal from './SkinSiteLoginModal.vue'
|
import SkinSiteLoginModal from './SkinSiteLoginModal.vue'
|
||||||
|
import SkinSiteMapView from './SkinSiteMapView.vue'
|
||||||
import SkinSiteNavCards from './SkinSiteNavCards.vue'
|
import SkinSiteNavCards from './SkinSiteNavCards.vue'
|
||||||
import SkinSiteProfileModal from './SkinSiteProfileModal.vue'
|
import SkinSiteProfileModal from './SkinSiteProfileModal.vue'
|
||||||
import SkinSiteProfileView from './SkinSiteProfileView.vue'
|
import SkinSiteProfileView from './SkinSiteProfileView.vue'
|
||||||
@@ -28,7 +29,7 @@ const messages = defineMessages({
|
|||||||
const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
|
const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
|
||||||
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
|
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
|
||||||
const profileModal = ref<InstanceType<typeof SkinSiteProfileModal>>()
|
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() {
|
async function copyAddress() {
|
||||||
try {
|
try {
|
||||||
@@ -45,6 +46,7 @@ async function copyAddress() {
|
|||||||
<div class="mx-auto flex w-full max-w-5xl flex-col gap-6 p-6">
|
<div class="mx-auto flex w-full max-w-5xl flex-col gap-6 p-6">
|
||||||
<SkinSiteProfileView v-if="view === 'players'" @back="view = 'home'" />
|
<SkinSiteProfileView v-if="view === 'players'" @back="view = 'home'" />
|
||||||
<SkinSiteDownloadsView v-else-if="view === 'downloads'" @back="view = 'home'" />
|
<SkinSiteDownloadsView v-else-if="view === 'downloads'" @back="view = 'home'" />
|
||||||
|
<SkinSiteMapView v-else-if="view === 'map'" @back="view = 'home'" />
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<header class="flex flex-col items-center gap-1 text-center">
|
<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 { 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({
|
const messages = defineMessages({
|
||||||
title: { id: 'app.skin-site.home.nav.title', defaultMessage: 'Skin site' },
|
title: { id: 'app.skin-site.home.nav.title', defaultMessage: 'Skin site' },
|
||||||
@@ -104,7 +104,7 @@ const items: NavItem[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
function open(item: 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)
|
emit('navigate', item.id === 'download' ? 'downloads' : item.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,3 +532,21 @@ export function skinSiteDownloadCategories(): Promise<
|
|||||||
> {
|
> {
|
||||||
return skinSiteRequest<SkinSiteDownloadCategory[]>('download/category/list', { auth: false })
|
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