feat:将每日新闻改为starlight皮肤站

This commit is contained in:
2026-09-11 20:52:50 +08:00
parent 4dae11919f
commit 046eff8296

View File

@ -1,64 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import { ExternalIcon, NewspaperIcon } from '@modrinth/assets' import { ExternalIcon } from '@modrinth/assets'
import { import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
defineMessages,
injectNotificationManager,
useFormatDateTime,
useVIntl,
} from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener' import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, ref } from 'vue'
import { useNetworkStatus } from '@/composables/useNetworkStatus'
import { get_minecraft_news, type MinecraftNewsItem } from '@/helpers/mc_news'
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager() const { handleError } = injectNotificationManager()
const { offline } = useNetworkStatus()
const formatDate = useFormatDateTime({ dateStyle: 'medium' })
const messages = defineMessages({ const messages = defineMessages({
news: { id: 'app.home.news.title', defaultMessage: 'Minecraft news' }, widgetTitle: { id: 'app.home.skin-site.title', defaultMessage: 'StarLight skin site' },
openArticle: { id: 'app.home.news.open-article', defaultMessage: 'Read on minecraft.net' }, luckyIndex: { id: 'app.home.skin-site.lucky-index', defaultMessage: 'Lucky index' },
jump: { id: 'app.home.skin-site.jump', defaultMessage: 'Jump to StarLight skin site' },
}) })
const NEWS_COUNT = 12 const SKIN_PROFILE_URL = 'https://skin.starlight.cool/profile'
const NEWS_SKELETON_COUNT = 4 const SKIN_HOME_URL = 'https://skin.starlight.cool/'
const newsItems = ref<MinecraftNewsItem[]>([]) async function openProfile() {
const loading = ref(true)
const htmlDecoder = document.createElement('textarea')
get_minecraft_news(NEWS_COUNT)
.then((items) => {
newsItems.value = items.map((item) => ({
...item,
title: decodeHtmlEntities(item.title),
}))
})
.catch(() => {
newsItems.value = []
})
.finally(() => {
loading.value = false
})
const visible = computed(() => !offline.value && (loading.value || newsItems.value.length > 0))
function newsDateLabel(item: MinecraftNewsItem): string | null {
if (!item.date) return null
const parsed = new Date(item.date)
return Number.isNaN(parsed.getTime()) ? null : formatDate(parsed)
}
function decodeHtmlEntities(value: string): string {
htmlDecoder.innerHTML = value
return htmlDecoder.value
}
async function openArticle(item: MinecraftNewsItem) {
try { try {
await openUrl(item.read_more_url) await openUrl(SKIN_PROFILE_URL)
} catch (error) {
handleError(error)
}
}
async function openSkinSite() {
try {
await openUrl(SKIN_HOME_URL)
} catch (error) { } catch (error) {
handleError(error) handleError(error)
} }
@ -66,59 +33,34 @@ async function openArticle(item: MinecraftNewsItem) {
</script> </script>
<template> <template>
<section <section class="flex min-w-0 flex-col gap-3 border-0 border-b-[1px] border-solid border-[--brand-gradient-border] p-4">
v-if="visible"
class="flex min-w-0 flex-col gap-3 border-0 border-b-[1px] border-solid border-[--brand-gradient-border] p-4"
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<NewspaperIcon class="size-4 shrink-0 text-secondary" aria-hidden="true" /> <ExternalIcon class="size-4 shrink-0 text-secondary" aria-hidden="true" />
<h2 class="m-0 truncate text-lg"> <h2 class="m-0 truncate text-lg">
{{ formatMessage(messages.news) }} {{ formatMessage(messages.widgetTitle) }}
</h2> </h2>
</div> </div>
<ul v-if="loading" class="m-0 flex list-none flex-col gap-1.5 p-0" aria-hidden="true">
<li <div class="flex flex-col gap-2">
v-for="index in NEWS_SKELETON_COUNT"
:key="index"
class="flex animate-pulse items-center gap-2.5 px-1.5 py-1.5"
>
<div class="h-9 w-16 shrink-0 rounded-lg bg-button-bg" />
<div class="flex min-w-0 flex-1 flex-col gap-1.5">
<div class="h-3 w-full rounded bg-button-bg" />
<div class="h-3 w-1/2 rounded bg-button-bg" />
</div>
</li>
</ul>
<ul v-else class="m-0 flex list-none flex-col p-0">
<li v-for="item in newsItems" :key="`${item.date ?? ''}:${item.title}`" class="group min-w-0">
<button <button
v-tooltip="formatMessage(messages.openArticle)"
type="button" type="button"
class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-1.5 py-1.5 text-left transition-colors hover:bg-button-bg" class="flex w-full cursor-pointer items-center justify-between rounded-lg border border-[--brand-gradient-border] bg-transparent px-3 py-2 text-left transition-colors hover:bg-button-bg"
@click="openArticle(item)" @click="openProfile"
> >
<img <span class="text-sm font-semibold text-contrast">
v-if="item.image_url" {{ formatMessage(messages.luckyIndex) }}
:src="item.image_url"
alt=""
class="h-9 w-16 shrink-0 rounded-lg object-cover"
loading="lazy"
/>
<div v-else class="h-9 w-16 shrink-0 rounded-lg bg-button-bg" />
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<span class="line-clamp-2 text-sm font-semibold leading-snug text-contrast">
{{ item.title }}
</span> </span>
<span v-if="newsDateLabel(item)" class="truncate text-xs text-secondary"> <ExternalIcon class="size-4 shrink-0 text-secondary" aria-hidden="true" />
{{ newsDateLabel(item) }}
</span>
</div>
<ExternalIcon
class="size-3.5 shrink-0 text-secondary opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
aria-hidden="true"
/>
</button> </button>
</li>
</ul> <button
type="button"
class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-[--brand-gradient] px-3 py-2 font-semibold text-[var(--color-contrast)] transition-opacity hover:opacity-90"
@click="openSkinSite"
>
<ExternalIcon class="size-4 shrink-0" aria-hidden="true" />
<span>{{ formatMessage(messages.jump) }}</span>
</button>
</div>
</section> </section>
</template> </template>