vue3仿写知乎日报

知乎日报

项目呈现

首页

在这里插入图片描述

文章详情页在这里插入图片描述

评论区

在这里插入图片描述

个人中心

在这里插入图片描述

我的收藏

在这里插入图片描述

搭建项目架子,完成相应配置

基于vue脚手架,快速创建vue3的项目

  1. 使用pnpm命令行,执行如下命令,pnpm create vue 快速创建项目
  2. 使用git init 初始化本地仓库,并在个人的GitHub上创建远程仓库,使用git命令连接两个仓库
  3. 引入vant组件库用于开发页面布局,使用第三方插件postcss实现项目的vw适配

相应配置

  1. 基于Eslint和prettier插件实现项目代码规范化
  2. 配置vant的按需自动引入
  3. 修改项目目录,适应项目的开发需求

创建相关模块,配置相应路由在这里插入图片描述

跨域问题

  1. 在访问知乎日报接口时,遇到了跨域问题,通过在vite.config.js文件中配置,解决跨域问题

    在这里插入图片描述

  2. 在request.js中将基地址改为‘baseurl’

在这里插入图片描述

封装请求模块

import axios from 'axios'
// Toast
import { showLoadingToast, closeToast } from 'vant'
import 'vant/es/toast/style'
const request = axios.create({
  baseURL: '/baseurl',
  timeout: 10000
})

// 添加请求拦截器
request.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    showLoadingToast({
      message: '加载中...',
      forbidClick: true,
      duration: 0
    })
    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)
// 添加响应拦截器
request.interceptors.response.use(
  function (response) {
    // 对响应数据做点什么
    // 数据响应回来后关闭showLoadingToast
    closeToast()
    return response
  },
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error)
  }
)
export default request

知乎日报-首页模块

首页的基本静态结构

<template>
  <!-- 顶部导航栏是固定定位 -->
  <div class="header">
    <div class="date" @click="backTop">
      <span class="day" style="color: black">{{ day }}</span>
      <span class="month">{{ month }}月</span>
    </div>

    <div class="title">| 知乎日报</div>
    <div class="image">
      <img :src="image" alt="" />
    </div>
  </div>

  <van-swipe
    class="my-swipe"
    :autoplay="4000"
    lay-render
    indicator-color="white"
  >
    <van-swipe-item
      v-for="item in imageList"
      :key="item.id"
      style="height: 100vw; width: 100vw; position: relative"
      @click="MouseEvent(item.id)"
    >
      <img :src="item.image" style="width: 100vw" />
      <div
        class="title"
        style="color: #fff; position: absolute; top: 57vw; left: 4vw"
      >
        {{ item.title }}
      </div>
      >
      <div
        class="author"
        style="
          color: #fff;
          position: absolute;
          top: 62vw;
          left: 4vw;
          font-size: 3vw;
        "
      >
        {{ item.hint }}
      </div>
    </van-swipe-item>
  </van-swipe>
  <!-- 文章列表 -->
  <articleList
    :list="list"
    :date="date"
    @update:list="updateList"
  ></articleList>
</template>

<style lang="scss" scoped>
.header {
  width: 100vw;
  height: 14vw;
  display: flex;
  position: fixed;
  z-index: 999;
  top: 0vw;
  background-color: #fff;

  .date {
    position: relative;
    display: flex;
    flex: 1;
    flex-direction: column;
    text-align: center;
    justify-content: center;
    margin-left: 2vw;
    .day {
      font-size: 6vw;
    }
    .month {
      font-size: 3vw;
    }
  }
  .title {
    flex: 6;
    font-size: 7vw;
    // background-color: aquamarine;
    line-height: 14vw;
    color: black;
    font-weight: bold;
  }
  .image {
    flex: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    img {
      width: 12vw;
      border-radius: 50%;
    }
  }
}
.my-swipe .van-swipe-item {
  color: #fff;
  font-size: 20px;
  line-height: 60vw;
  text-align: center;
  background-color: #39a9ed;
}
</style>

首页的文章列表封装成一个组件

// 1. 封装获取文章列表的接口
// 获取文章最新列表数据
export const getArticelLaest = () => {
  return request.get('/news/latest')
}

// 获取往日文章列表
export const getArticelbefore = (date) => {
  return request.get(`/news/before/${date}`)
}
文章列表的静态样式
// articleList的静态样式
<template>
  <ul>
    <van-list
      v-model:loading="loading"
      :finished="finished"
      finished-text="没有更多了"
      @load="onLoad"
    >
      <!-- <van-cell v-for="item in list" :key="item" :title="item" /> -->
      <template v-for="(item, index) in list" :key="item.id">
        <div class="date" v-if="index % 5 === 0 && index > 0">
          {{ item.date.substring(4, 6) }}月{{ item.date.substring(6, 8) }}日
          ————————————————————
        </div>
        <div class="main" @click="goDetail(item.id)">
          <div class="left" style="overflow: hidden">
            <div class="title">
              {{ item.title }}
            </div>
            <div class="author">{{ item.hint }}</div>
          </div>
          <div class="right">
            <img :src="item.images[0]" alt="" />
          </div>
        </div>
      </template>
    </van-list>
  </ul>
</template>

<style lang="scss" scoped>
.date {
  width: 100vw;
  font-size: 4vw;
  color: #999;
}
.main {
  width: 100vw;
  height: 20vw;
  padding: 4vw;
  display: flex;
  margin-bottom: 8vw;
  .left {
    flex: 9;
    height: 20vw;
    .title {
      width: 70vw;
      font-size: 5vw;
      height: 15vw;
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2; /* 指定显示的行数 */
      line-height: 1.5; /* 指定行高 */
      max-height: calc(1.5 * 2); /* 计算最大高度,需与行数和行高相对应 */
      text-overflow: ellipsis;
      overflow: hidden;
    }
    .author {
      font-size: 3vw;
    }
  }
  .right {
    flex: 3;
    display: flex;
    flex-direction: column;
    align-items: end;
    img {
      flex: 1;
      width: 20vw;
    }
  }
}
</style>

文章列表采用vant-ui中的List列表,实现下滑刷新功能
const emits = defineEmits(['update:list'])
// 设置一个时间来计数
let time = 0
const today = new Date()
// 当组件滚动到底部时,触发onLoad事件,进行数据更新
// 此处直接进行数据请求,然后通过子传父,通知父组件进行数据更新list数组,再通过defineProps进行接收
// 获取到往日消息,通过子传父,更新list数组
const onLoad = async () => {
  // 此处是设置求几天前的函数
  const before = new Date(today)
  before.setDate(today.getDate() - time) // 减一求得就是昨天的数据,以此类推
  const year = before.getFullYear()
  const month = (before.getMonth() + 1).toString().padStart(2, '0')
  const day = before.getDate().toString().padStart(2, '0')

  const formattedDate = year + month + day
  // console.log(formattedDate) // 输出格式化后的日期字符串,例如:20231202
  const res = await getArticelbefore(formattedDate)
  // 通知父组件更新list数组,子组件是通过list来同台渲染数据的
  emits('update:list', res.data.stories, formattedDate)
  // 加载状态结束
  loading.value = false
  time++
}
通过路由跳转到文章详情页-动态路由传参
const goDetail = (id) => {
  //动态路由传参
  router.push(`/article/detail/${id}`)
}

文章详情页的静态样式

<template>
  <div
    class="body"
    ref="body"
    v-touch:swipe.left="SwipeLeft"
    v-touch:swipe.right="SwipeRight"
  ></div>
  <div class="footer">
    <div class="footer-left">
      <van-icon name="arrow-left" @click="goback" />
    </div>
    <div
      class="footer-commont"
      @click="router.push(`/comments/${route.params.id}`)"
    >
      <van-icon name="comment-o" :badge="comments" />
    </div>
    <div class="footer-good">
      <van-icon name="good-job-o" :badge="likes" />
    </div>
    <div class="footer-collect" @click="goCollect">
      <van-icon name="star" v-if="collect" />
      <van-icon name="star-o" v-else />
    </div>
  </div>
</template>

// 样式
<style lang="scss" scoped>
// 给headline样式设置important,保证样式优先级

.headline {
  width: 100vw !important;
  height: 45vw !important;
}
.body {
  margin-bottom: 20vw;
}

.footer {
  width: 100vw;
  height: 12vw;
  background-color: #fefefe;
  z-index: 9999999999999;
  display: flex;
  position: fixed;
  bottom: 0;
  font-size: 5vw;
  justify-content: center;
  align-items: center;
  color: #5e5d5d;
  .footer-left {
    flex: 1;
  }
  .footer-commont {
    flex: 1;
  }
  .footer-good {
    flex: 1;
  }
  .footer-collect {
    flex: 1;
  }
}
</style>
详情页api设置
// 获取文章内容详情
export const getArticledetail = (id) => {
  return request.get(`/news/${id}`)
}

// 获取文章额外参数
export const getArticelcomment = (id) => {
  return request.get(`/story-extra/${id}`)
}

文章内容详情获取到的是html格式的内容和在线的样式表
// 通过原生js调整获取到的文章内容
const getPage = async () => {
  const res = await getArticledetail(route.params.id)
  // console.log(res)
  id.value = res.data.id
  Title.value = res.data.title
  image.value = res.data.image
  apiStyle.value = res.data.css
  const linkElement = document.createElement('link')
  // 引入外部css样式
  linkElement.rel = 'stylesheet'
  linkElement.href = apiStyle.value
  document.head.appendChild(linkElement)
  body.value.innerHTML = res.data.body
  const imageDiv = document.querySelector('.img-place-holder')
  // console.log(imageDiv)
  imageDiv.style.height = '100vw'
  const img = document.createElement('img')
  img.src = res.data.image
  imageDiv.appendChild(img)
  img.style.height = '100vw'
  const title = document.createElement('h1')
  title.innerText = res.data.title
  imageDiv.appendChild(title)
  imageDiv.style.position = 'relative'
  title.style.position = 'absolute'
  title.style.top = '82vw'
  title.style.left = '4vw'
  title.style.color = '#fff'
}
// 在onmounted中发送请求,获取数据
onMounted(() => {
  getPage()
})

// 底部定位
const goback = () => {
  router.go(-1)
}
// 新闻额外信息
// 评论总数
const comments = ref('')
// 点赞总数
const likes = ref('')
const getExtraInfo = async () => {
  const res = await getArticelcomment(route.params.id)
  // console.log(res)
  comments.value = res.data.comments
  likes.value = res.data.popularity
}
文章详情页的收藏功能-pinia持久化
// 使用pinia和pinia的持久化插件实现文章收藏功能
// 1. 创建collect仓库
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCollectStore = defineStore(
  'collect',
  () => {
    const collectList = ref([])
    const setCollect = (obj) => {
      collectList.value.push(obj)
    }
    const removeCollect = (id) => {
      collectList.value.forEach((item, index) => {
        if (item.id === id) {
          collectList.value.splice(index, 1)
        }
      })
    }
    return {
      setCollect,
      removeCollect,
      collectList
    }
  },
  {
    persist: true
  }
)

// 2. 在详情页中引入并使用

import {useCollectStore} from '@/stores'
const collectStore = useCollectStore()
// 设置id,title,和image的变量,为了收藏做准备
const id = ref('')
const Title = ref('')
const image = ref('')
// 开始先判断此文章是否被收藏
const isCollect = collectStore.collectList.some((item) => {
  return item.id === route.params.id
})
// console.log(isCollect)
let collect = ref(isCollect)
const goCollect = () => {
  // 如果一开始不是确认收藏
  collect.value = !collect.value
  if (collect.value) {
    const obj = {
      id: route.params.id,
      title: Title.value,
      image: image.value,
      isCollect: true
    }
    collectStore.setCollect(obj)
    showToast('收藏成功')
  } else {
    collectStore.removeCollect(route.params.id)
    showToast('取消收藏成功')
  }
}
文章详情页的手指滑动切换下一篇文章的功能
// 1. 创建id数组仓库,在首页下滑获取文章列表的同时获取到相应的文章id存储到仓库中
// 用来创建数组id的模块
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useArrayIdStore = defineStore('arrayId', () => {
  const arrayId = ref(0)
  const setArrayId = (newArrayId) => {
    arrayId.value = newArrayId
  }
  return {
    arrayId,
    setArrayId
  }
})

// 2. 使用id数组仓库
import { useArrayIdStore} from '@/stores'
const ArrayIdStore = useArrayIdStore()


// 3. 使用vue-touch插件来监听手指滑动屏幕的事件
// 手指从右向左滑动
const SwipeLeft = () => {
  const ProxyArray = Object.values(ArrayIdStore.arrayId)
  const index = ProxyArray.indexOf(+route.params.id)

  if (index < ProxyArray.length - 1) {
  // 使用replace防止产生过多路由历史记录
    router.replace(`/article/detail/${ProxyArray[index + 1]}`)
    // 重新渲染页面
    getPage()
    getExtraInfo()
  }
}
// 手指从左向右滑动
const SwipeRight = () => {
  const ProxyArray = Object.values(ArrayIdStore.arrayId)
  const index = ProxyArray.indexOf(+route.params.id)
  if (index > 0) {
    router.replace(`/article/detail/${ProxyArray[index - 1]}`)
    getPage()
    getExtraInfo()
  }
}

知乎日报-个人模块

个人模块的静态样式

<script setup>
import { useRouter } from 'vue-router'
import image from '@/asset/TX1582_05.jpg'
const router = useRouter()
</script>
<template>
  <van-nav-bar title="个人中心" left-arrow @click-left="router.go(-1)" />
  <div class="avatar">
    <img :src="image" alt="" />
    <span class="name">大大咧咧</span>
  </div>
  <div class="myCollect" @click="router.push('/collect')">
    <van-divider />
    <span>我的收藏</span>
    <van-icon name="arrow" class="left" />
    <van-divider />
  </div>
</template>
<style lang="scss" scoped>
.avatar {
  width: 100vw;
  height: 40vw;
  //   margin: 0 auto;
  img {
    width: 20vw;
    height: 20vw;
    border-radius: 50%;
    display: block;
    margin: 10vw auto;
  }
  .name {
    font-size: 6vw;
    color: black;
    font-weight: bold;
    display: block;
    width: 100vw;
    text-align: center;
    margin-top: -6vw;
  }
}
.myCollect {
  width: 100vw;
  height: 10vw;
  span {
    font-size: 4vw;
    margin-left: 3vw;
  }
  .left {
    float: right;
    font-size: 4vw;
    color: #d3d0d0;
    margin-right: 3vw;
  }
}
</style>

我的收藏-使用仓库里的数据渲染

<script setup>
import { useCollectStore } from '@/stores/index.js'
import { useRouter } from 'vue-router'
const router = useRouter()
const collectStore = useCollectStore()
const collectList = collectStore.collectList
console.log(collectList)
// 取消收藏
const closeCollect = (id) => {
  collectStore.removeCollect(id)
}
</script>
<template>
  <van-nav-bar title="我的收藏" left-arrow @click-left="router.go(-1)" />
  <ul>
    <li
      v-for="item in collectList"
      :key="item.id"
      @click="router.push(`/article/detail/${item.id}`)"
    >
      <van-swipe-cell>
        <div class="main">
          <div class="left" style="overflow: hidden">
            <div class="title">{{ item.title }}</div>
          </div>
          <div class="right">
            <img :src="item.image" alt="" />
          </div>
        </div>
        <van-divider />
        <template #right>
          <van-button
            square
            text="取消收藏"
            type="danger"
            class="delete-button"
            style="height: 100%"
            @click="closeCollect(item.id)"
          />
        </template>
      </van-swipe-cell>
    </li>
    <div class="finishText">没有更多内容</div>
  </ul>
</template>
<style lang="scss" scoped>
.finishText {
  width: 100vw;
  height: 15vw;
  font-size: 5vw;
  color: #999;
  text-align: center;
  line-height: 15vw;
  // margin-top: 10vw;
  background-color: #e8e8e8;
}
.main {
  width: 100vw;
  height: 20vw;
  padding: 4vw;
  display: flex;
  margin-bottom: 8vw;
  .left {
    flex: 9;
    height: 20vw;
    .title {
      width: 70vw;
      font-size: 5vw;
      height: 15vw;
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 2; /* 指定显示的行数 */
      line-height: 1.5; /* 指定行高 */
      max-height: calc(1.5 * 2); /* 计算最大高度,需与行数和行高相对应 */
      text-overflow: ellipsis;
      overflow: hidden;
    }
    .author {
      font-size: 3vw;
    }
  }
  .right {
    flex: 3;
    display: flex;
    flex-direction: column;
    align-items: end;
    img {
      flex: 1;
      width: 20vw;
    }
  }
  .span {
    width: 100vw;
    height: 1vw;
    text-align: center;
    margin-top: -3vw;
  }
}
</style>

知乎日报-评论区模块

评论区的静态样式

<template>
  <van-nav-bar
    style="position: fixed; top: 0; left: 0; right: 0; z-index: 1000"
    title="标题"
    left-arrow
    @click-left="router.go(-1)"
  >
    <template #title>
      <span>{{ TotalComments }}条评论</span>
    </template>
  </van-nav-bar>
  <div class="comments">
  // 长评论
    <div class="longcomments" v-if="longcomments.length > 0">
      <div class="title">{{ longcomments.length }}条长评</div>
      <ul>
        <li v-for="item in longcomments" :key="item.id">
          <div class="body">
            <div class="avatar">
              <img :src="item.avatar" alt="" />
            </div>
            <div class="main">
              <div class="author">{{ item.author }}</div>
              <div class="content" v-html="item.content"></div>
              <div class="reply" v-if="item.reply_to">
                <span
                  >//{{ item.reply_to.author }}:
                  <van-text-ellipsis
                    rows="1"
                    :content="item.reply_to.content"
                    expand-text="展开"
                    collapse-text="收起"
                /></span>
              </div>
              <div class="footer">
                <div class="time">
                  {{ formatDate(item.time) }}
                </div>
                <div class="likes">
                  <span style="margin-right: 2vw">{{ item.likes }}</span>
                  <van-icon name="good-job-o" />
                  <van-icon name="comment-circle-o" class="comment" />
                </div>
              </div>
              <van-divider />
            </div>
          </div>
        </li>
      </ul>
    </div>
    // 短评论
    <div class="shortcomments" v-if="shortcomments.length > 0">
      <div class="title">{{ shortcomments.length }}条短评</div>
      <ul>
        <li v-for="item in shortcomments" :key="item.id">
          <div class="body">
            <div class="avatar">
              <img :src="item.avatar" alt="" />
            </div>
            <div class="main">
              <div class="author">{{ item.author }}</div>
              <div class="content" v-html="item.content"></div>
              <div class="reply" v-if="item.reply_to">
                <span
                  >//{{ item.reply_to.author }}:
                  <van-text-ellipsis
                    rows="1"
                    :content="item.reply_to.content"
                    expand-text="展开"
                    collapse-text="收起"
                /></span>
              </div>
              <div class="footer">
                <div class="time">
                  {{ formatDate(item.time) }}
                </div>
                <div class="likes">
                  <span style="margin-right: 2vw">{{ item.likes }}</span>
                  <van-icon name="good-job-o" />
                  <van-icon name="comment-circle-o" class="comment" />
                </div>
              </div>
              <van-divider />
            </div>
          </div>
        </li>
      </ul>
    </div>
    <div class="total">
      <span>已显示全部评论</span>
    </div>
  </div>
</template>

// 样式
<style lang="scss" scoped>
.comments {
  margin-top: 15vw;
}
.title {
  font-size: 16px;
  font-weight: bold;
  color: #333;
  margin-bottom: 10px;
  margin-top: 1vw;
  margin-left: 2vw;
}
.body {
  width: 100vw;
  display: flex;
  margin-left: 2vw;
  // 可以解析多个换行符
  white-space: pre-line;
  .avatar {
    flex: 1;
    img {
      width: 8vw;
      height: 8vw;
      border-radius: 50%;
    }
  }
  .main {
    flex: 9;
    padding: 2vw 4vw 2vw 2vw;
    .author {
      margin-top: -1vw;
      margin-bottom: 1vw;
      font-weight: bold;
    }
    .reply {
      color: #b6b5b5;
      font-size: 4vw;
      .van-text-ellipsis {
        display: inline;
      }
    }

    .footer {
      display: flex;
      justify-content: space-between;
      text-align: center;
      .time {
        color: #b6b5b5;
        font-size: 4vw;
        margin-top: 3vw;
      }
      .likes {
        color: #b6b5b5;
        font-size: 4vw;
        margin-top: 3vw;
        margin-left: 45vw;
        .comment {
          margin-left: 5vw;
        }
      }
    }
  }
}
.total {
  width: 100vw;
  height: 20vw;
  text-align: center;
  line-height: 20vw;
  color: #b6b5b5;
}
</style>

评论区的逻辑处理

// 1. 封装api接口
// 获取新闻对应长评
export const getArticelcommentlong = (id) => {
  return request.get(`/story/${id}/long-comments`)
}

// 获取新闻对应短评
export const getArticelcommentshort = (id) => {
  return request.get(`/story/${id}/short-comments`)
}

// 2. 动态渲染
const getLongComments = async () => {
  const res = await getArticelcommentlong(route.params.id)
  // console.log(res.data.comments[0].content)
  longcomments.value = res.data.comments
  TotalComments.value += longcomments.value.length
}
getLongComments()
const getShortComments = async () => {
  const res = await getArticelcommentshort(route.params.id)
  // console.log(res.data.comments)
  shortcomments.value = res.data.comments
  TotalComments.value += shortcomments.value.length
}
getShortComments()

// 3. 由于api返回的是一段时间戳,需要一个格式化函数
// 将格式化时间戳
const formatDate = (time) => {
  const timestamp = time
  const date = new Date(timestamp * 1000)

  const month = date.getMonth() + 1
  const day = date.getDate()
  const hour = date.getHours()
  const minute = date.getMinutes()
  return `${month}-${day} ${hour}:${minute}`
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值