Files
Cloud-Blog/app/components/main/footer.vue
2026-01-18 12:32:55 +08:00

135 lines
4.0 KiB
Vue

<!-- eslint-disable vue/no-v-html -->
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<template>
<footer
class="text-center mt-auto w-full flex flex-col gap-2 py-8 px-4 border-t border-white/10 dark:border-white/5">
<!-- 一言 -->
<p v-if="showHitokoto && quote" class="text-text-muted text-sm m-0 italic">
{{ quote }}<span v-if="from" class="ml-1.5"> {{ from }}</span>
<span v-if="from && fromWho" class="ml-1.5"> · {{ fromWho }}</span>
</p>
<!-- 备案信息 -->
<p v-if="contact?.beian" class="text-text-muted text-xs m-0">
<NuxtLink
:to="contact.beianLink || 'https://beian.miit.gov.cn/'"
class="opacity-85 transition-all duration-200 hover:text-accent hover:opacity-100">
{{ contact.beian }}
</NuxtLink>
</p>
<div v-if="adStats && processedAds.length">
<template v-for="ad in processedAds" :key="ad.link">
<a
v-if="isExternal(ad.link)"
:href="ad.link"
target="_blank"
rel="noreferrer"
class="text-text-muted text-sm m-0">
<span v-html="ad.html"></span>
</a>
<NuxtLink v-else :to="ad.link" class="text-text-muted text-sm m-0">
<span v-html="ad.html"></span>
</NuxtLink>
</template>
</div>
<p class="text-text-muted text-xs m-0">
Powered by
<a
href="https://nuxt.com"
target="_blank"
rel="noreferrer"
class="text-primary hover:text-accent transition-colors"
>Nuxt 4</a
>
·
<a
href="https://tailwindcss.com"
target="_blank"
rel="noreferrer"
class="text-primary hover:text-accent transition-colors"
>Tailwind CSS 4</a
>
·
<a
href="https://vuejs.org"
target="_blank"
rel="noreferrer"
class="text-primary hover:text-accent transition-colors"
>Vue 3</a
>
</p>
<p>
© {{ new Date(siteConfig.siteMeta.startTime).getFullYear() }}-{{ new Date().getFullYear() }}
{{ siteConfig.siteMeta.author }} |
<NuxtLink to="/rss.xml" class="text-primary" external>RSS</NuxtLink> |
<NuxtLink to="/sitemap.xml" class="text-primary" external>Sitemap</NuxtLink>.
</p>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="contact?.customHtml" v-html="contact.customHtml"></div>
</footer>
</template>
<script setup>
import { onMounted, ref } from "vue";
import siteConfig from "~/config";
const contact = siteConfig.footer || {};
const quote = ref("");
const from = ref("");
const fromWho = ref("");
const showHitokoto = siteConfig.footer?.hitokoto?.enable;
const adStats = siteConfig.ad?.enable;
const ads = siteConfig.ad?.ads || [];
// processedAds: replace root-relative src ("/...") with full site URL to
// avoid issues when deployed under different hosts/base paths or when
// root-relative assets are unavailable. Keeps existing HTML otherwise.
const processedAds = (ads || []).map((ad) => {
const html =
typeof ad.body === "string"
? ad.body.replace(/src="\/(?!\/)/g, `src="${siteConfig.siteMeta.url}/`)
: ad.body;
return { ...ad, html };
});
const buildHitokotoUrl = () => {
const type = siteConfig.footer?.hitokoto?.type;
const url = new URL("https://v1.hitokoto.cn/");
if (Array.isArray(type)) {
type.filter(Boolean).forEach((t) => url.searchParams.append("c", t));
} else if (typeof type === "string") {
type
.split("&")
.map((t) => t.trim())
.filter(Boolean)
.forEach((t) => url.searchParams.append("c", t));
}
return url.toString();
};
const fetchHitokoto = async () => {
try {
const resp = await fetch(buildHitokotoUrl());
const data = await resp.json();
quote.value = data.hitokoto || "";
from.value = data.from || "";
fromWho.value = data.from_who || "";
} catch (e) {
console.warn("Hitokoto fetch failed", e);
}
};
const isExternal = (url) => {
return typeof url === "string" && /^https?:\/\//.test(url);
};
onMounted(() => {
if (showHitokoto) fetchHitokoto();
});
</script>