Files
Cloud-Blog/app/components/main/footer.vue
2026-01-24 22:19:21 +08:00

168 lines
5.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 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-4 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>
<!-- Git build info -->
<p v-if="build?.short" class="text-text-muted text-xs m-0">
构建自提交
<template v-if="commitUrl">
<a
:href="commitUrl"
target="_blank"
rel="noreferrer"
class="text-primary hover:underline"
>{{ build.short }}</a
>
</template>
<template v-else>
<span :title="build.sha">{{ build.short }}</span>
</template>
<span v-if="build.date"> · {{ build.date }}</span>
</p>
</footer>
</template>
<script setup>
import { onMounted, ref, computed } 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);
};
// runtime build info injected from nuxt.config at build time
const runtimeConfig = useRuntimeConfig();
const build = runtimeConfig.public?.build || null;
const repoBase = siteConfig.siteMeta?.repo || null;
const commitUrl = computed(() => {
if (!build?.sha || !repoBase) return null;
let base = repoBase;
if (!/^https?:\/\//.test(base)) {
base = `https://github.com/${base}`;
}
base = base.replace(/\.git$/, "").replace(/\/$/, "");
return `${base}/commit/${build.sha}`;
});
onMounted(() => {
if (showHitokoto) fetchHitokoto();
});
</script>