Nuxt3重写博客(二):Vue3封装各种卡片(自取哈)

1,201 阅读8分钟

今日文章将讲述如何实现一个博客首页的各种卡片组件,包括基础卡片、专题卡片、文章卡片、热搜词条卡片、七日热门卡片、网站浏览卡片、动态卡片以及版本更新卡片。这些组件使用了Vue 3和Nuxt 3框架的特性,例如Composition API、<script setup>语法糖、响应式引用(ref)和具名插槽。

上一张传送门 @ Nuxt3重写博客(一):首页设计和Vue3导航封装

img_v3_02au_1241db53-5f3a-4db3-853c-bfe802f5295g.jpg

【设计图镇楼】

基础卡片

  • 用途:作为其他所有卡片的基础容器。
  • 关键点:支持标题、更多链接和底部分隔线的可选显示。
<template>
  <div class="base-card">
    <div
      v-if="title || path"
      class="base-card-title"
      :class="[lineShow ? 'title-line-show' : '']"
    >
      <div v-if="title" class="title">
        {{ title }}
      </div>
      <div v-if="path" class="more">更多</div>
    </div>
    <div class="p2" v-if="!(title || path)"></div>
    <div class="base-card-text">
      <slot> </slot>
    </div>
  </div>
</template>
<script lang="ts" setup>
const props = withDefaults(
  defineProps<{
    title?: string;
    path?: string;
    lineShow?: boolean;
  }>(),
  {
    lineShow: false,
  }
);
</script>

<style>
.base-card {
  background-color: #fff;
  border-radius: 5px;
  width: 100%;
  box-shadow: 0 1px 4px rgb(21 26 48 / 8%);
  box-sizing: border-box;
}
.base-card + .base-card {
  margin-top: 15px;
}

.base-card-title {
  margin: 0 15px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 60px;
}

.base-card-title .title {
  font-size: 16px;
  font-weight: 600;
  line-height: 24px;
  padding: 20px 0 16px 0;
  color: #292929;
}

.title-line-show {
  border-bottom: 1px solid #e0e0e0;
}

.base-card-text {
  margin: 0 15px;
}
.base-card .more {
  cursor: pointer;
}
.p2 {
  width: 100%;
  height: 2rem;
  line-height: 2rem;
}
</style>

入参

1. title

  • 类型: string (可选)
  • 用途: 用于显示卡片的标题。
  • 示例:
    <BaseCard title="卡片标题">
      <p>这是卡片的内容。</p>
    </BaseCard>
    
    在这个示例中,卡片的标题会显示为“卡片标题”。

2. path

  • 类型: string (可选)
  • 用途: 用于显示一个“更多”链接,通常用于导航到更多内容。
  • 示例:
    <BaseCard title="卡片标题" path="/more">
      <p>这是卡片的内容。</p>
    </BaseCard>
    
    在这个示例中,卡片的右上角会显示一个“更多”链接,点击后会导航到 /more 路径。

3. lineShow

  • 类型: boolean (可选)
  • 默认值: false
  • 用途: 控制标题下方是否显示一条分隔线。
  • 示例:
    <BaseCard title="卡片标题" :lineShow="true">
      <p>这是卡片的内容。</p>
    </BaseCard>
    
    在这个示例中,卡片标题下方会显示一条分隔线,因为 lineShow 被设置为 true

组合示例

以下是一个组合使用所有三个属性的示例:

<template>
  <div class="home">
    <div class="home-left">
      <card-base title="滚动推荐">
        <ul style="padding: 0 10px 10px 10px">
          <li>推荐轮播</li>
          <li>推荐轮播</li>
          <li>推荐轮播</li>
          <li>推荐轮播</li>
          <li>推荐轮播</li>
          <li>推荐轮播</li>
          <li>推荐轮播</li>
        </ul>
      </card-base>
      <card-base title="活动/专栏/专题">
        <ul style="padding: 0 10px 10px 10px">
          <li>活动1</li>
          <li>专栏1</li>
          <li>专题1</li>
        </ul>
      </card-base>
      <card-base title="最新文章" path="/article">
        <ul style="padding: 0 10px 10px 10px">
          <li>文章1</li>
          <li>文章1</li>
          <li>文章1</li>
          <li>文章1</li>
          <li>文章1</li>
          <li>文章1</li>
        </ul>
      </card-base>
    </div>
    <div class="home-right">
      <card-base title="用户工作台">
        <ul style="padding: 15px 20px">
          用户工作台
        </ul>
      </card-base>
      <card-base title="热搜词条">
        <ul style="padding: 15px 20px">
          热搜词条
        </ul>
      </card-base>
      <card-base title="网站流量">
        <ul style="padding: 15px 20px">
          网站流量
        </ul>
      </card-base>
      <card-base title="动态">
        <ul style="padding: 15px 20px">
          动态
        </ul>
      </card-base>
      <card-base title="更新">
        <ul style="padding: 15px 20px">
          更新
        </ul>
      </card-base>
    </div>
  </div>
</template>

<style>
/* 全局样式 */
.pc .home {
  display: flex;
  justify-content: space-between;
}
.home-left {
  flex: 0 0 960px;
}
.home-right {
  flex: 0 0 300px;
}
</style>

在这个示例中:

  • title 属性设置为 "示例卡片",所以卡片会显示这个标题。
  • path 属性设置为 "/more",所以卡片右上角会显示一个“更多”链接,点击后会导航到 /more 路径。
  • lineShow 属性设置为 true,所以卡片标题下方会显示一条分隔线。

视觉效果总结

  • 如果 title 存在,卡片顶部会显示标题。
  • 如果 path 存在,标题右侧会显示一个“更多”链接。
  • 如果 lineShowtrue,标题下方会显示一条分隔线。
  • 如果 titlepath 都不存在,标题区域会被一个占位符替代。

这些属性使得 BaseCard 组件非常灵活,可以根据不同的需求进行配置和使用。

image-20240516160354693.png

专题卡片

  • 用途:展示特定主题的媒体(图片或视频)、标题和一系列相关文章。
  • 关键点:媒体类型的动态处理,文章列表的渲染。
<template>
  <div class="topic-card">
    <div class="media-container">
      <img
        v-if="topic.mediaType === 'image'"
        :src="topic.mediaSrc"
        alt="topic media"
      />
      <video v-if="topic.mediaType === 'video'" controls>
        <source :src="topic.mediaSrc" type="video/mp4" />
        Your browser does not support the video tag.
      </video>
      <div class="duration" v-if="topic.duration">{{ topic.duration }}</div>
      <div class="topic-title-overlay">{{ topic.title }}</div>
    </div>
    <div class="topic-content">
      <ul class="article-list">
        <li v-for="(article, index) in topic.articles.slice(0, 3)" :key="index">
          <p class="topic-description">
            <iconpark-icon
              class="text-blue margin-right-l"
              name="dot"
            ></iconpark-icon>
            <span class="article-title">{{ article.title }}</span>
          </p>
          <div class="topic-author">
            作者: <span>{{ article.author }}</span>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script lang="ts" setup>
interface Article {
  title: string;
  link: string;
  author: string;
}

interface Topic {
  mediaType: "image" | "video";
  mediaSrc: string;
  title: string;
  description: string;
  author: string;
  duration?: string;
  articles: Article[];
}

const props = defineProps<{
  topic: Topic;
}>();
</script>

<style scoped>
.topic-card {
  border: 1px solid #ddd;
  border-radius: 4px;
  overflow: hidden;
  width: 320px;
  margin-bottom: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: transform 0.2s;
}

.topic-card:hover {
  transform: translateY(-5px);
}

.media-container {
  position: relative;
  width: 100%;
  height: 180px;
  overflow: hidden;
}

.media-container img,
.media-container video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.duration {
  position: absolute;
  top: 10px;
  right: 10px;
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
  padding: 2px 5px;
  border-radius: 3px;
  font-size: 12px;
}

.topic-title-overlay {
  position: absolute;
  bottom: 0;
  width: 100%;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  text-align: center;
  padding: 10px;
  font-size: 16px;
  font-weight: bold;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topic-content {
  padding: 15px;
}

.topic-description {
  font-size: 14px;
  margin: 0 0 10px;
  color: #555;
  display: flex;
  align-items: center;
  line-height: 1.2rem;
}

.topic-author {
  font-size: 12px;
  color: #999;
  margin-bottom: 10px;
  margin-left: 12px;
}

.article-list {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

.article-list li {
  margin-bottom: 10px;
}

.article-title {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  cursor: pointer;
}

.article-title:hover {
  text-decoration: underline;
}
</style>

文章卡片

  • 用途:在之前的文章中已经介绍,用于展示新闻或文章摘要。
  • 关键点:插槽的使用,使得组件内容高度可定制。

这卡片就是之前文章vue3 插槽一览和封装新闻卡片中的,就不费劲搬过来了

热搜词条卡片

  • 用途:展示热门搜索词条,支持输入框搜索。
  • 关键点:热门词条的动态样式处理,搜索功能的实现。
<template>
  <div class="search-card">
    <div>
      <input
        v-model="searchQuery"
        type="text"
        placeholder="Search..."
        class="search-input"
      />
    </div>
    <ul class="hot-search">
      <li
        v-for="item in hotSearchItems"
        :key="item.name"
        :class="{ hot: item.hot }"
        class="hot-item"
      >
        <span v-if="item.hot">🔥</span> {{ item.name }}
      </li>
    </ul>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

interface HotSearchItem {
  name: string;
  hot: boolean;
}

const searchQuery = ref("");
const hotSearchItems = ref<HotSearchItem[]>([
  { name: "Redis", hot: true },
  { name: "Node.js", hot: false },
  { name: "Vue.js", hot: true },
  { name: "Python", hot: false },
  { name: "JavaScript", hot: true },
]);
</script>

<style scoped>
.search-card {
  padding: 10px;
}

.search-input {
  width: 100%;
  padding: 5px;
  margin-bottom: 10px;
  border: 1px solid #ddd;
  border-radius: 3px;
}

.hot-search {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}
.hot-search li {
  line-height: 2rem;
}

.hot-item {
  margin-right: 10px;
  display: flex;
  align-items: center;
}

.hot-item.hot {
  color: red;
}
</style>

七日热门卡片

  • 用途:展示一周内热门文章或内容。
  • 关键点:列表渲染,数据的动态处理。
<template>
  <div class="hot-articles-card">
    <ul>
      <li v-for="(article, index) in articles" :key="index">
        <p>【{{ article.type }}】</p>
        {{ article.title }}
      </li>
    </ul>
  </div>
</template>

<script lang="ts" setup>
interface Article {
  type: string;
  title: string;
  link: string;
}
withDefaults(
  defineProps<{
    articles?: Article[];
  }>(),
  {
    articles: () => [
      { type: "Node.js", title: "Redis 12.0 发布", link: "#" },
      { type: "Python", title: "Python 在科学计算中的应用", link: "#" },
      { type: "JavaScript", title: "Vue 8.0 发布", link: "#" },
      { type: "JavaScript", title: "Deno 2.0 正式版发布", link: "#" },
      { type: "Python", title: "TensorFlow 3.0 发布预告", link: "#" },
    ],
  }
);
</script>

<style scoped>
.hot-articles-card {
  padding: 5px 0 10px 0;
}

.hot-articles-card ul {
  list-style-type: none;
  padding: 0;
}

.hot-articles-card ul li {
  max-width: 270px;
  margin-bottom: 5px;
  display: flex;
  text-overflow: ellipsis;
  overflow: hidden;
  word-break: break-all;
  white-space: nowrap;
}

.hot-articles-card li p {
  text-decoration: none;
  color: #007bff;
  margin-right: 5px;
}
</style>

网站流量卡片

  • 用途:展示网站的流量统计信息。
  • 关键点:统计信息的布局和样式处理。
<template>
  <div class="traffic-stats">
    <div class="stats">
      <div class="stat">
        <h3>今日 PV</h3>
        <p class="text-blue">{{ todayPV }}</p>
      </div>
      <div class="stat">
        <h3>今日 UV</h3>
        <p class="text-cyan">{{ todayUV }}</p>
      </div>
      <div class="stat">
        <h3>10分钟在线</h3>
        <p class="text-green">{{ tenMinOnline }}</p>
      </div>
      <div class="stat">
        <h3>历史</h3>
        <p>
          <span class="text-blue">{{ historicalPV }}</span
          >/<span class="text-cyan">{{ historicalUV }}</span>
        </p>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { withDefaults, defineProps } from "vue";

const props = withDefaults(
  defineProps<{
    todayPV?: number;
    todayUV?: number;
    tenMinOnline?: number;
    historicalPV?: number;
    historicalUV?: number;
  }>(),
  {
    todayPV: 0,
    todayUV: 0,
    tenMinOnline: 0,
    historicalPV: 0,
    historicalUV: 0,
  }
);
</script>

<style scoped>
.traffic-stats {
  padding: 10px 0;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

.stat {
  flex: 1 1 100px;
  text-align: center;
}

h2 {
  margin-bottom: 20px;
}

h3 {
  margin-bottom: 10px;
}

p {
  font-size: 1.5em;
  font-weight: bold;
}
</style>

动态卡片

  • 用途:展示网站或用户的最新动态。
  • 关键点:动态类型的样式区分,链接的处理。
<template>
  <div class="activity-card">
    <ul>
      <li v-for="(activity, index) in activities" :key="index">
        <div class="type" :class="activity.type">
          {{ activity.type }}
        </div>
        <a
          :href="activity.link"
          :aria-label="`${activity.type}: ${activity.content}`"
        >
          {{ activity.content }}
        </a>
      </li>
    </ul>
  </div>
</template>

<script lang="ts" setup>
interface Activity {
  type: string;
  content: string;
  link: string;
}

withDefaults(
  defineProps<{
    activities?: Activity[];
  }>(),
  {
    activities: () => [
      { type: "更新文章", content: "Redis 6.0 发布", link: "#" },
      { type: "发布文章", content: "Python 在科学计算中的应用", link: "#" },
      { type: "添加分类", content: "JavaScript 分类", link: "#" },
      { type: "添加标签", content: "Deno 标签", link: "#" },
      { type: "更新文章", content: "TensorFlow 3.0 发布预告", link: "#" },
    ],
  }
);
</script>

<style scoped>
.activity-card {
  padding: 0;
  font-family: Arial, sans-serif;
}

.activity-card ul {
  list-style-type: none;
  padding: 0;
}

.activity-card ul li {
  line-height: 2rem;
  display: flex;
  text-overflow: ellipsis;
  overflow: hidden;
  word-break: break-all;
  white-space: nowrap;
  width: 270px;
}

.activity-card a {
  text-decoration: none;
  color: #007bff;
}

.activity-card a:hover {
  text-decoration: underline;
}

.type {
  font-weight: bold;
  margin-right: 5px;
}

.type.更新文章 {
  color: #28a745;
}

.type.发布文章 {
  color: #17a2b8;
}

.type.添加分类 {
  color: #ffc107;
}

.type.添加标签 {
  color: #fd7e14;
}
</style>

版本更新卡片

  • 用途:公告网站或应用的最新版本更新信息。
  • 关键点:列表渲染更新内容,标题和日期的展示。
<template>
  <div class="update-card">
    <h2>{{ title }}</h2>
    <p>{{ date }}</p>
    <ul>
      <li v-for="(item, index) in updates" :key="index">{{ item }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "UpdateCard",
  props: {
    title: {
      type: String,
      default: "个人博客开源 V0.0.2",
    },
    date: {
      type: String,
      default: "2024-05-17",
    },
    updates: {
      type: Array,
      default: () => [
        "添加导航",
        "添加基础卡片和各种模块卡片UI封装",
        "引入了color-ui的配色方案",
        "重构了布局",
      ],
    },
  },
};
</script>

<style>
.update-card {
  padding: 10px;
  font-family: Arial, sans-serif;

总结

image.png

【成品镇楼图】

今天这些组件共同构成了一个功能丰富、视觉吸引的博客首页。通过组件化的方式,每个部分都可以独立开发和维护,极大地提高了代码的可重用性和项目的可维护性。在实现这些组件时,关键在于理解Vue 3的Composition API、组件传参、插槽使用等核心概念,以及如何将设计稿转化为实际的代码实现。

希望这篇文章能够帮助你学习和掌握这些重要的前端开发技能。