知乎日报
项目呈现
首页
文章详情页
评论区
个人中心
我的收藏
搭建项目架子,完成相应配置
基于vue脚手架,快速创建vue3的项目
- 使用pnpm命令行,执行如下命令,pnpm create vue 快速创建项目
- 使用git init 初始化本地仓库,并在个人的GitHub上创建远程仓库,使用git命令连接两个仓库
- 引入vant组件库用于开发页面布局,使用第三方插件postcss实现项目的vw适配
相应配置
- 基于Eslint和prettier插件实现项目代码规范化
- 配置vant的按需自动引入
- 修改项目目录,适应项目的开发需求
创建相关模块,配置相应路由
跨域问题
-
在访问知乎日报接口时,遇到了跨域问题,通过在vite.config.js文件中配置,解决跨域问题
-
在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}`
}