【全栈之巅】Node.js + Vue.js 全栈开发王者荣耀手机端官网和管理后台学习笔记(3.13-3.15)
本项目是 学习Bilibili 全栈之巅 视频教程相关源码和体会
https://gitee.com/blaunicorn/node-vue-wangzherongyao
持续更新中…
3.13 web页新闻资讯的数据接口
// server\models\Category.js
// 优化完善分类模型,建立虚拟联接
const mongoose = require('mongoose')
// 定义模型字段
const schema = new mongoose.Schema({
name: {
type: String,
required: true
},
parent: { type: mongoose.SchemaTypes.ObjectId, ref: 'Category' } // 类型为ObjectId 并关联Category表
})
// 3.13 设置虚拟字段:子分类,类似vue中的计算属性,它是通过已定义的schema属性的计算\组合\拼接得到的新的值
schema.virtual('children', {
localField: '_id', // 内键,schema对应的模型Title的_id
foreignField: 'parent', //外键,关联模型Category的parent键
justOne: false, // 只查询一条数据
ref: 'Category' // 关联的模型
})
// 3.13 分类关联新闻标题
schema.virtual('newsList', {
localField: '_id', // 内键,schema对应的模型Category的_id
foreignField: 'categories', //外键,关联模型Article的categories键
justOne: false, // 只查询一条数据
ref: 'Article' // 关联的模型
})
// 导出Category模型,哪里需要用,哪里引入,引入到 routes/admin/index.js
module.exports = mongoose.model('Category', schema)
// server\routes\web\index.js
// 新增 新闻列表接口,用于前端调用。以分类为主题,关联新闻
router.get('/news/list', async (req, res) => {
// //3.13 调出子分类,顺便调出子分类里的新闻,用populate关联,用lean展示出来,但存在问题不能查询单独的分类数量
// const parent = await Category.findOne({
// name: '新闻资讯'
// }).populate({
// path: 'children',
// populate: {
// path: 'newsList'
// }
// }).lean()
// 3.13 另一种方式,聚合查询,可以同时查询多次,聚合参数叫聚合管道
const parent = await Category.findOne({
name: '新闻资讯'
})
const cats = await Category.aggregate([
// 条件查询:字段 = 上级分类,找到分类,这一步与where查询没有太大区别
{ $match: { parent: parent._id } },
// 类似与关系数据库的关联联接,左关联联接left join
// 定义模型是,第三个参数collection省略了,它表示集合的名字,省略后默认是模型的复数小写。
// 从哪个集合,本地键,外键、as给起个名字
{ $lookup: { from: 'articles', localField: '_id', foreignField: 'categories', as: 'newsList' } },
// 定义要几个.添加\修改字段,特殊操作符slice
// 每一个分类只要5个
{ $addFields: { newsList: { $slice: ['$newsList', 5] } } }
]
)
const subCats = cats.map(v => v._id)
// 热门分类,是独立于四个分类,新增的,不限制分类,条件是 子分类是那些 in操作符会筛选出字段值等于制定数组中任何值的文档
cats.unshift({
name: '热门',
newsList: await Artice.find().where({
categories: { $in: subCats }
// 关联 categories字段,把_id拓展为名称
}).populate('categories').limit(5).lean()
})
// 把newsList上增加catergoryName,方便前端显示
cats.map(cat => {
cat.newsList.map(news => {
news.categoryName = cat.name === '热门' ? news.categories[0].name : cat.name
return news
})
return cat
})
res.send(cats)
})
3.14 web页首页新闻资讯界面展示
前端安装 axios 配置http.js 偷个懒,和admin一致,注意的是请求地址为
baseURL: 'http://localhost:3000/web/api'
// web端安装日期时间格式化工具
npm install dayjs --save
// 单行文字,多余的省略掉
// text overflow
.text-ellipsis {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
完整代码如下
// web\src\views\Home.vue
<template>
<div class="home">
<swiper ref="mySwiper" :options="swiperOptions">
<swiper-slide>
<img
class="w-100"
src="../assets/images/201.jpeg"
alt=""
sizes=""
srcset=""
/>
</swiper-slide>
<swiper-slide>
<img
class="w-100"
src="../assets/images/202.jpeg"
alt=""
sizes=""
srcset=""
/>
</swiper-slide>
<swiper-slide>
<img
class="w-100"
src="../assets/images/203.jpeg"
alt=""
sizes=""
srcset=""
/>
</swiper-slide>
<swiper-slide>
<img
class="w-100"
src="../assets/images/204.jpeg"
alt=""
sizes=""
srcset=""
/>
</swiper-slide>
<div
class="swiper-pagination pagination-home text-right px-3 pb-2"
slot="pagination"
></div>
</swiper>
<!-- end of swiper -->
<!-- 图标导航 -->
<!-- 外部容器text-center pt-3 内部容易mb-3 这样能统一样式 -->
<transition name="fade" mode="out-in">
<div
class="nav-icons bg-white mt-3 d-flex flex-wrap text-center pt-3 text-grey-1"
>
<!-- <div class="nav-icons bg-white mt-3 text-center pt-3 text-grey-1"> -->
<!-- <div class="nav-item mb-3" v-for="n in 10" :key="n">
<i class="sprite sprite-news"></i>
<div class="py-2">爆料站</div>
</div> -->
<!-- 处理底部的收起按钮,需要把d-flex flex-wrap 放在子集的div上,跟它平级写个收起的div -->
<div class="d-flex flex-wrap" :class="{ toggleActive: isCollapse }">
<a href class="nav-item my-3">
<i class="sprite sprite-news"></i>
<div>爆料站</div>
</a>
<a href class="nav-item my-3">
<i class="sprite sprite-practice"></i>
<div>故事站</div>
</a>
<a href class="nav-item my-3">
<i class="sprite sprite-affair"></i>
<div>周边商城</div>
</a>
<a href class="nav-item my-3 border-none">
<i class="sprite sprite-mall"></i>
<div>体验服</div>
</a>
<a href class="nav-item my-2">
<i class="sprite sprite-start"></i>
<div>新人专区</div>
</a>
<a href class="nav-item my-2">
<i class="sprite sprite-honour"></i>
<div>荣耀·传承</div>
</a>
<a href class="nav-item my-2">
<i class="sprite sprite-community"></i>
<div>同人社区</div>
</a>
<a href class="nav-item my-2 border-none">
<i class="sprite sprite-base"></i>
<div>王者营地</div>
</a>
<a href class="nav-item my-2">
<i class="sprite sprite-echart"></i>
<div>公众号</div>
</a>
<a href class="nav-item my-2">
<i class="sprite sprite-edition"></i>
<div>版本介绍</div>
</a>
</div>
<!-- <div class="d-flex flex-wrap"> -->
<div
class="bg-light py-2 fs-sm;"
style="width: 100%"
@click="switchActive"
>
<i
class="sprite sprite-arrow mr-1"
:style="{ transform: isCollapse ? 'rotate(180deg)' : '' }"
></i>
<span class="retract">{{ isCollapse ? '收起' : '展开' }}</span>
</div>
</div>
</transition>
<!-- end of 图标模块 -->
<!-- begin of 字体图标 -->
<!-- <i class="iconfont icon-news text-primary"></i> -->
<!-- end of 字体图标 -->
<!-- begin of 新闻资讯卡片 -->
<div class="card mt-3 p-3 bg-white">
<div class="card-header d-flex ai-center pb-3">
<i class="iconfont icon-caidananniudianji" style="color: deeppink"></i>
<div class="fs-xl flex-1 px-2">新闻资讯</div>
<i class="iconfont icon-menu"></i>
</div>
<div class="card-body pt-3">
<div class="nav jc-between">
<div class="nav-item active">
<div class="nav-link">热门</div>
</div>
<div class="nav-item">
<div class="nav-link">新闻</div>
</div>
<div class="nav-item">
<div class="nav-link">新闻</div>
</div>
<div class="nav-item">
<div class="nav-link">新闻</div>
</div>
<div class="nav-item">
<div class="nav-link">新闻</div>
</div>
</div>
<div class="pt-3">
<swiper>
<swiper-slide v-for="m in 5" :key="m"
><div class="py-2" v-for="n in 5" :key="n">
<span>[新闻]</span>
<span>|</span>
<span>春和景明柳垂莺娇,峡谷好礼随春报到</span>
<span>06/12</span>
</div></swiper-slide
>
</swiper>
</div>
</div>
</div>
<a-card icon="caidananniudianji" title="新闻资讯-全局组件">
<!-- 应用插槽展示数据 -->
<div class="nav jc-between">
<div class="nav-item active">
<div class="nav-link">热门</div>
</div>
<div class="nav-item">
<div class="nav-link">新闻</div>
</div>
<div class="nav-item">
<div class="nav-link">新闻</div>
</div>
<div class="nav-item">
<div class="nav-link">新闻</div>
</div>
<div class="nav-item">
<div class="nav-link">新闻</div>
</div>
</div>
<div class="pt-3">
<swiper>
<swiper-slide v-for="m in 5" :key="m"
><div class="py-2" v-for="n in 5" :key="n">
<span>[新闻]</span>
<span>|</span>
<span>春和景明柳垂莺娇,峡谷好礼随春报到</span>
<span>06/12</span>
</div></swiper-slide
>
</swiper>
</div>
</a-card>
<!-- 封装好的高级组件 -->
<m-list-card
icon="caidananniudianji"
title="新闻资讯-ListCard组件"
:categories="newsCats"
>
<!-- 在父组件里,不通过循环,直接拿到子组件里的具名slot的数据,
这样的好处是 子组件的内容可以由父组件决定怎么展示 -->
<template #items="{ category }">
<div
class="py-2 fs-lg d-flex"
v-for="(item, index) in category.newsList"
:key="index"
>
<span class="text-info" v-text="`[${item.categoryName}]`"
>[新闻]</span
>
<span class="px-1">|</span>
<span
class="flex-1 text-dark-1 text-ellipsis pr-2"
v-text="item.title"
>春和景明柳垂莺娇,峡谷好礼随春报到</span
>
<span class="text-grey-1 fs-sm">{{ item.createdAt | date }}</span>
</div>
</template>
<!-- <template v-slot:heros="{ category }"></template> -->
</m-list-card>
<m-card icon="caidananniudianji" title="新闻资讯-局部组件"></m-card>
<m-card icon="caidananniudianji" title="英雄列表"></m-card>
<m-card icon="caidananniudianji" title="精彩视频"></m-card>
<m-card icon="caidananniudianji" title="图文攻略"></m-card>
<!-- end of 新闻资讯卡片 -->
</div>
</template>
<script>
import dayjs from 'dayjs';
// @ is an alias to /src
import Card from '../components/Card';
export default {
filters: {
date(val) {
return dayjs(val).format('MM/DD');
},
},
name: 'Home',
components: { 'm-card': Card },
data() {
return {
swiperOptions: {
slidesPerView: 1,
autoplay: {
disableOnInteraction: false,
delay: 2000,
},
pagination: {
el: '.swiper-pagination',
clickable: true,
},
},
isCollapse: true,
// 定义ListCard组件的数据结构
// newsList的简单写法,新建数组5填充1,再map循环替换成对象
newsCats: [
{
_id: 1,
name: '热门',
newsList: new Array(5).fill(1).map((v) => ({
_id: v + 201,
categoryName: '赛事',
title: '景明柳垂莺娇,峡谷好礼随春报到',
date: '06/01',
})),
},
{
_id: 2,
name: '新闻',
newsList: new Array(5).fill(2).map((v) => ({
_id: v + 202,
categoryName: '赛事',
title: '景明柳垂莺娇,峡谷好礼随春报到',
date: '06/01',
})),
},
],
// 后台数据
// newsCats: [],
herosCats: [],
};
},
created() {
this.fetchNewsCats();
},
mounted() {
console.log('Current Swiper instance object', this.swiper);
// this.swiper.slideTo(3, 1000, false);
},
computed: {
swiper() {
return this.$refs.mySwiper.$swiper;
},
},
methods: {
async fetchNewsCats() {
const res = await this.$http.get('news/list');
this.newsCats = res.data;
},
switchActive() {
this.isCollapse = !this.isCollapse;
},
},
};
</script>
<style lang='scss' scope>
@import '../style/variables.scss';
// 重新定义一个class,便于单独管理各个页面的swipers
.pagination-home {
.swiper-pagination-bullet {
display: inline-block;
opacity: 1;
border-radius: 0.1538rem;
background-color: map-get($colors, 'white');
// background-color: #ffffff;
&.swiper-pagination-bullet-active {
background: map-get($colors, 'info');
}
}
}
.nav-icons {
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
.nav-item {
width: 25%;
// 取消右侧的边框
border-right: 1px solid $border-color;
&:nth-child(4n) {
border-right: none;
}
}
}
.toggleActive {
height: 60px;
overflow: hidden;
}
.fade-enter {
opacity: 0;
}
.fade-leave {
opacity: 1;
}
.fade-leave-active,
.fade-enter-active {
transition: opacity 0.9s;
}
</style>
3.15 web首页英雄列表提取官方数据
// 简化版
$$('.hero-nav > li').map(( li,i)=>{return {heros:$$('li',$$('.hero-list')[i]).map(el=>{return {name:$$('h3',el)[0].innerHTML}}),name:li.innerText}})
// 获取img链接,并转成json数据
JSON.stringify($$('.hero-nav > li').map(( li,i)=>{return {heros:$$('li',$$('.hero-list')[i]).map(el=>{return {name:$$('h3',el)[0].innerHTML,avatar:$$('img',el)[0].src}}),name:li.innerText}}))