跳转到内容

自建组件

时光2025/9/50 0 m

底部版权声明组件

说明

在技术文档中添加规范的版权声明,既能保护内容权益,也能提升文档的专业感。本组件参考唯知笔记的底部版权样式,结合 VitePress 主题特性优化实现,支持自适应布局与交互反馈,效果简洁且实用。

组件效果

底部版权声明组件

新建组件

  1. 组件文件结构 首先在 VitePress 主题目录下,按以下结构创建 DocFooterCopyright.vue 组件文件:
bash
.vitepress
├─ theme
  ├─ components
  ├─ DocFooterCopyright.vue
  ├─ MyLayout.vue
  1. 编写版权声明组件代码,以下是完整的 DocFooterCopyright.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>

注册组件

MyLayout.vue
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 交互),同时支持自定义隐藏评论区。

组件效果

友链组件

新建组件

  1. 新建组件与文件结构

    在 VitePress 主题目录下创建友链组件相关文件,结构如下:

bash
.vitepress
├─ theme
  ├─ components
  ├─ SLink
  ├─ index.vue
  └─ LinkItem.vue
  ├─ MyLayout.vue
  ├─ Twikoo.vue
  └─ index.ts
└─ config.mts
  1. 编写友链组件代码

    友链组件分为两部分: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>

注册组件

docs/.vitepress/theme/index.ts
ts
import SLink from "./components/SLink/index.vue";

export default {
  enhanceApp({ app }) {
    // 注册全局组件
    app.component("friend-link", SLink);
  },
};

使用方法

  • frontmatter 中设置 layoutfriend-link,启用友链布局模式
  • 建议设置 sidebar: false,界面更美观
  • 设置 comments: false 可关闭底部评论区
links.md
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
---

VitePress Algolia Twikoo EdgeOne Copyright