自建组件
底部版权声明组件
说明
在技术文档中添加规范的版权声明,既能保护内容权益,也能提升文档的专业感。本组件参考唯知笔记的底部版权样式,结合 VitePress 主题特性优化实现,支持自适应布局与交互反馈,效果简洁且实用。
组件效果
新建组件
- 组件文件结构 首先在 VitePress 主题目录下,按以下结构创建 DocFooterCopyright.vue 组件文件:
bash
.vitepress
├─ theme
│ ├─ components
│ │ ├─ DocFooterCopyright.vue
│ │ ├─ MyLayout.vue
- 编写版权声明组件代码,以下是完整的 DocFooterCopyright.vue 代码,包含自适应样式、交互效果与可配置参数,可直接复制使用并根据需求修改配置:
vue
<template>
<div v-if="shouldShow" class="shiguang-public">
<div class="copyright-card">
<!-- 公共图标 -->
<span class="copyright-symbol shiguang-icon shiguang-icon-public"></span>
<div class="copyright-content">
<!-- 作者信息 -->
<div class="copyright-item">
<span class="copyright-meta">
<span class="shiguang-icon shiguang-icon-user"></span>
<span class="meta-text">本文作者</span>:
</span>
<span class="copyright-info">
<a :href="config.authorUrl" target="_blank" rel="noopener">{{
config.authorName
}}</a>
</span>
</div>
<!-- 文章标题 -->
<div class="copyright-item">
<span class="copyright-meta">
<span class="shiguang-icon shiguang-icon-title"></span>
<span class="meta-text">本文标题</span>:
</span>
<span class="copyright-info">
{{ $frontmatter.title }}
</span>
</div>
<!-- 文章链接 -->
<div class="copyright-item">
<span class="copyright-meta">
<span class="shiguang-icon shiguang-icon-link"></span>
<span class="meta-text">本文链接</span>:
</span>
<span class="copyright-info">
<a :href="currentUrl" target="_blank" rel="noopener">{{
currentUrl
}}</a>
</span>
</div>
<!-- 版权声明 -->
<div class="copyright-item">
<span class="copyright-meta">
<span class="shiguang-icon shiguang-icon-cc"></span>
<span class="meta-text">版权声明</span>:
</span>
<span class="copyright-info">
本站文章除特别声明外,均采用
<a :href="config.licenseUrl" target="_blank" rel="noopener">{{
config.licenseName
}}</a>
协议,转载请注明来自
<a :href="config.siteUrl" target="_blank" rel="noopener">{{
config.siteName
}}</a
>!
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import { useData, useRoute } from "vitepress";
// 统一配置区
const config = {
authorName: "时光",
authorUrl: "https://github.com/nxcare",
siteName: "时光笔记",
siteUrl: "https://kandu.cxcare.top/",
licenseName: "CC BY-NC-SA 4.0",
licenseUrl: "https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh",
};
// 获取 frontmatter 和路由
const { frontmatter } = useData();
const route = useRoute();
// 是否显示版权组件
const shouldShow = computed(() => frontmatter.value.copyright !== false);
// 当前页面完整 URL
const currentUrl = computed(() => {
const baseUrl = config.siteUrl;
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
const path = route.path === "/" ? "" : route.path.replace(/^\//, "");
return `${normalizedBaseUrl}${path}`;
});
</script>
<style scoped>
.shiguang-public {
margin: 2rem 0;
}
.copyright-card {
position: relative;
padding: clamp(12px, 4vw, 16px) clamp(16px, 6vw, 20px);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background-color: var(--vp-c-bg-alt);
background: linear-gradient(to bottom, var(--vp-c-bg-alt), var(--vp-c-bg));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: clamp(14px, 4vw, 15px);
line-height: 1.7;
overflow: hidden;
}
.copyright-card:hover {
border-color: var(--vp-c-brand);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(33, 122, 244, 0.12), 0 0 0 1px var(--vp-c-brand);
}
/* 公共信号图标装饰 */
.copyright-symbol {
position: absolute;
top: 12px;
right: 16px;
color: var(--vp-c-text-3);
font-size: 20px;
opacity: 0.7;
transition: all 0.3s ease;
pointer-events: none;
}
.copyright-card:hover .copyright-symbol {
color: var(--vp-c-brand);
opacity: 1;
transform: scale(1.1);
}
/* 内容区布局 */
.copyright-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.copyright-item {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 6px 8px;
}
.copyright-meta {
display: inline-flex;
align-items: center;
color: var(--vp-c-text-2);
white-space: nowrap;
font-weight: 500;
}
.copyright-meta .shiguang-icon {
margin-right: 4px;
font-size: 1em;
}
.meta-text {
font-variant: small-caps; /* 小型大写字母,提升设计感 */
}
.copyright-info {
color: var(--vp-c-text-1);
word-break: break-all;
}
.copyright-info a {
color: var(--vp-c-brand-1);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.copyright-info a:hover {
color: var(--vp-c-brand-dark);
text-decoration: underline;
}
/* 图标使用 Emoji(更现代且无需字体) */
.shiguang-icon::before {
transform: none !important;
display: inline-block;
}
.shiguang-icon-user::before {
content: "👤";
}
.shiguang-icon-title::before {
content: "📝";
}
.shiguang-icon-link::before {
content: "🔗";
}
.shiguang-icon-cc::before {
content: "🌐";
}
.shiguang-icon-public::before {
content: "📡";
}
/* 移动端适配 */
@media (max-width: 768px) {
.copyright-card {
padding: 12px 14px;
font-size: 14px;
border-radius: 10px;
}
.copyright-symbol {
top: 10px;
right: 12px;
font-size: 18px;
}
.copyright-item {
gap: 4px 8px;
}
.meta-text {
font-size: 0.95em;
}
}
@media (min-width: 1024px) {
.copyright-card {
padding: 16px 20px;
}
}
</style>
注册组件
vue
<script setup lang="ts">
import DefaultTheme from "vitepress/theme";
import DocFooterCopyright from "./DocFooterCopyright.vue";
</script>
<template>
<DefaultTheme.Layout v-bind="$attrs">
<!-- 底部版权声明组件 -->
<template #doc-footer-before>
<DocFooterCopyright />
</template>
</DefaultTheme.Layout>
</template>
<style scoped></style>
友链组件
说明
特别中意唯知笔记的友链组件,作者也出了教程,但配置相对复杂,需要额外安装第三方库,不是我想要的,所以仿照作者的友链界面和实现流程,重新编写了这个组件,相较于原组件:
- 无需依赖第三方变量,减少配置成本;
- 用原生 Flex 布局替代 Element Plus 的
el-row
/el-col
,无需额外安装 UI 库; - 具有相同的核心功能(分组展示、自适应布局、hover 交互),同时支持自定义隐藏评论区。
组件效果
新建组件
新建组件与文件结构
在 VitePress 主题目录下创建友链组件相关文件,结构如下:
bash
.vitepress
├─ theme
│ ├─ components
│ │ ├─ SLink
│ │ │ ├─ index.vue
│ │ │ └─ LinkItem.vue
│ │ ├─ MyLayout.vue
│ │ ├─ Twikoo.vue
│ └─ index.ts
└─ config.mts
编写友链组件代码
友链组件分为两部分:
index.vue
(负责分组展示与留言区)和LinkItem.vue
(单个友链卡片)。Twikoo.vue
是友链组件下方评论区必需组件,使用前需要安装 Twikoo 服务,完整代码如下:
vue
<template>
<div class="my-links-container">
<!-- 页面主标题区域 -->
<div class="my-links-title">
<h1>{{ title }}</h1>
</div>
<!-- 顶部Banner区域 -->
<div v-if="bannerShow" class="flink-banner" id="banners">
<!-- 左上角smallTitle -->
<div class="icon-heartbeat1 banners-small-title">
{{ smallTitle }}
</div>
<!-- 右上角功能按钮组 -->
<div v-if="bannerButtonGroupShow" class="banner-button-group">
<!-- 随机访问按钮 -->
<button class="banner-button secondary" @click="handleRandomVisit" :disabled="allLinks.length === 0"
aria-label="随机访问友链">
<i class="icon-paper-plane" style="font-size: 18px;"></i>
<span class="banner-button-text">随机访问</span>
</button>
<!-- 申请友链按钮 -->
<a class="banner-button primary" href="#post-comment" :disabled="!shouldShow" aria-label="申请友链">
<i class="icon-link" style="font-size: 18px;"></i>
<span class="banner-button-text">申请友链</span>
</a>
</div>
<!-- 两行头像横向无限滚动区域(错位排列) -->
<div class="tags-group-all" ref="scrollContainer">
<div class="tags-group-wrapper">
<!-- 第一行 -->
<div class="tags-group-row" :class="{ 'offset-start': index % 2 === 0 }" v-for="(row, index) in avatarRows"
:key="index">
<div class="tags-group-content">
<a v-for="(link, linkIndex) in row" :key="linkIndex" class="tags-group-icon" target="_blank"
:href="link.link" :title="link.name" rel="external nofollow noopener">
<img :src="link.avatar" :alt="link.name" loading="lazy" :class="{ irregular: link.irregular }">
</a>
</div>
</div>
</div>
</div>
</div>
<!-- 友链分组列表,每个分组包含标题、描述和友链列表 -->
<div v-for="(group, index) in linksData" :key="index" class="my-links-group">
<!-- 分组标题容器 -->
<div class="title-wrapper">
<h3>{{ group.title }}</h3>
</div>
<!-- 分组描述文本 -->
<p class="group-desc">{{ group.desc }}</p>
<!-- 友链列表容器 -->
<div class="links-grid">
<!-- 每个友链项使用LinkItem子组件展示,通过:data传递友链信息 -->
<div v-for="link in group.list" :key="link.link" class="links-grid__item">
<LinkItem :data="link" />
</div>
</div>
</div>
<!-- 留言/评论区域,默认显示,可通过frontmatter隐藏 -->
<div v-if="commentShow" class="my-message-section" id="post-comment">
<div class="title-wrapper">
<h3>申请友链</h3>
</div>
<p>想要和我交换友链?请在评论区按以下格式留言 💞</p>
<!-- 留言卡片容器 -->
<div class="message-card">
<!-- 复制按钮区域 -->
<div class="copy-button-container">
<button class="copy-button" @click="copyMessageFormat" :aria-label="copyButtonText">
<i class="icon-copy" style="font-size: 16px;"></i>
<span class="copy-button-text">{{ copyButtonText }}</span>
</button>
</div>
<p>留言格式:</p>
<!-- 示例格式 -->
<pre ref="messageFormat">
名称: 时光笔记
链接: https://notes.ksah.cn
头像: https://notes.ksah.cn/logo.png
描述: 干货满满的技术笔记</pre>
<!-- 评论区插槽 -->
<!-- 默认为Twikoo评论组件,可通过插槽自定义其他评论系统 -->
<slot name="comments">
<Twikoo />
</slot>
</div>
</div>
</div>
</template>
<script setup>
import { useData } from "vitepress";
import LinkItem from "./LinkItem.vue";
// 导入Twikoo评论组件
import Twikoo from "../Twikoo.vue";
import { computed, ref } from "vue";
/**
* 单个友链的数据结构定义
* @typedef {Object} Link
* @property {string} name - 友链网站名称
* @property {string} link - 友链网站URL地址
* @property {string} avatar - 友链网站头像/Logo的图片URL
* @property {string} descr - 友链网站的简短描述
* @property {boolean} [irregular] - 可选参数,默认值为false,为false时将把头像处理为圆形
*/
/**
* 友链分组的数据结构定义
* @typedef {Object} LinkGroup
* @property {string} title - 分组标题
* @property {string} desc - 分组描述文字
* @property {Link[]} list - 该分组下的友链列表数组
*/
// 从页面frontmatter中获取配置数据
const { frontmatter } = useData();
// 从frontmatter中读取links字段,如果未定义则使用空数组
const linksData = computed(() => frontmatter.value.links || []);
// 从frontmatter中读取title字段,默认值为"我的友链"
const title = computed(() => frontmatter.value.title || "我的友链");
// 当frontmatter中comments为false时隐藏,默认显示
const commentShow = computed(() => frontmatter.value.comments !== false);
// 当frontmatter中banner为false时隐藏,默认显示
const bannerShow = computed(() => frontmatter.value.banner !== false);
// 当frontmatter中bannerButtonGroup为false时隐藏,默认显示
const bannerButtonGroupShow = computed(() => frontmatter.value.bannerButtonGroup !== false);
// 可自定义frontmatter中的smallTitle,作为banner的小标题,默认值为"与各位博主一起成长进步"
const smallTitle = computed(() => frontmatter.value.smallTitle || "与各位博主一起成长进步");
const allLinks = computed(() => {
return linksData.value.reduce((acc, group) => {
const processedLinks = group.list.map(link => ({
...link,
avatar: link.avatar
}));
acc.push(...processedLinks);
return acc;
}, []);
});
// 将头像平均分成两行,并复制内容以实现无缝滚动
const avatarRows = computed(() => {
const avatars = allLinks.value;
if (avatars.length === 0) return [[], []];
const mid = Math.ceil(avatars.length / 2);
const row1 = avatars.slice(0, mid);
const row2 = avatars.slice(mid);
// 复制内容以实现无缝滚动
return [
[...row1, ...row1], // 第一行复制一份
[...row2, ...row2] // 第二行复制一份
];
});
const handleRandomVisit = () => {
if (allLinks.value.length === 0) return;
const randomIndex = Math.floor(Math.random() * allLinks.value.length);
const randomLink = allLinks.value[randomIndex];
window.open(randomLink.link, "_blank");
};
// 复制功能相关
const messageFormat = ref(null);
const copyButtonText = ref('复制格式');
const copyMessageFormat = async () => {
if (!messageFormat.value) return;
const text = messageFormat.value.textContent;
await navigator.clipboard.writeText(text);
// 复制成功反馈
copyButtonText.value = '已复制 !';
// 2秒后恢复原文本
setTimeout(() => {
copyButtonText.value = '复制格式';
}, 2000);
};
</script>
<style scoped>
/* 字体图标 */
@import url("https://cdn.ksah.cn/fonts/icomoon/font.css");
/* 主容器样式 */
.my-links-container {
max-width: 1500px;
margin: 0 auto;
padding: 40px 10px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
/* 标题区域样式 */
.my-links-title {
margin-bottom: 50px;
padding: 0 10px;
}
/* 主标题样式 */
.my-links-title h1 {
font-size: 2rem;
font-weight: 600;
background: -webkit-linear-gradient(10deg, #bd34fe 5%, #e43498 15%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
line-height: 1.2;
display: inline-block;
}
/* Banner区域 */
.flink-banner {
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
border-radius: 12px;
padding: 50px 20px 30px;
margin-bottom: 60px;
position: relative;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
}
/* 左上角图标 */
.icon-heartbeat1::before {
margin-right: 8px;
}
/* 左上角smallTitle */
.banners-small-title {
position: absolute;
top: 20px;
left: 20px;
font-size: 1.5rem;
font-weight: 500;
color: var(--vp-c-text-1);
z-index: 2;
}
/* 右上角按钮组 */
.banner-button-group {
position: absolute;
top: 20px;
right: 20px;
display: flex;
gap: 12px;
z-index: 2;
}
.banner-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
border: none;
text-decoration: none;
}
.banner-button.secondary {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.banner-button.primary {
background: var(--vp-button-brand-bg);
color: var(--vp-button-brand-text);
}
/* 两行头像横向滚动区域 */
.tags-group-all {
width: 100%;
overflow: hidden;
padding: 40px 0 10px;
position: relative;
}
/* 滚动包装器 */
.tags-group-wrapper {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 每一行 */
.tags-group-row {
display: flex;
width: max-content;
animation: scrollRow 60s linear infinite;
will-change: transform;
backface-visibility: hidden;
}
/* 内容组 */
.tags-group-content {
display: flex;
gap: 20px;
padding: 0 10px;
}
/* 上下行错位排列 */
.offset-start {
margin-left: 60px;
/* 错开半个头像 */
}
/* 滚动动画 */
@keyframes scrollRow {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-40%);
}
}
/* 头像样式 */
.tags-group-icon {
flex: 0 0 120px;
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
border: 2px solid var(--vp-c-bg-soft);
transition: transform 0.3s ease, box-shadow 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.tags-group-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.tags-group-icon img.irregular {
border-radius: 8px;
object-fit: contain;
}
.tags-group-icon:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
z-index: 1;
}
.my-links-group {
margin-bottom: 40px;
}
/* 分组标题装饰线样式 */
.title-wrapper {
position: relative;
margin: 40px 0;
height: 1px;
background: #ddd;
transition: 0.3s;
}
/* 分组标题文字样式 */
.title-wrapper h3 {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: var(--vp-c-bg);
padding: 0 20px;
font-size: 1.3rem;
font-weight: 600;
color: var(--vp-c-text-1);
z-index: 1;
}
/* 分组描述文字样式 */
.group-desc {
text-align: center;
color: var(--vp-c-text-2);
font-size: 0.95rem;
margin-bottom: 30px;
padding: 0 10px;
}
/* 友链网格布局 - 核心响应式实现 */
.links-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
/* 让所有行的内容居中对齐 */
gap: 24px;
margin-bottom: 60px;
padding: 0 8px;
}
/* 每个友链项的样式,设置基础宽度 */
.links-grid__item {
flex: 0 0 calc(100% - 24px);
/* 移动设备:每行1个 */
break-inside: avoid;
}
/* 平板设备:每行2个 */
@media (min-width: 768px) {
.links-grid__item {
flex: 0 0 calc(50% - 24px);
}
}
/* 桌面设备:每行最多4个 */
@media (min-width: 1024px) {
.links-grid__item {
flex: 0 0 calc(25% - 24px);
}
}
/* 留言区样式 */
.my-message-section {
text-align: center;
margin-top: 20px;
}
/* 留言卡片样式 */
.message-card {
width: 100%;
max-width: 1500px;
margin: 30px auto;
padding: 32px;
border-radius: 12px;
background: var(--vp-c-bg);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid var(--vp-c-divider);
text-align: left;
position: relative;
}
/* 复制按钮容器 */
.copy-button-container {
position: absolute;
top: 20px;
right: 20px;
z-index: 2;
}
/* 复制按钮样式 */
.copy-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
text-decoration: none;
border: none;
}
.copy-button:hover {
background: var(--vp-button-brand-bg);
color: var(--vp-button-brand-text);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.copy-button:active {
transform: translateY(0);
}
/* 示例格式代码块样式 */
.message-card pre {
background: var(--vp-code-block-bg);
padding: 16px;
border-radius: 8px;
font-size: 0.95rem;
overflow-x: auto;
margin: 20px 0;
border: 1px solid var(--vp-c-divider);
line-height: 1.5;
position: relative;
}
/* 移动端留言卡片适配 */
@media (max-width: 768px) {
.message-card {
padding: 24px;
margin: 24px auto;
}
.copy-button-container {
position: static;
margin-bottom: 16px;
display: flex;
justify-content: flex-end;
}
.copy-button {
padding: 6px 12px;
font-size: 0.85rem;
}
.tags-group-icon {
flex: 0 0 80px;
width: 80px;
height: 80px;
}
.tags-group-content {
gap: 15px;
}
.offset-start {
margin-left: 40px;
/* 移动端适配 */
}
.flink-banner {
padding: 30px 15px 20px;
}
.banner-button {
padding: 6px 12px;
font-size: 0.85rem;
}
/* 两个按钮 */
.banner-button-group {
display: none;
}
/* 移动端滚动速度调整 */
.tags-group-row {
animation-duration: 40s;
}
}
/* 减少动画对性能的影响 */
@media (prefers-reduced-motion: reduce) {
.tags-group-row {
animation: none;
}
}
</style>
vue
<template>
<div class="link-item-card">
<!-- 标签 -->
<div class="link-tag" v-if="data.tag">{{data.tag}}</div>
<a :href="data.link" :title="data.name" target="_blank" rel="noopener">
<!-- 头像 -->
<div class="link-avatar">
<img
v-if="!imageFailed && data.avatar"
:src="data.avatar"
:alt="data.name"
@error="handleImageError"
:class="{ irregular: data.irregular }"
/>
<span v-else class="avatar-placeholder">
{{ data.name ? data.name.charAt(0).toUpperCase() : '?' }}
</span>
</div>
<!-- 信息 -->
<div class="link-content">
<div class="link-name">{{ data.name }}</div>
<div class="link-desc" :title="data.descr">
{{ data.descr }}
</div>
</div>
</a>
</div>
</template>
<script setup>
defineProps({
data: {
type: Object,
required: true
}
})
import { ref } from 'vue'
const imageFailed = ref(false)
const handleImageError = () => {
imageFailed.value = true
}
</script>
<style scoped>
.link-tag {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
font-size: 0.75rem;
font-weight: 500;
padding: 1px 6px;
border-radius: 8px;
background-color: var(--vp-badge-tip-bg);
color: var(--vp-badge-tip-text);
letter-spacing: 0.2px;
}
.link-item-card {
position: relative;
height: 100px;
border-radius: 12px;
background: var(--vp-c-bg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
border: 1px solid var(--vp-c-divider);
transition: all 0.3s ease;
overflow: hidden;
}
.link-item-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.link-item-card a {
display: flex;
align-items: center;
height: 100%;
text-decoration: none;
color: inherit;
}
.link-avatar {
flex: 0 0 100px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
}
.link-avatar img {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
transition: transform 0.3s ease;
}
.link-avatar img.irregular {
border-radius: 8px;
object-fit: contain;
}
.link-avatar .avatar-placeholder {
width: 60px;
height: 60px;
background: #f0f0f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: #555;
font-size: 1.2rem;
}
.link-item-card:hover .link-avatar img,
.link-item-card:hover .avatar-placeholder {
transform: scale(1.2);
}
.link-content {
flex: 1;
padding: 0 16px 0 0px;
}
.link-name {
font-size: 1rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
word-wrap: break-word;
}
.link-desc {
font-size: 0.875rem;
color: var(--vp-c-text-2);
display: -webkit-box; /* 兼容 WebKit 旧版本 */
display: box;
-webkit-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2; /* 兼容旧版 */
line-clamp: 2; /* 标准属性(核心) */
overflow: hidden;
line-height: 1.4;
}
</style>
vue
<template>
<div id="twikoo"></div>
</template>
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { useRoute } from 'vitepress'
const route = useRoute()
const initTwikoo = async () => {
// 判断是否在浏览器环境中
if (typeof window !== 'undefined') {
const twikoo = await import('twikoo')
twikoo.init({
envId: 'https://twikoo.cxcare.top/', // 换成你自己配置的域名
el: '#twikoo'
})
}
}
// 监听路由刷新评论
watch(route, () => {
initTwikoo()
})
onMounted(() => {
initTwikoo()
})
</script>
注册组件
ts
import SLink from "./components/SLink/index.vue";
export default {
enhanceApp({ app }) {
// 注册全局组件
app.component("friend-link", SLink);
},
};
使用方法
- 在
frontmatter
中设置layout
为friend-link
,启用友链布局模式 - 建议设置
sidebar: false
,界面更美观 - 设置
comments: false
可关闭底部评论区 - 设置
banner: false
可关闭顶部头像滚动 - 设置
bannerButtonGroup: false
可关闭 banner 顶部按钮组 - 设置
smallTitle
可设置 banner 左侧小标题
md
---
layout: friend-link
title: 我的友链
sidebar: false
links:
- title: 鸣谢
desc: "建站中学习和使用了以下博客/网站的技术和分享,特别鸣谢!\U0001FAE1"
list:
- name: 时光笔记
link: 'https://kandu.cxcare.top/'
avatar: 'https://kandu.cxcare.top/logo.svg'
irregular: true
tag: 编程爱好者
descr: 干货满满的技术笔记
- name: vitepress-teek
link: 'https://vp.teek.top/'
avatar: 'https://vitepress.yiov.top/logo.png'
irregular: true
tag: teek主题
descr: 一个轻量、简洁高效、灵活配置,易于扩展的 VitePress 主题
- name: VitePress
link: 'https://vitepress.dev/zh/'
avatar: 'https://vitepress.dev/vitepress-logo-mini.svg'
irregular: true
tag: vitepress官网
descr: 由 Vite 和 Vue 驱动的静态站点生成器
- name: VitePress 快速上手
link: 'https://vitepress.yiov.top/'
avatar: 'https://vitepress.dev/vitepress-logo-mini.svg'
irregular: true
tag: 美化教程
descr: 如果你也想搭建它,那跟我一起做吧
- name: 唯知笔记
link: 'https://note.weizwz.com/'
avatar: 'https://note.weizwz.com/logo.png'
irregular: true
tag: 实用笔记
descr: 探索知识的无限可能
- name: vitepress-theme-async
link: 'vitepress-theme-async'
avatar: 'https://vitepress-theme-async.imalun.com/logo@128x128.png'
irregular: true
tag: 主题参考
descr: 一个简单而轻量级的 Vitepress 主题
- name: Nólëbase Integrations
link: 'https://nolebase-integrations.ayaka.io/pages/zh-CN/integrations/'
avatar: 'https://nolebase-integrations.ayaka.io/android-chrome-192x192.png'
irregular: true
tag: 美化参考
descr: 多元化的文档工程工具合集
- name: vitepress plugins
link: 'https://github.com/T-miracle/vitepress-plugins'
avatar: 'https://avatars.githubusercontent.com/u/44044121?s=48&v=4'
irregular: true
tag: 实用插件
descr: 3个 vitepress 插件
- title: 传送门
desc: "聚集众多优秀独立博客,随机传送 \U0001F680"
list:
- name: One Blog
link: 'https://onedayxyy.cn/'
avatar: 'https://onedayxyy.cn/favicon.ico'
irregular: false
tag: teek伙伴
descr: 明心静性,爱自己
- name: 威威 Blog
link: 'https://dl-web.top/'
avatar: 'https://dl-web.top/avatar/avatar.svg'
irregular: false
tag: teek伙伴
descr: 人心中的成见是一座大山
- name: Hyde Blog
link: 'https://teek.seasir.top/'
avatar: 'https://teek.seasir.top/favicon.ico'
irregular: false
tag: teek伙伴
descr: 人心中的成见是一座大山
- name: bugcool
link: 'https://bugcool.cn/'
avatar: 'https://bugcool.cn/wp-content/uploads/2025/08/cropped-s-1.png'
irregular: false
tag: 科技语者
descr: 写代码哪有没 bug 的?
- name: LI SIR
link: 'https://lisir.me/'
avatar: 'https://lisir.me/logo.png'
irregular: false
tag: 美化参考
descr: 你的时间花在哪里,你的收获就在哪里
- name: 王嘉祥
link: 'https://blog.jiaxiang.wang/'
avatar: 'https://blog.jiaxiang.wang/img/logo.webp'
irregular: false
tag: Zola博客纵享丝滑
descr: 唱响科普和人生兴事,分享科技与美好生活
permalink: /links
prev:
text: 特殊符号大全
link: /notes/special-symbol
---
轮播图组件
组件效果
新建组件
- 组件文件结构 首先在 VitePress 主题目录下,按以下结构创建 Carousel.vue 组件文件:
bash
.vitepress
├─ theme
│ ├─ components
│ │ ├─ Carousel.vue
- 编写版权声明组件代码,以下是完整的 Carousel.vue 代码,包含自适应样式、交互效果与可配置参数,可直接复制使用并根据需求修改配置:
vue
<template>
<div class="carousel-container">
<!-- 轮播图容器 -->
<div class="carousel-wrapper" :style="{ height: `${height}px` }">
<!-- 图片展示区域 -->
<div class="carousel-slide">
<div
class="image-container"
:class="{ 'fade-enter': isTransitioning }"
ref="imageContainer"
>
<!-- 当前显示的图片 -->
<img
v-if="currentImage"
:src="currentImage"
:alt="imageAlt"
class="carousel-image"
@load="handleImageLoad"
/>
<!-- 全屏按钮 -->
<button
class="fullscreen-btn"
@click="toggleFullscreen"
:title="isFullscreen ? '退出全屏' : '全屏查看'"
v-if="currentImage"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline v-if="!isFullscreen" points="15 3 21 3 21 9"></polyline>
<polyline v-if="!isFullscreen" points="9 21 3 21 3 15"></polyline>
<line v-if="!isFullscreen" x1="21" y1="3" x2="14" y2="10"></line>
<line v-if="!isFullscreen" x1="3" y1="21" x2="10" y2="14"></line>
<polyline
v-if="isFullscreen"
points="6 18 12 18 12 24"
></polyline>
<polyline v-if="isFullscreen" points="18 6 12 6 12 0"></polyline>
<line v-if="isFullscreen" x1="18" y1="6" x2="24" y2="12"></line>
<line v-if="isFullscreen" x1="6" y1="18" x2="0" y2="12"></line>
</svg>
</button>
<!-- 全屏状态下的控制按钮 -->
<div
class="fullscreen-controls"
v-if="isFullscreen && images.length > 1"
>
<button class="control-btn prev-btn" @click="showPreviousImage">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<button class="control-btn next-btn" @click="showNextImage">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
<!-- 初始加载状态指示器 -->
<div v-if="isInitialLoading" class="loading-indicator">
<div class="spinner"></div>
<p>加载图片中...</p>
</div>
<!-- 初始加载错误提示 -->
<div v-if="hasInitialError" class="error-message">
<p>图片加载失败</p>
<button @click="initializeCarousel" class="retry-button">
重新加载
</button>
</div>
</div>
</div>
<!-- 非全屏状态下的控制按钮 -->
<button
class="control-btn prev-btn"
@click="showPreviousImage"
:disabled="images.length <= 1 || isFullscreen || isInitialLoading"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<button
class="control-btn next-btn"
@click="showNextImage"
:disabled="images.length <= 1 || isFullscreen || isInitialLoading"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
<!-- 指示器 -->
<div
class="carousel-indicators"
v-if="images.length > 1 && !isFullscreen && !isInitialLoading"
>
<button
v-for="(img, index) in images"
:key="index"
class="indicator-dot"
:class="{ active: currentIndex === index }"
@click="goToImage(index)"
:title="`查看第 ${index + 1} 张图片`"
></button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue";
// 组件属性定义
const props = defineProps({
// 轮播图高度
height: {
type: Number,
default: 400,
},
// 自动轮播间隔时间(毫秒)
interval: {
type: Number,
default: 8000,
},
// 图片API地址
imageApi: {
type: String,
required: true,
},
// 图片alt文本
imageAlt: {
type: String,
default: "轮播图片",
},
// 初始加载的图片数量
initialImages: {
type: Number,
default: 3,
validator: (value) => value >= 1, // 至少加载1张初始图片
},
// 预加载的图片数量(当前图片之后的数量)
preloadCount: {
type: Number,
default: 2,
validator: (value) => value >= 1, // 至少预加载1张
},
// 初始图片加载的间隔时间(毫秒)
initialLoadDelay: {
type: Number,
default: 500,
},
// 最大重试次数
maxRetry: {
type: Number,
default: 3,
},
});
// 状态管理
const images = ref([]); // 已加载并可显示的图片列表
const preloadQueue = ref([]); // 预加载队列(即将加入轮播的图片)
const currentIndex = ref(0); // 当前显示图片的索引
const currentImage = ref(""); // 当前显示的图片URL
const isInitialLoading = ref(false); // 初始加载状态
const hasInitialError = ref(false); // 初始加载错误状态
const isTransitioning = ref(false); // 图片切换过渡状态
const isFullscreen = ref(false); // 全屏状态
const intervalId = ref(null); // 自动轮播定时器ID
const isPreloading = ref(false); // 预加载状态(用于避免重复预加载)
// DOM引用
const imageContainer = ref(null);
// 生成唯一标识符,用于确保图片请求的唯一性
const generateUniqueId = () => {
return Date.now() + "-" + Math.random().toString(36).substring(2, 10);
};
// 加载单个图片(返回Promise,用于预加载)
const loadImage = async () => {
try {
const uniqueId = generateUniqueId();
const imageUrl = `${props.imageApi}?uid=${uniqueId}`;
// 检查图片是否已在列表中(避免重复)
const allImages = [...images.value, ...preloadQueue.value];
if (allImages.includes(imageUrl)) {
return loadImage(); // 如果重复则重新加载
}
// 创建Image对象进行预加载
return new Promise((resolve, reject) => {
const img = new Image();
img.src = imageUrl;
img.onload = () => {
resolve(imageUrl); // 加载成功返回URL
};
img.onerror = () => {
reject(new Error(`无法加载图片: ${imageUrl}`));
};
});
} catch (error) {
console.error("图片加载失败:", error);
throw error;
}
};
// 批量预加载图片
const preloadImages = async (count) => {
// 如果正在预加载中,则不重复执行
if (isPreloading.value) return;
isPreloading.value = true;
try {
// 创建指定数量的加载任务
const loadPromises = Array.from({ length: count }, () =>
loadImageWithRetry()
);
const newImages = await Promise.all(loadPromises);
// 将预加载的图片添加到队列
preloadQueue.value = [...preloadQueue.value, ...newImages];
} catch (error) {
console.error("批量预加载失败:", error);
} finally {
isPreloading.value = false;
}
};
// 带重试机制的图片加载
const loadImageWithRetry = async (retry = 0) => {
try {
return await loadImage();
} catch (error) {
if (retry < props.maxRetry) {
// 重试前等待一段时间,避免立即重试
await new Promise((resolve) => setTimeout(resolve, 1000 * (retry + 1)));
return loadImageWithRetry(retry + 1);
} else {
throw new Error(`超过最大重试次数(${props.maxRetry}次)`);
}
}
};
// 初始化轮播(加载初始图片)
const initializeCarousel = async () => {
isInitialLoading.value = true;
hasInitialError.value = false;
images.value = [];
preloadQueue.value = [];
currentIndex.value = 0;
currentImage.value = "";
try {
// 串行加载初始图片,避免同时请求导致重复
for (let i = 0; i < props.initialImages; i++) {
// 每张图片加载前增加延迟
if (i > 0) {
await new Promise((resolve) =>
setTimeout(resolve, props.initialLoadDelay)
);
}
const imageUrl = await loadImageWithRetry();
images.value.push(imageUrl);
}
// 初始图片加载完成后,立即预加载下一批
await preloadImages(props.preloadCount);
// 设置第一张图片为当前显示图片
currentImage.value = images.value[0];
} catch (error) {
console.error("轮播初始化失败:", error);
hasInitialError.value = true;
} finally {
isInitialLoading.value = false;
}
};
// 检查并补充预加载队列
const checkAndPreload = async () => {
// 当预加载队列中的图片数量不足时,补充预加载
if (preloadQueue.value.length < props.preloadCount) {
const needToLoad = props.preloadCount - preloadQueue.value.length;
await preloadImages(needToLoad);
}
};
// 显示下一张图片
const showNextImage = () => {
if (images.value.length <= 1) return;
isTransitioning.value = true;
// 计算下一张图片的索引
const nextIndex = (currentIndex.value + 1) % images.value.length;
// 如果下一张是列表中的最后几张,从预加载队列补充新图片
if (
nextIndex >= images.value.length - props.preloadCount &&
preloadQueue.value.length > 0
) {
// 从预加载队列中取出一张图片添加到轮播列表
const newImage = preloadQueue.value.shift();
images.value.push(newImage);
}
// 更新当前索引和图片
currentIndex.value = nextIndex;
currentImage.value = images.value[nextIndex];
// 触发补充预加载
checkAndPreload();
// 过渡动画结束
setTimeout(() => {
isTransitioning.value = false;
}, 500);
};
// 显示上一张图片
const showPreviousImage = () => {
if (images.value.length <= 1) return;
isTransitioning.value = true;
currentIndex.value =
(currentIndex.value - 1 + images.value.length) % images.value.length;
currentImage.value = images.value[currentIndex.value];
setTimeout(() => {
isTransitioning.value = false;
}, 500);
};
// 跳转到指定图片
const goToImage = (index) => {
if (index === currentIndex.value || index < 0 || index >= images.value.length)
return;
isTransitioning.value = true;
currentIndex.value = index;
currentImage.value = images.value[index];
// 如果跳转到较后的位置,检查是否需要补充预加载
if (index >= images.value.length - props.preloadCount) {
checkAndPreload();
}
setTimeout(() => {
isTransitioning.value = false;
}, 500);
};
// 切换全屏状态
const toggleFullscreen = async () => {
if (!imageContainer.value) return;
if (isFullscreen.value) {
// 退出全屏
if (document.exitFullscreen) {
await document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
await document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
await document.msExitFullscreen();
}
} else {
// 进入全屏
if (imageContainer.value.requestFullscreen) {
await imageContainer.value.requestFullscreen();
} else if (imageContainer.value.webkitRequestFullscreen) {
await imageContainer.value.webkitRequestFullscreen();
} else if (imageContainer.value.msRequestFullscreen) {
await imageContainer.value.msRequestFullscreen();
}
}
};
// 监听全屏状态变化
const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement;
};
// 启动自动轮播
const startAutoPlay = () => {
// 清除已有的定时器
if (intervalId.value) {
clearInterval(intervalId.value);
}
// 设置新的定时器
intervalId.value = setInterval(() => {
if (!isInitialLoading.value && !hasInitialError.value) {
showNextImage();
}
}, props.interval);
};
// 组件挂载时初始化
onMounted(async () => {
// 初始化轮播
await initializeCarousel();
// 启动自动轮播
if (images.value.length > 1) {
startAutoPlay();
}
// 监听全屏状态变化事件
document.addEventListener("fullscreenchange", handleFullscreenChange);
document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
document.addEventListener("msfullscreenchange", handleFullscreenChange);
});
// 组件卸载时清理
onUnmounted(() => {
// 清除定时器
if (intervalId.value) {
clearInterval(intervalId.value);
}
// 移除事件监听
document.removeEventListener("fullscreenchange", handleFullscreenChange);
document.removeEventListener(
"webkitfullscreenchange",
handleFullscreenChange
);
document.removeEventListener("msfullscreenchange", handleFullscreenChange);
// 如果处于全屏状态,退出全屏
if (isFullscreen.value) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
});
// 监听轮播间隔变化,重新启动自动轮播
watch(
() => props.interval,
() => {
startAutoPlay();
}
);
// 处理图片加载完成事件
const handleImageLoad = () => {
// 图片加载完成后关闭过渡状态
isTransitioning.value = false;
};
</script>
<style scoped>
.carousel-container {
position: relative;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.carousel-wrapper {
position: relative;
width: 100%;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.carousel-slide {
position: relative;
width: 100%;
height: 100%;
}
.image-container {
position: relative;
width: 100%;
height: 100%;
}
.carousel-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.5s ease-in-out;
}
/* 全屏按钮样式 */
.fullscreen-btn {
position: absolute;
bottom: 16px;
right: 16px;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
z-index: 4;
backdrop-filter: blur(2px);
}
.fullscreen-btn:hover {
background-color: rgba(0, 0, 0, 0.7);
transform: scale(1.1);
}
/* 全屏状态下的控制按钮容器 */
.fullscreen-controls {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* 让点击穿透到图片上 */
}
/* 全屏状态下的控制按钮 */
.fullscreen-controls .control-btn {
pointer-events: auto; /* 恢复按钮的点击能力 */
background-color: rgba(0, 0, 0, 0.7);
}
/* 全屏状态下的样式调整 */
:fullscreen .carousel-image {
object-fit: contain;
background-color: #000;
}
:-webkit-full-screen .carousel-image {
object-fit: contain;
background-color: #000;
}
:-ms-fullscreen .carousel-image {
object-fit: contain;
background-color: #000;
}
:fullscreen .fullscreen-btn {
background-color: rgba(0, 0, 0, 0.7);
}
/* 过渡动画 */
.fade-enter {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 加载指示器 */
.loading-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.8);
z-index: 10;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 错误消息 */
.error-message {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.8);
z-index: 10;
padding: 20px;
text-align: center;
}
.retry-button {
margin-top: 16px;
padding: 8px 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.retry-button:hover {
background-color: #2980b9;
}
/* 控制按钮 */
.control-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.3);
color: white;
border: none;
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
z-index: 5;
}
.control-btn:hover {
background-color: rgba(0, 0, 0, 0.5);
transform: translateY(-50%) scale(1.1);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: translateY(-50%);
}
.prev-btn {
left: 16px;
}
.next-btn {
right: 16px;
}
/* 指示器 */
.carousel-indicators {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 16px;
}
.indicator-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #ddd;
border: none;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
}
.indicator-dot.active {
background-color: #3498db;
transform: scale(1.2);
}
.indicator-dot:hover:not(.active) {
background-color: #bbb;
}
/* 响应式调整 */
@media (max-width: 768px) {
.control-btn {
width: 36px;
height: 36px;
}
.fullscreen-btn {
width: 36px;
height: 36px;
bottom: 12px;
right: 12px;
}
.carousel-indicators {
gap: 6px;
}
.indicator-dot {
width: 8px;
height: 8px;
}
}
</style>
使用方法
新建一个 markdown 文件,填写下面内容即可显示
markdown
---
permalink: /images
sidebar: false
outline: false
---
<script setup>
import Carousel from './.vitepress/theme/components/Carousel.vue'
</script>
## 自建 API
<Carousel
imageApi="https://rpic.cxcare.top/api"
height="500"
interval="6000"
imageAlt="自建API图片"
/>
## 威威 API
<Carousel
imageApi="https://random.dl-web.top"
height="500"
interval="6000"
imageAlt="威威随机图片"
/>
## One API
<Carousel
imageApi="https://imgapi.onedayxyy.cn"
height="500"
interval="6000"
imageAlt="威威随机图片"
/>
## 白木 API
<Carousel
imageApi="https://baimu.live/api/tp/acg/ecy.php"
height="500"
interval="6000"
imageAlt="威威随机图片"
/>