自建组件
底部版权声明组件
说明
在技术文档中添加规范的版权声明,既能保护内容权益,也能提升文档的专业感。本组件参考唯知笔记的底部版权样式,结合 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-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";
/**
* 组件配置项(可根据个人需求修改)
* - authorName: 作者名称
* - authorUrl: 作者主页链接(如 GitHub、个人博客)
* - siteName: 网站名称
* - siteUrl: 网站首页链接
* - licenseName: 版权协议名称(如 CC BY-NC-SA 4.0)
* - licenseUrl: 版权协议详情链接
*/
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-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
(单个友链卡片),完整代码如下:
vue
<template>
<div class="my-links-container">
<!-- 页面主标题区域 -->
<div class="my-links-title">
<h1>{{ title }}</h1>
</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="shouldShow" class="my-message-section">
<div class="title-wrapper">
<h3>留链吗</h3>
</div>
<p>留恋的小伙伴,想要和我做友链 💞</p>
<!-- 留言卡片容器 -->
<div class="message-card">
<p>欢迎在评论区留言,格式如下:</p>
<!-- 示例格式 -->
<pre>
名称: VitePress
链接: https://vitepress.dev
头像: https://vitepress.dev/logo.png
描述: 构建惊艳的 Vue 驱动的静态网站</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 } 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 shouldShow = computed(() => frontmatter.value.comments !== false);
</script>
<style scoped>
/* 主容器样式 */
.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;
}
/* 分组标题装饰线样式 */
.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;
transition: all 0.2s ease;
}
/* 移动端留言卡片适配 */
@media (max-width: 768px) {
.message-card {
padding: 24px;
margin: 24px auto;
}
}
/* 示例格式代码块样式 */
.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;
}
/* 留言卡片悬停效果 */
.message-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.12);
}
</style>
vue
<template>
<div class="link-item-card">
<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-item-card {
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>
注册组件
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
可关闭底部评论区
md
---
layout: friend-link
title: 我的友链
sidebar: false
permalink: links
links:
- title: 鸣谢
desc: "建站中学习和使用了以下博客/网站的技术和分享,特别鸣谢!\U0001FAE1"
list:
- name: vitepress-teek
link: "https://vp.teek.top/"
avatar: "https://vitepress.yiov.top/logo.png"
irregular: true
descr: 一个轻量、简洁高效、灵活配置,易于扩展的 VitePress 主题
- name: VitePress
link: "https://vitepress.dev/zh/"
avatar: "https://vitepress.dev/vitepress-logo-mini.svg"
irregular: true
descr: 由 Vite 和 Vue 驱动的静态站点生成器
- name: VitePress 快速上手教程
link: "https://vitepress.yiov.top/"
avatar: "https://vitepress.dev/vitepress-logo-mini.svg"
irregular: true
descr: 如果你也想搭建它,那跟我一起做吧
- name: 唯知笔记
link: "https://note.weizwz.com/"
avatar: "https://note.weizwz.com/logo.png"
irregular: true
descr: 探索知识的无限可能
- title: 传送门
desc: "聚集众多优秀独立博客,随机传送 \U0001F680"
list:
- name: One Blog
link: "https://onedayxyy.cn/"
avatar: "https://onedayxyy.cn/favicon.ico"
descr: 明心静性,爱自己
irregular: false
- name: 威威 Blog
link: "https://dl-web.top/"
avatar: "https://dl-web.top/avatar/avatar.svg"
descr: 人心中的成见是一座大山
irregular: false
- name: Hyde Blog
link: "https://teek.seasir.top/"
avatar: "https://teek.seasir.top/favicon.ico"
descr: 人心中的成见是一座大山
irregular: false
- name: bugcool
link: "https://bugcool.cn/"
avatar: "https://bugcool.cn/wp-content/uploads/2025/08/cropped-s-1.png"
descr: 写代码哪有没 bug 的?
irregular: false
---