Skip to content
0

v3-友链循环风格

v3-友链循环风格

image-20251015003628189

目录

[toc]

背景

时光大佬更新了 友联 新功能,那么我这里也更新下哦。😊

环境

Teek@1.5.1-2025.10.19版本

Teek-one开箱即用版仓库:https://cnb.cool/onedayxyy/vitepress-theme-teek-one-public

版权

配置来自《时光》大佬提供的功能,感谢大佬💖💖💖。

配置

提示

特别中意唯知笔记的友链组件,作者也出了教程,但配置相对复杂,需要额外安装第三方库,不是我想要的,所以仿照作者的友链界面和实现流程,重新编写了这个组件,相较于原组件:

  • 无需依赖第三方变量,减少配置成本;
  • 用原生 Flex 布局替代 Element Plus 的 el-row/el-col,无需额外安装 UI 库;
  • 具有相同的核心功能(分组展示、自适应布局、hover 交互),同时支持自定义隐藏评论区。

1.新建组件

  1. 新建组件与文件结构

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

    ts
    .vitepress
    ├─ theme
    │  ├─ components
    │  │  ├─ SLink
    │  │  │  ├─ index.vue
    │  │  │  └─ LinkItem.vue
    │  │  ├─ MyLayout.vue
    │  │  ├─ Twikoo.vue
    │  └─ index.ts
    └─ config.mts

    image-20250903060120881

  2. 编写友链组件代码

    友链组件分为两部分:index.vue(负责分组展示与留言区)和 LinkItem.vue(单个友链卡片),完整代码如下:

    详细信息
    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">
    名称: One
    链接: https://onedayxyy.cn/
    头像: https://img.onedayxyy.cn/images/Teek/Teekwebsite/xyy-logo.webp
    描述: 明心静性,爱自己</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" ref="cardRef">
        <!-- 标签 -->
        <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 lang="ts">
    import { ref, onMounted, onBeforeUnmount } from "vue";
    
    interface LinkData {
      name: string;
      link: string;
      tag?: string;
      avatar?: string;
      descr?: string;
      irregular?: boolean;
    }
    
    defineProps<{
      data: LinkData;
    }>();
    
    const imageFailed = ref(false);
    const handleImageError = () => (imageFailed.value = true);
    
    const cardRef = ref<HTMLElement | null>(null);
    let rafId: number;
    const lights: any[] = [];
    const lightCount = 1; // 只用一个光圈覆盖整个卡片
    
    const lerp = (start: number, end: number, t: number) => start + (end - start) * t;
    
    // 预设的光圈颜色配置表
    // 格式:rgba(红, 绿, 蓝, 透明度)
    // 透明度 0.8 表示半透明效果
    const presetColors = [
      "rgba(255, 99, 132, 0.8)", // 红色调
      "rgba(54, 162, 235, 0.8)", // 蓝色调
      "rgba(255, 159, 64, 0.8)", // 橙色调
      "rgba(75, 192, 192, 0.8)", // 青色调
      "rgba(153, 102, 255, 0.8)", // 紫色调
      "rgba(255, 159, 64, 0.8)", // 橙色调
      "rgba(255, 99, 132, 0.8)", // 红色调
      "rgba(0, 255, 255, 0.8)", // 青绿色
    ];
    
    const getRandomColor = () => presetColors[Math.floor(Math.random() * presetColors.length)];
    
    const getGradientColor = (lightColor: string) => {
      const [r, g, b] = lightColor.match(/\d+/g)!.map(Number);
      return `rgba(${r}, ${g}, ${b}, 0.6)`; // 让光圈中心更亮
    };
    
    onMounted(() => {
      if (!cardRef.value) return;
      const card = cardRef.value;
      const lightColor = getRandomColor();
    
      const handleMouseEnter = () => {
        for (let i = 0; i < lightCount; i++) {
          const lightEl = document.createElement("div");
          lightEl.classList.add("light");
          lightEl.style.position = "absolute";
          lightEl.style.borderRadius = "50%";
          lightEl.style.pointerEvents = "none";
          lightEl.style.background = `rgba(255,255,255,0)`;
          card.appendChild(lightEl);
    
          lights.push({
            el: lightEl,
            x: card.offsetWidth / 2,
            y: card.offsetHeight / 2,
            size: Math.max(card.offsetWidth, card.offsetHeight) * 1.8,
            opacity: 0,
            targetX: card.offsetWidth / 2,
            targetY: card.offsetHeight / 2,
            speed: 0.08,
            lightColor,
          });
        }
      };
    
      const handleMouseLeave = () => {
        lights.forEach(light => {
          light.el.style.opacity = "0";
          setTimeout(() => light.el.remove(), 300);
        });
        lights.length = 0;
      };
    
      const handleMouseMove = (e: MouseEvent) => {
        const rect = card.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
    
        lights.forEach(light => {
          light.targetX = x;
          light.targetY = y;
    
          light.x = lerp(light.x, light.targetX, light.speed);
          light.y = lerp(light.y, light.targetY, light.speed);
    
          const maxDim = Math.max(card.offsetWidth, card.offsetHeight);
          const targetSize = maxDim * 1.8;
          light.size = lerp(light.size, targetSize, 0.1);
    
          light.opacity = lerp(light.opacity, 0.6, 0.1);
    
          const gradientColor = getGradientColor(light.lightColor);
          light.el.style.background = `radial-gradient(circle at ${light.x}px ${light.y}px, ${gradientColor} 0%, rgba(255,255,255,0) 100%)`;
    
          light.el.style.width = `${light.size}px`;
          light.el.style.height = `${light.size}px`;
          light.el.style.left = `${light.x - light.size / 2}px`;
          light.el.style.top = `${light.y - light.size / 2}px`;
          light.el.style.opacity = light.opacity.toString();
        });
      };
    
      card.addEventListener("mouseenter", handleMouseEnter);
      card.addEventListener("mouseleave", handleMouseLeave);
      card.addEventListener("mousemove", handleMouseMove);
    
      const animate = () => (rafId = requestAnimationFrame(animate));
      animate();
    
      onBeforeUnmount(() => cancelAnimationFrame(rafId));
    });
    </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;
      z-index: 0; /* 作为光圈定位参考 */
    }
    
    .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;
      position: relative;
      z-index: 1; /* 在光圈之上 */
    }
    
    .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;
      position: relative;
      z-index: 1;
    }
    
    .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;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: normal;
    }
    
    .link-desc {
      font-size: 0.875rem;
      color: var(--vp-c-text-2);
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2;
      overflow: hidden;
      line-height: 1.4;
    }
    
    /* 光圈样式 */
    .light {
      position: absolute;
      border-radius: 50%;
      pointer-events: none;
      z-index: 0;
      transition:
        width 0.1s,
        height 0.1s,
        opacity 0.1s;
    }
    
    .banner-button.primary[data-v-7a7fc354] {
        background: #0975e3;
        color: var(--vp-button-brand-text);
    }
    </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.onedayxyy.cn/', // 换成你自己配置的域名
            el: '#twikoo'
          })
        }
      }
      
      // 监听路由刷新评论
      watch(route, () => {
        initTwikoo()
      })
      
      onMounted(() => {
        initTwikoo()
      })
      </script>

2.注册组件

ts
import SLink from "./components/SLink/index.vue";

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

3.使用方法

  • frontmatter 中设置 layoutfriend-link,启用友链布局模式
  • 建议设置 sidebar: false,界面更美观
  • 设置 comments: false 可关闭底部评论区

新建如下文件:

使用模板
md
---
date: 2025-09-03 02:06:02
layout: friend-link
title: 我的友链
banner: true
bannerButtonGroup: true
smallTitle: 与各位博主一起成长进步
comments: true
sidebar: false
permalink: /about/friend-links
article: false
# comment: false
copyright: false
articleShare: false
appreciation: false
links:
  - title: 个人站点
    #desc: "建站中学习和使用了以下博客/网站的技术和分享,特别鸣谢!💖"
    list:
      - name: One
        link: https://onedayxyy.cn/
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/xyy-logo.webp
        irregular: true
        descr: 明心静性,爱自己(基于vitepress)
      # - name: One
      #   link: https://zola.onedayxyy.cn/
      #   avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/xyy-logo.webp
      #   irregular: true
      #   descr: 明心静性,爱自己(基于rust开发的zola,移植自张洪heo博客)         
      # - name: VitePress
      #   link: https://vitepress.dev/zh/
      #   avatar: https://vitepress.dev/vitepress-logo-mini.svg
      #   irregular: true
      #   descr: 由 Vite 和 Vue 驱动的静态站点生成器
  - title: 大佬
    #desc: "建站中学习和使用了以下博客/网站的技术和分享,特别鸣谢!💖"
    list:
      - name: 宇阳
        link: https://liuyuyang.net
        avatar: https://q1.qlogo.cn/g?b=qq&nk=3311118881&s=640
        irregular: true
        descr: 记录所学知识,缩短和大神的差距!
      - name: Teeker
        link: https://vp.teek.top/
        avatar: https://vitepress.yiov.top/logo.png
        irregular: true
        descr: 一个轻量、简洁高效、灵活配置,易于扩展的 VitePress 主题
        tag: Teek官网        
  - title: VitePress
    # desc: "建站中学习和使用了以下博客/网站的技术和分享,特别鸣谢!💖"
    list:
      - name: vitepress-theme-teek
        link: https://vp.teek.top/
        avatar: https://vitepress.yiov.top/logo.png
        irregular: true
        descr: 一个轻量、简洁高效、灵活配置,易于扩展的 VitePress 主题
        tag: Teek官网
      - name: VitePress
        link: https://vitepress.dev/zh/
        avatar: https://vitepress.dev/vitepress-logo-mini.svg
        irregular: true
        descr: 由 Vite 和 Vue 驱动的静态站点生成器
  - title: Teek道友
    # desc: "聚集众多优秀独立博客,随机传送 \U0001F680"
    list:
      - name: 天客 - Teeker
        descr: 朝圣的使徒,正在走向编程的至高殿堂!(Teek作者,大佬666)
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/notes.teek.top.png
        link: https://notes.teek.top/  
        tag: Teek作者
        irregular: true
      - name: One 
        descr: 明心静性,爱自己 
        avatar: https://img.onedayxyy.cn/images/xyy-logo.ico 
        link: https://onedayxyy.cn/ 
        irregular: true
      - name: Hyde Blog
        descr: 前端大佬,666 
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/hyde.webp
        link: https://teek.seasir.top/
        badge: 前端大佬
        irregular: true
      - name: 二丫讲梵 
        descr: 💻学习📝记录🔗分享 
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/eryajiangfan.png 
        link: https://jenkinsguide.opsre.top/ 
        # link: https://wiki.eryajf.net/ 
        irregular: true
      - name: 威威 Blog
        descr: teek伙伴
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/dl-web.top.png
        link: https://dl-web.top/
        irregular: true
        tag: Teek开发者2号
      - name: W3C
        descr: teek伙伴
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/teek.w3c.cool.svg
        link: https://teek.w3c.cool/
        irregular: true         
      - name: 时光驿站
        descr: 干活满满的技术笔记
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/kandu.cxcare.top.svg
        link: https://kandu.cxcare.top/ 
        irregular: true
      - name: 白木🗼共灵知塔
        descr: 前端大佬
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/baimu.live.png
        link: https://baimu.live/
        irregular: true          
      - name: 白木
        descr: SCUM中文服务器后台说明文档
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/st.samgame.cn.png
        link: https://st.samgame.cn/
        irregular: true             
      - name: 凿壁偷光不算偷 Blog
        descr: teek道友 
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/sinc-us-kg-favicon.ico 
        link: https://sinc.us.kg/  
        irregular: true
      - name: SnowLin Blog
        descr: 喵喵(? 
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/SnowLinBlog-logo.png 
        link: https://blog.snowlinlan.com/
        irregular: true
      - name: 心流笔记
        descr: 坚持 & 汲取 & 分享(一个记录生活与学习过程中灵感和感悟的空间)
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/blog.wilsonzy.cn.ico
        link: http://blog.wilsonzy.cn/c/StreamNotes/
        irregular: true
      - name: 科技语者
        descr: 拥抱科技奔赴星辰大海
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/www.uopy.top.ico
        link: https://www.uopy.top/
        irregular: true
      - name: OpForge
        descr: 运维锻造,知识沉淀
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/opforge.srebro.cn.png
        link: https://opforge.srebro.cn/
        irregular: true
      - name: 乔克视界
        descr: 云原生爱好者
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/jokerbai.com.ico
        link: https://jokerbai.com/
        irregular: true
      - name: 记得勇敢
        descr: 常用组件及代码封装
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/vp.xiaoying.org.cn.png
        link: https://vp.xiaoying.org.cn/
        irregular: true
      - name: 骏骏骏的博客
        descr: 后端程序员
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/blog.azhxsn.cn.svg
        link: https://blog.azhxsn.cn/
        irregular: true
      - name: MaraPython
        descr: 行而不辍,未来可期
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/marapython.com.png
        link: https://marapython.com/
        irregular: true
      - name: Mujin Blog
        descr: 只要一息尚存,就还有希望
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/blog.charily.top.ico
        link: https://blog.charily.top/
        irregular: true              
  - title: 张洪Heo风格博客
    # desc: "聚集众多优秀独立博客,随机传送 \U0001F680"
    list:
      - name: 王嘉祥
        link: https://blog.jiaxiang.wang/
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/blog.jiaxiang.wang.webp
        descr: 唱响科普和人生兴事,分享科技与美好生活(rust写的zola主题,移植于张洪heo)
        irregular: true
  - title: 简约唯美风格博客
    # desc: "聚集众多优秀独立博客,随机传送 \U0001F680"
    list:
      - name: 浩瀚星河
        link: https://www.golangblog.com/
        avatar: https://img.onedayxyy.cn/images/Teek/Teekwebsite/www.golangblog.com.jpeg
        descr: 一个程序员的碎碎念
        irregular: true
  - title: 技术类
    # desc: "聚集众多优秀独立博客,随机传送 \U0001F680"
    list:
      - name: 25196
        link: https://www.yangworld.top/
        avatar: https://q1.qlogo.cn/g?b=qq&nk=2779643212&s=640
        descr: 互联网上的小窝
        irregular: true  
      - name: 杨工子
        link: https://www.yangmufa.cn
        avatar: https://img.yangmufa.cn/roc-blog/8c492941ce294ff597feb51f4fd8ff92.jpg
        descr: 我是杨工子;练习编程两年半,C#.Net全栈工程师、RocBlog(鲲鹏博客)作者。
        irregular: true                       
categories:
  - 关于
coverImg: https://img.onedayxyy.cn/images/TeekCover/1.webp
---

4.配置评论

(1)注释掉评论:

编辑docs\.vitepress\config.ts文件:

ts
  // // 评论配置
  // comment: {
  //   provider: "twikoo",
  //   options: CommentData,
  // },

(2)安装twikoo评论组件

bash
pnpm add twikoo

(3)引入评论组件

编辑docs\.vitepress\theme\components\TeekLayoutProvider.vue文件:

ts
import Twikoo from './Twikoo.vue' //评论组件


    <!-- 评论组件 -->
    <!-- <template #doc-after> -->
    <template #teek-doc-after-appreciation-after>
      <Twikoo />
    </template>

具体信息可见我开源库:https://cnb.cool/onedayxyy/vitepress-theme-teek-one-public

(4)解决首页个人简介里头像异常问题

编辑docs\index.md文件:

md
.tk-avatar {
    height: var(--tk-avatar-size) !important;
    width: var(--tk-avatar-size) !important;
}
.tk-avatar--circle {
    border-radius: 50% !important;
}

image-20251023100425060

5.组件效果

http://localhost:5173/about/friend-links

在线测试:https://onedayxyy.cn/about/friend-links

image-20251022171201539

image-20251022171228091

结束。

最近更新