Files
Cloud-Home/app/components/FriendsSection.vue
RhenCloud 6b05f7c74e style(app): 使用 NuxtImg 替代 img 标签
在多个组件中将 `img` 标签替换为 `NuxtImg` 标签,提升图片加载的性能和优化。例如,在 `AboutSection.vue`、`FriendsSection.vue`、`HeroSection.vue`、`ProjectsSection.vue`、`SitesSection.vue` 和 `SkillsSection.vue` 中的图片标签。

refactor(app): 扩展 `nuxt.config.ts` 配置

扩展了 `nuxt.config.ts` 配置文件中的模块配置,添加了 `@nuxt/image` 和 `@nuxt/eslint` 模块。同时,优化了 `routeRules` 配置,以支持预渲染和增量静态生成。
2025-12-18 22:13:47 +08:00

242 lines
11 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.
<template>
<div class="card panel flex flex-col gap-2.5">
<h2 class="m-0 mb-1 text-lg font-semibold">友情链接</h2>
<p class="text-text-muted text-sm m-0 mb-3 block">欢迎互换友链 · Friends</p>
<div class="grid grid-cols-1 gap-4 w-full max-w-[1100px] mx-auto sm:grid-cols-2">
<article
v-for="f in displayedFriends" :key="f.url"
class="rounded-[14px] border border-white/10 bg-gradient-to-br from-white/5 to-white/0 px-4 py-3.5 transition-all duration-200 hover:-translate-y-[3px] hover:border-pink-400/50 w-[290px] h-[145px] flex flex-col">
<div class="flex items-center justify-between mb-1.5">
<div class="flex items-center gap-2 min-w-0">
<NuxtImg
v-if="f.avatar" :src="f.avatar" :alt="f.name" loading="lazy"
class="w-12 h-12 rounded-full object-cover border border-white/15" />
<h3 class="m-0 font-semibold text-base whitespace-nowrap overflow-hidden text-ellipsis">
{{ f.name }}
</h3>
</div>
<span class="rounded-full px-2.5 py-1 text-xs bg-purple-400/15 text-purple-300">友链</span>
</div>
<p class="text-sm text-white/60 flex-1 overflow-hidden truncate-lines-2 mb-2">
{{ f.desc || "一个有趣的站点" }}
</p>
<NuxtLink
:to="f.url"
class="inline-flex items-center gap-1.5 mt-auto shrink-0 font-semibold text-pink-300 hover:text-pink-400 transition-all duration-200 hover:gap-2">
访问
</NuxtLink>
</article>
</div>
</div>
<section class="card flex flex-col gap-2.5">
<div class="flex justify-center items-center align-center flex-wrap">
<button
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary cursor-pointer transition-all duration-200 hover:bg-primary/20 hover:border-primary/80 hover:shadow-lg hover:shadow-primary/25"
@click="openForm">
申请友链
</button>
</div>
</section>
<Teleport to="body">
<div
v-if="showDialog" class="fixed inset-0 bg-black/45 backdrop-blur-sm flex items-center justify-center z-50"
@click.self="closeDialog">
<div
class="min-w-[280px] max-w-[420px] bg-gradient-to-br from-pink-500/12 to-white/8 border border-white/15 rounded-2xl p-4 shadow-xl">
<h3 class="m-0 mb-2">{{ dialogTitle }}</h3>
<p class="text-text-muted text-sm mb-4">{{ dialogText }}</p>
<div class="flex justify-end">
<button
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary cursor-pointer hover:bg-primary/20 transition-all"
@click="closeDialog">
好的
</button>
</div>
</div>
</div>
</Teleport>
<!-- 申请友链模态弹窗 -->
<Teleport to="body">
<div
v-if="showFormModal"
class="fixed inset-0 bg-black/45 backdrop-blur-sm flex items-center justify-center z-50"
@click.self="showFormModal = false">
<div
class="w-[92%] max-w-[540px] bg-gradient-to-br from-white/8 to-primary/6 border border-white/15 rounded-2xl p-6 shadow-xl">
<h3 class="m-0 mb-4 text-center">申请友链</h3>
<div class="mb-4 text-sm text-text-primary">
<div class="mb-2 font-semibold">请在申请前在你站点添加以下信息示例 JSON</div>
<pre class="bg-white/6 border border-white/10 rounded-lg p-3 text-xs overflow-auto">
<code>{{ exampleJson }}</code>
</pre>
</div>
<form class="grid grid-cols-1 sm:grid-cols-2 gap-3" @submit.prevent="submitForm">
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold sm:col-span-2">
网站名称 *
<input
v-model="form.name" required placeholder="网站名称"
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" >
</label>
<!-- URL Email 同行 -->
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold">
网站链接 *
<input
v-model="form.url" type="url" required placeholder="https://example.com"
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" >
</label>
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold">
联系邮箱 *
<input
v-model="form.email" type="email" required placeholder="example@example.com"
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" >
</label>
<!-- 描述 头像 同行 -->
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold">
网站描述
<input
v-model="form.desc" placeholder="可选"
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" >
</label>
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold">
头像链接
<input
v-model="form.avatar" type="url" placeholder="可选,展示头像"
class="px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary focus:outline-none" >
</label>
<label class="flex flex-col gap-1 text-sm text-text-primary font-semibold sm:col-span-2">
想说的话
<div class="flex items-center gap-2">
<textarea
v-model="form.message" placeholder="可选最多50字" maxlength="50"
class="flex-1 px-2.5 py-2 rounded-xl border border-white/20 bg-white/8 text-text-primary h-24 resize-none"/>
</div>
</label>
<div class="sm:col-span-2 flex items-center justify-center gap-3 mt-2">
<button
type="button" class="px-3 py-2 rounded-2xl border border-white/10 bg-white/6"
@click="showFormModal = false">
取消
</button>
<button
type="submit" :disabled="loading"
class="px-3 py-2 rounded-2xl border border-primary/50 bg-primary/12 text-text-primary disabled:opacity-50">
{{ loading ? "提交中..." : "提交" }}
</button>
</div>
<div class="sm:col-span-2">
<span class="text-text-muted text-sm">{{ message }}</span>
</div>
</form>
</div>
</div>
</Teleport>
</template>
<script setup>
import { reactive, ref, watch, computed } from "vue";
import siteConfig from "../config/siteConfig";
const props = defineProps({ friends: { type: Array, default: () => [] } });
const showFormModal = ref(false);
const loading = ref(false);
const message = ref("");
const showDialog = ref(false);
const dialogTitle = ref("");
const dialogText = ref("");
const form = reactive({
name: "",
url: "",
desc: "",
email: "",
avatar: "",
message: "",
});
const displayedFriends = ref([]);
const shuffle = (list) => {
const arr = [...list];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
};
watch(
() => props.friends,
(val) => {
displayedFriends.value = shuffle(val || []);
},
{ immediate: true }
);
const exampleJson = computed(() => {
const name = siteConfig.profile?.name || siteConfig.siteMeta?.title || "";
const url = siteConfig.siteMeta?.url || "";
const desc = siteConfig.profile?.bio || "";
const email = siteConfig.profile.email || "";
const avatarRaw = siteConfig.profile?.avatar || "";
const avatar = resolveUrl(avatarRaw);
return JSON.stringify({ name, url, desc, email, avatar }, null, 2);
});
const openForm = () => {
showFormModal.value = true;
};
// resolve possible local paths to absolute URLs using site meta URL
const resolveUrl = (p) => {
if (!p) return "";
const s = String(p).trim();
if (/^https?:\/\//i.test(s) || /^\/\//.test(s)) return s;
const base = (siteConfig.siteMeta && siteConfig.siteMeta.url) ? String(siteConfig.siteMeta.url).replace(/\/$/, "") : "";
if (!base) return s;
if (s.startsWith("/")) return base + s;
return base + "/" + s;
};
const submitForm = async () => {
loading.value = true;
message.value = "";
try {
const resp = await fetch("/api/send-mail", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!resp.ok) throw new Error("send failed");
form.name = "";
form.url = "";
form.desc = "";
form.email = "";
form.avatar = "";
form.message = "";
message.value = "提交成功,已发送申请邮件";
showFormModal.value = false;
dialogTitle.value = "提交成功";
dialogText.value = "已发送申请邮件,感谢你的提交,将会尽快审核并在通过后通过邮件联系。";
showDialog.value = true;
} catch (e) {
message.value = "提交失败,请稍后重试";
dialogTitle.value = "提交失败";
dialogText.value = "邮件发送失败,请稍后重试或检查网络连接。";
showDialog.value = true;
console.error(e);
} finally {
loading.value = false;
}
};
const closeDialog = () => {
showDialog.value = false;
};
</script>