”仙路尽头谁为峰,一见无始道成空。“
2013年初读遮天,十年后遮天的动漫正式上线。依稀记得高中记忆力“三十年河东三十年河西”的萧炎和独断万古的荒天帝。不知道当年经典绝伦的小说,如今改成动漫口碑如何。
于是就打开腾讯视频看看评分,每个视频都要点开才能看到评分和介绍。随即就萌生了用技术整合国漫评分内容的想法。历经一周,使用python采集国漫评分数据、Java搭建后台,以及VUE + ElementPlus构建前端页面,最后完成了一个简单的评分展示系统。
看看动图:
首先进入一个动漫的播放页,页面主要有左侧的评分数据,和右侧的简介数据。
POST https://pbaccess.video.qq.com/trpc.message.grade_adapter.GradeService/GetGradeDetail?video_appid=3000010&vplatform=2
{
"cid": "mcv8hkc8zk8lnov"
}
先研究一下评分数据如何获取,在控制台可以找到从后台请求的数据内容。
从请求返回的数据可以看到,可以获取到评分、点评人数、推荐比例等数据。接着对url进行分析,看如何才能获取到这些数据。
可以看到GetGradeDetail 的url,只有一个cid参数。
POST https://pbaccess.video.qq.com/trpc.universal_backend_service.page_server_rpc.PageServer/GetPageData?video_appid=3000010&vplatform=2&vversion_name=8.2.97
{
"req_from": "web",
"cid": "mzc00200s86alsp",
"vid": "a0047tbsjbs",
"lid": "",
"page_type": "detail_operation",
"page_id": "detail_page_introduction"
}
这里需要两个参数,cid和vid,根据我的理解,cid就是cartoon_id,是一部动漫的唯一标识,vid是video_id,是每部动漫每一集的唯一标识。那么就来看cid和vid是如何来获取。
我是通过国漫列表页跳转到播放页的,所以就去列表页看看如何获取cid。
进入腾讯视频的国漫列表,看一下国漫列表。
这种有侧边栏的网站,基本上都是异步请求数据,然后渲染到展示区域。下拉动漫列表:
可以看到动漫区域一直在刷新,这样就肯定了之前的想法。
F12进入开发者工具,通过搜索能功能找到对应的url。
这里可以看到每部动漫的cid。然后对playload进行分析,查看请求的参数信息。
参数是一个json串,有很多参数,我们通过查看js源码,来确定这些参数是如何生成的。
通过搜索来找到对应的请求部分源码:
可以看到请求参数里面有:x、l、R、n、video_guid几个可变参数。通过分析和debug最后得知,每次请求的变化的只有x,即page_index和page,其他的参数都是固定值。
这里就举例一下:比如channel_id对应的n为什么是100119。
最后一行有个vS()方法,就是调用了上面的请求,i.value对应的就是形参n。i的生成可以在第五行代码中看到,用了一个lambda函数,遍历过滤n.channelListData.channleList。打印此变量:
过滤条件就是是channel_ename == t.channelId,这里的t.channelId通过打印发现是“cartoon”,可以看到channel_ename为cartoon对应的channel_id为100119.
那么t是怎么来的呢,t是setup的参数,而setup是用来解构props的,所以t就是props,props在vue中用来接收父组件传值。
所以t就是父组件传给渲染动漫列表的组件一个参数值,其中包含channel_id。接下来的工作就是获取vid。
上面已经分析完请求了,接着就是利用python的requests模块,构建请求.通过对返回的json分析,获取目标数据。
从最后一行代码可以看到,数据在CardList1中获取,然后层层解构,遍历获取cid。
这样就将第一页前30个国漫的cid获取到了。当我修改变量获取第二页数据,即index = 1的时候,程序开始报下标越界的错误,那么应该是没有获取到数据,我们debug一下。
可以看到第一页数据,是从CardList1 获取,第二页数据就变成了CardList0。这是因为请求第一页的时候,需要返回筛选条件列表,放在了CardList0中。到了第二页就不需要了,所以这里要修改代码做判断。
然后就是对爬取的index做一个限制,目前设置为20次,即爬取20页,爬取每一页sleep(3)。这样就可以获取所有动漫的cid。
我们在国漫列表页点击连接进入播放页的时候,是先进入https://v.qq.com/x/cover/cid.html,然后再跳转到cid/vid.html。
我们先对cid.html页面进行分析。
在html网页中发现了vid的列表信息,对于网页中数据的提取一般使用正则表达式。这样在我们获取了cid之后,就能获取vid。
至此,cid和vid都获取到了。
首先构建请求参数,cid和vid设置为空字符。
将cid放到参数中,发起评分grade_url请求。
来获取image_url(封面图片url)、热度、评分、推荐区间比例等数据。接着将cid、vid(从vids列表中任取一个即可)放到动漫简介请求参数中,发起请求。
从返回值可以获取到各种标签数据,对json解析,获取自己需要的数据。
从获取的image_url中可以下载封面图片,图片存储我准备了三种方案:
方案一可能会在请求的时候出现跨域等问题,而且必须联网,从而请求失败。方案二将图片下载到本地,比较方便。方案三就是会对服务器网络和MySQL的IO造成压力,这里是测试,所以问题不大,这里我选用了方案三。
从image_url中获取图片bytes,然后经过一些工具类转换成base64字符串。
urllib.request.urlretrieve(url=image_url, filename='tmp.jpg')
image_source = Image.open('tmp.jpg')
byte_source = BytesIO()
image_source.save(byte_source, format="JPEG")
byte_data = byte_source.getvalue()
base64_str = base64.b64encode(byte_data).decode("ascii")
从image_url请求的是avif的bytes,这里我直接使用urllib将图片下载保存成jpg格式。然后利用Image和BytesIO模块将二进制转换成base64的字符串。
在img标签中,通过src引入base64和引入图片路径是一样的效果。
但是这个方案在最后又被否决了,原因就是:转换成base64之后,MySQL中的varchar和Text都装不下,所以我又选择了方案二,将图片按照cid命名下载到了本地。
设计一个存储模块,将上面的评分数据和简介数据存储到MySQL中,这里先根据定义表、数据字段。
create table cartoon(
cid varchar(35) not null primary key,
name varchar(50),
title varchar(100),
score varchar(5),
promoter_score varchar(20),
evaluate_number varchar(30),
type_ varchar(4) comment '类型id',
year varchar(10),
tag_text varchar(10) comment 'VIP' ,
main_genres varchar(20) comment '类型',
hotval varchar(20) comment '热度',
episode_all int(8) comment '全集',
dimension varchar(100) comment '评分比例',
update_notify_desc varchar (100) comment '更新周期',
update_time varchar(20) comment '自定义数据修改日期',
cover_description text comment '描述'
) default charset='utf8';
进入MySQL执行建表语句
利用python的pymysql开发数据存储模块,这里一共简单实现了两个功能:
sql = f"select cid from cartoon where cid = '{cid}'"
cursor = conn.cursor()
cursor.execute(sql)
result = cursor.fetchone()
if result:
# 可以更新评分、热度、时间等字段
print(name, '已经存在于数据库中...')
continue
如果存在于数据库的话,可以执行update更新评分、热度等信息,这里先不实现,只是使用continue跳出循环,然后采集下一条数据。在程序的运行过程中,如果出现异常,重新启动程序,这些数据就可以避免再重新获取。
sql = f'''insert into cartoon (cid, name, title, score, promoter_score, evaluate_number, type_, year, tag_text, main_genres, hotval, episode_all, dimension, update_notify_desc, update_time, cover_description)
values ('{cid}', '{name}', '{title}', '{score}', '{promoter_score}', '{evaluate_number}', '{type_}', '{year}', '{tag_text}', '{main_genres}', '{hotval}', '{episode_all}', '{dimension}', '{update_notify_desc}', '{update_time}', '{cover_description}')'''
cursor = conn.cursor()
cursor.execute(sql)
conn.commit()
启动程序,开始爬取数据。
最后在数据库中查看爬取的国漫信息。
上面请求的数据都是json格式,因为不是所有的json返回的都是全字段,很多的json都没有一些字段。所以在爬取过程中,需要根据报错信息一直调整自己的代码。
例如在解析字符串的时候,判断json里是否有这个字段,json中的json是否是NoneType,否则都会报错。下面就是针对于评分数据json的处理:
从图中可以看到,动漫信息可能是从enough或者lack字段获取,而且还有是NoneType。我对这种情况的处理就是:如果没有评分数据,通过continue跳出这个国漫信息的爬取。
前端使用webpack + vue + ElmentPlus + TypeScript + scss,使用vue脚手架创建一个项目,导入到IDE。
页面左侧做一个垂直轮播,右侧显示评分、简介等信息,每次刷新
首先使用ElementPlus的container进行布局,将整个页面分为aside和main左右两个区域。
左侧Aside的显示轮播组件\<Carousel>,轮播使用的是ElementPlus的carousel组件,直接从官网针贴代码到组件中。
这时候访问前台页面。
从页面看,基本的布局就完成了,接下来就是对轮播优化、main区域展示设计以及css细节优化,先对轮播图进行样式调节。
轮播图使用的是ElementPlus的el-carousel走马灯组件。动漫的封面是长图,像素是770 * 1080,这里进行50%的等比例缩放,来设置轮播框的宽高。
下载了一张封面图,通过img标签放在el-carousel-item中,然后进行css设置:
$width: 385px;
$height: 540px;
img {
width: $width;
height: $height;
box-shadow: 1px 1px 1px #888888;
border-radius: 16px;
}
.carousel {
bottom: 80px;
}
.el-carousel__item {
width: $width;
height: $height;
padding-left: 50px;
background-color: rgba(0, 0, 0, 0);
}
.el-carousel__mask {
background-color: rgba(0, 0, 0, 0) !important;
}
主要是对img和el-carouselitem组件做了一些细节、定位的设计。这里提一下*el-carouselmask*,必须要要加important来强制改变为透明颜色,这样才能和背景色颜色一样。
最后大概是这个样子。
再看看main区域的数据展示。
d这一块其实是在后面才设计的,但是布局是在最上方,这里就先说说这里实现。样例如下:
这里没有啥设计,定义了一个title.vue。评分星号是用ElementPlus的el-rate实现的,标签使用el-tag实现的。
然后就是一些css的微调:
.header_div {
width: 510px;
height: 138px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
}
.tags {
color: white;
font-size: 12px;
opacity: 0.6;
margin-left: -4px;
}
.rate-star {
margin: 0px 0 0 20px
}
.mx-1 {
margin: 4px 0 0 7px;
}
.title-name {
color: white;
font-size: 40px;
box-shadow: #141414;
}
定义了一个score组件,来简单设计了一个评分推荐页面
没有前端设计的这种艺术细胞,所以这里打算复刻腾讯视频的评分展示页。评分数据展示我使用的是ElementPlus的el-card组件,
<el-card class="box-card">
</el-card>
<style scoped lang="scss">
$background_layout: rgb(236 217 217 / 5%);
.box-card {
width: 326px;
height: 138px;
background: $background_layout;
border: none;
}
</style>
这里只对长宽、颜色进行了简单的定义,el-card先不填充内容。
评分两侧的两个svg是wheat图标,先去icons网站下载wheat svg。
因为想找个一模一样的比较麻烦,我就用Adobe Illustrator做了一个水平镜像。这样就获取了评分两侧的wheat,然后放到@/assets/svg下面。
接着在项目中定义Icon组件,用来引用svg图标。
setup(props) {
const iconStyle = computed(() => {
const {size, color} = props
let s = `${size.replace('px', '')}px`
return {
fontSize: s,
color: color,
}
})
if (props.name.startsWith('el-icon-')) {
return () => h('el-icon', {
class: 'icon el-icon',
style: iconStyle.value
}, [h(resolveComponent(props.name))])
// svg图标
} else if (props.name.startsWith('svg-icon')) {
return () => h(svg, {name: props.name, size: props.size, color: props.color})
} else {
return () => h('i', {
class: 'icon ' + props.name,
style: iconStyle.value
})
}
}
然后在vue.config.js中使用svg-sprite-loader加载器,将svg文件加载进来,Icon组件就可以引用svg。
chainWebpack: (config) => {
// 内置的svg处理排除指定目录下的文件
config.module.rule('svg').exclude.add(resolve('src/assets/svg')).end()
config.module
.rule('svg-sprite-loader')
.test(/\.svg$/)
.include.add(resolve('src/assets/svg'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'svg-icon_[name]'
})
}
在原有的svg文件名前加上_svg-icon__ 前缀。这样,wheat的svg就可以被Icon组件引用。这一块的具体实现我在之前拆解BuildAdmin的Icon组件和定义svg图标里面都有写,这里不做赘述,有兴趣的话可以参考之前的文章。
这样就能实现完成图标的引用了:
<Icon class="nav-menu-icon" name="svg-icon_wheat-left" />
<Icon class="nav-menu-icon" name="svg-icon_wheat-right" />
通过Icon组件中name属性,就能使用刚刚加载的的svg图标。
从上图看,两个svg图标是垂直分布,而我想要得的水平分布,所以接下来就利用css进行布局。
从整体分布来看,因为既有垂直又有水平分布,所以这里要用flex弹性布局,即display:flex。
从左到右,分为评分区域和推荐区域两个div。按照这个思路,这里先将html部分写出来。
<template>
<el-card class="box-card">
<div class="user-rating-col__container">
<div class="user-rating-card__left"></div>
<div class="user-rating-card__right"></div>
</div>
</el-card>
</template>
定义了user-rating-card的左右div。left是评分区域,rigth是推荐区域。因为两个区域是水平分布的,这里先将父div设置为弹性分布:
.user-rating-col__container {
display: flex;
flex-wrap: nowrap;
}
默认是row水平分布,所以这里可以不定义flex-direction。
left评分div从上到下分为垂直分布的三个部分,所以是布局方向flex-direction是column垂直分布。中间部分是由两个svg,一个评分span构成,使用默认水平分布。
在user-rating-card__left中定义html:
<div class="user-rating-card__left">
<span class="user-rating-card__title">腾讯视频评分</span>
<div class="card-wheat">
<Icon class="nav-menu-icon_left" name="svg-icon_wheat-left2" size="38" color="silver"/>
<div class="card-wheat__percent"> 9.7</div>
<Icon class="nav-menu-icon_right" name="svg-icon_wheat-right2" size="38" color="silver"/>
</div>
<span class="user-rating-card__desc">83.4万人点评</span>
</div>
中间的三个部分看做一个整体,都放在一个card-wheat div进行flex布局。
然后先对user-rating-card__left作垂直分布的设置。
.user-rating-card__left {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 0 0 auto;
margin-right: 10px;
font-size: 10px;
line-height: 16px;
}
将评分部分设置为column列分布,居中对齐。再对中间部分card-wheat做一个水平分布,并使用align-items和justify-content实现水平和垂直居中分布。
.card-wheat {
display: flex;
align-items: center;
justify-content: space-between;
margin: 9px 0 15px;
}
水平分布就不用设置flex-direction,因为默认就是row。最后就是对评分和span文字的css定义:
.card-wheat__percent {
display: flex;
justify-content: center;
width: 52px;
font-size: 22px;
line-height: 36px;
font-weight: 500;
color: #ff7612;
white-space: nowrap;
}
span {
color: white;
white-space: nowrap;
}
再看评分区域,已经初具雏形。
里面wheat的svg后来我又给设置成银色了。下面点评的span透明度也设置成了0.6。这个就从后面接着看吧。
弹性布局,贵在弹性二字,这里看着虽然居中,等定义user-rating-card__right区域后再看效果。
先将推荐区域user-rating-card__right进行flex水平布局。
.user-rating-card__right {
flex: 1 1 auto;
display: flex;
align-items: center;
}
再对user-rating-card__right布局分析。文字垂直靠右对齐,比例条部分垂直分布,所以分左右两个div水平布局,div内垂直布局。
左侧就是文字lable,右侧就是比例展示,左侧就是一个简单的div:
<div class="user-rating-card__labels">
<div class="user-rating-card__label"> 强推</div>
<div class="user-rating-card__label"> 推荐</div>
<div class="user-rating-card__label"> 可看</div>
<div class="user-rating-card__label"> 一般</div>
<div class="user-rating-card__label"> 不推荐</div>
</div>
然后定义css样式:
.user-rating-card__labels {
display: flex;
flex-direction: column;
margin-right: 8px;
}
.user-rating-card__label {
flex: 0 0 auto;
display: flex;
justify-content: flex-end;
white-space: nowrap;
height: 16px;
margin: 1px 0;
font-size: 10px;
padding: 0 12px;
color: white;
}
设置flex-direction为垂直对齐,将justify-content设置为flex-end来尾部对齐,margin后面和比例条一起调整对齐。
右侧比例展示,我使用的是ElementPlus的Progress进度条组件,还可以设置各种颜色。
<div class="user-rating-card__bars">
<el-progress v-for="(item, index) in customColors" :percentage="item.percentage"
:color="item.color" :key="index" :show-text="false"/>
</div>
<script setup lang="ts">
import {reactive} from 'vue'
const customColors = reactive([
{color: '#f56c6c', percentage: 20},
{color: '#e6a23c', percentage: 40},
{color: '#5cb87a', percentage: 60},
{color: '#1989fa', percentage: 80},
{color: '#6f7ad3', percentage: 100},
])
</script>
在script中定义了颜色和百分比,v-for循环遍历创建了五个el-progress,其中percentage组件为显示的百分比,show-text设置为false,这样不显示进度文本。
然后对user-rating-card__bars垂直布局:
.user-rating-card__bars {
display: flex;
flex-direction: column;
margin-top: 11px;
}
.el-progress--line {
margin-bottom: 12px;
width: 200px;
}
el-progress--line要设置进度条宽度,margin-top和margin-bottom是为了和左侧文字对齐,自己调出来的。
后面将box-card的width改成了510px,同时对各个组件使用margin进行了微调。最后展示效果:
定义了一个description组件,展示了动漫简介、title、热度、剧集等基本信息。
这里分成了两个部分,动漫简介其实就是一个div。
这部分的html两三行,没什么好说的。主要实现就是当文本过长是,如何限制住文本,我这里用css设置,最多只显示4行,多余的就用...表示。
.description {
width: 510px;
height: 138px;
color: white;
opacity: 0.75;
font-size: 16px;
margin-top: 40px;
// 多行隐藏
max-height: 5rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
line-height: 1.2rem;
}
同样使用的是el-card卡片进行的展示,左边的图标是从网上下载的svg,右侧是简单的span文本展示,这里看看html。
<el-card class="box-card">
<div class="playlist-intro-info__item">
<Icon name="svg-icon_title" size="12"/>
<span class="title">少年不屈,异火不熄!</span>
</div>
<div class="playlist-intro-info__item">
<Icon name="svg-icon_fire" size="12"/>
<span class="hot">11935 · 内地 · 2015 · 虐心爱情 · 东方玄幻</span>
</div>
<div class="playlist-intro-info__item">
<Icon name="svg-icon_time" size="12"/>
<span class="title">共158集,会员看全集</span>
</div>
</el-card>
然后就是定义css样式,对playlist-intro-info__item父div做一个flex弹性分布,用来调整居中。
.playlist-intro-info__item {
display: flex;
justify-content: center;
margin-top: 12px;
}
本来我将justify-content设置成了flex-start靠左分布,后来觉得设置成center更有趣些。
接着就是对svg和span进行css定义:
span {
color: white;
font-size: 12px;
opacity: 0.6;
margin-left: 8px;
}
svg {
margin-top: 2px;
}
基本上都是对细节和位置的细微调整。至此的页面设计就完成了就下来就是在main区域进行布局排版。最后效果如下:
感觉右边空荡荡的,我直接反手修改布局。设置了左右两个el-aside。
这样就有两个炫酷的轮播框了...
但是这时候问题就又来了,左右轮播图是镜像关系。左侧轮播框指示器在右侧,右侧轮播框的也在右侧,这样就不对称了,调了一阵儿也没成功,后来索性直接使用indicator-position将指示器去掉了。
<el-carousel indicator-position="none" />
但是图片的box-shadow阴影都在右侧,也不是镜像关系,所以我直接复制了一个carousel-right.vue。
然后修改box-shadow,让其在左侧显示阴影。
img {
box-shadow: -1px 1px 1px #888888;
}
这样就完成进行两个轮播图的镜像。
这时候两侧轮播图是各玩各的,在el-carousel有一个属性:pause-on-hover,即鼠标悬浮时暂停自动切换,这个默认值为true。当我悬停在一个轮播框的时候,这个轮播图其实就已经不动了,但另一个还在轮播。所以这里就要想着如何将两个轮播图同步起来。
这时候我就想到了el-carousel的autoplay自动属性了。当我悬停在一个轮播图的时候,就触发一个hover事件,将另一轮播图的autoplay设置为false,这样两个轮播图都不会动了。所以,这里得先定义一个全局状态变量,这里我用的是pinia。
定义了一个useCarousel状态,里面有autoplay属性,初始值为true,自动播放并定义了鼠标进入悬停的mouseEnter和鼠标离开的mouseLeave两个方法。
当鼠标悬停在轮播框,会触发轮播图的pause-on-hover停止播放属性,同时调用mouseEnter,将autoplay设置为false.当鼠标离开,轮播图恢复播放,这时候调用mouseLeave(),将autoplay设置为true。所以两个事件需要绑定在轮播图组件上。
cartoonData变量是为后面存储后台请求预留的字段。
在两个轮播图的el-carousel组件中做以下修改。
<el-carousel
:autoplay="carouselStore.state.autoplay"
@mouseenter="carouselStore.mouseEnter"
@mouseleave="carouselStore.mouseLeave"
/>
<script setup lang="ts">
import {useCarousel} from '@/stores/carousel'
const carouselStore = useCarousel()
</script>
el-carousel的autoplay属性由全局状态控制,并用v-on(@)来绑定鼠标悬停和离开事件。通过修改autoplay来通知另一个轮播框是否暂停/恢复播放。
效果如下:
这样,前端部分就涉及完成了,虽然也没什么美感。。接下来就是从后台写一个获取数据的接口,来根据轮播图修改对应的评分等展示信息。
从上面的前端设计来看,因为也没有搜索之类的设计,所以只需要一个接口获取MySQL中的评分信息就可以了。后台接口我这里就选择Java的springboot + Mybatis来做一个数据查询接口。前台请求就是axios。
已经两三年没有写过后台接口了,写的时候有点生疏感。
使用的阿里Druid,来定义数据源。
使用 @Value来读取application.properties中的数据库配置信息。
定义了两个mapper,一个是分页查询,使用评分排序;另一个就是count统计。
@Mapper
public interface CartoonMapper {
@Select("select * from cartoon order by score desc limit #{start}, #{num};")
List<CartoonDomain> listCartoon(int start, int num);
@Select("select count(*) from cartoon")
int countCartoon();
}
其中CartoonDomain是和数据库的cartoon的ORM实体类。
然后实现service,来调用mapper中的dao。
@Service
public class CartoonService {
@Autowired
private CartoonMapper cartoonMapper;
public List<CartoonDomain> listCartoon10(int start, int num) {
List<CartoonDomain> cartoonList= cartoonMapper.listCartoon(start, num);
return cartoonList;
}
public int countCartoon() {
int count = cartoonMapper.countCartoon();
return count;
}
}
就实现了一个controller。
@RequestMapping("cartoon")
@RestController
public class CartoonController {
@Autowired
private CartoonService cartoonService;
@RequestMapping(value="/listCartoon", method= RequestMethod.POST)
public HashMap<String, Object> getCartoon(@RequestBody Params params) {
int num = 10;
List<CartoonDomain> cartoonList = cartoonService.listCartoon(params.getPage() * 10, num);
int count = cartoonService.countCartoon();
HashMap<String, Object> map = new HashMap<>();
map.put("count", count);
map.put("data", cartoonList);
return map;
}
}
使用 @RequestBody 限定前端传入一个json格式的参数在getCartoon() 中,定义了一个Params实体类来接收参数,这个类中只有一个page字段,用来实现mapper中的分页,这里num指定了每页10个。
使用 @RestController限定返回值为json,第一层json是count和data字段,data字段里面存放的是国漫数据的list。
启动springboot程序,使用Http Client进行请求测试:
可以看到已经请求到了目标数据。接下来就是在前端来实现请求模块。
首先就是安装axios
在src/utils目录中封装一个请求工具类axios.ts。
import axios from "axios";
import {AxiosRequestConfig} from "axios";
export function createAxios(baseURL: string, axiosConfig: AxiosRequestConfig) {
const Axios = axios.create({
baseURL: baseURL,
headers: {
"Content-Type": "application/json;charset=UTF-8"
},
responseType: 'json'
})
return Axios(axiosConfig)
}
使用axios的create封装了一个createAxios() 方法,构建了一个模板化的创建Axios实例的工具。我们只要传入baseURL和请求配置axiosConfig就可以直接发起请求。
然后在src下新建一个api目录,在api中新增cartoon.ts用来封装请求。
import {createAxios} from "@/utils/axios";
const baseURL = "http://localhost:8080/cartoon_web/"
export function getCartoonList(page: number) {
const data = {
'page': page
}
return createAxios(
baseURL,
{url: 'cartoon/listCartoon', method: 'post', data: data}
)
}
实现了getCartoonList 方法来调用springboot定义的接口,通过createAxios() 方法,然后将url、method和data以字典形式传入axiosConfig,然后发起请求。
接下来就是调用getCartoonList请求后获取数据。在哪里获取数据,因为数据是渲染在carousel、title的组件上的,所以可以在他们的父组件进行请求,即布局组件。
import {onBeforeMount} from 'vue'
import {getCartoonList} from "@/api/cartoon/cartoon";
onBeforeMount(() => {
getCartoonList(0).then((res) => {
console.log(res.data)
})
})
天下武功,唯快不破。在onBeforeMount生命周期函数发起请求,请求之后使用then回调函数来处理响应数据,这里先简单的打印一下,看看数据。
刷新页面,控制台跨域错误,导致无法请求数据。
返回springboot的应用,在controller的getCartoon方法上,添加 @CrossOrigin注解允许跨域。
再次刷新页面,看控制台,已经输出请求数据。
和之前使用Http Client测试请求的的数据是一样的。接下来就是如何处理数据渲染到各个组件上了。
因为是多个组件都会用到响应数据做渲染,所以要像之前写过的路由动态加载一样,将这些数据放到pinia作为全局状态变量。这里就在之前实现carousel同步的pinia状态useCarousel中进行新增。
之前预留了cartoonData字段就是用来存储data信息的,同时有新增了4个字段。
maxPage表示最多能向后台请求多少次数据,这个可以根据后台返回的count计算得到。
currentPage表示当前请求的是第几页数据,从0开始,如果 = maxPage则从0开始重新获取。
maxIndex是表示轮播图轮播图最多可以播放到的index,到达时则请求下一页的数据。
currentIndex表示当前幻灯片的缩影,从0开始,如果 = maxIndex则发起请求
在layout布局组件中,发起数据请求,根据count计算出总页数(从0开始),然后赋值给maxPage。
这样就将请求的第一页数据,放到了cartoonData共享变量中,然后就是渲染数据,先将图片渲染到carousel组件,这里有两个carousel,渲染方式都一样,所以这里只挑一个写。
carousel主要是图片,这里要注意的一定就是img的src属性,必须要用require来加载图片。
在layout中获取了第一页数据,那么后面如何获取后面的数据,这个就在carousel中实现,在轮播图中有一个change事件,当切换图片时,就会自动调用此方法,所以思路就是当轮播到第10张图片时,就进行下一页请求。
function change(current: number, pre: number) {
const maxIndex = carouselStore.state.maxIndex
if (current == maxIndex) {
let currentPage = carouselStore.state.currentPage + 1
if (currentPage > carouselStore.state.maxPage) {
currentPage = 0
}
getCartoonList(currentPage).then((res) => {
carouselStore.setCartoonData(res.data['data'])
carouselStore.state.currentPage = currentPage
})
}
carouselStore.state.currentIndex = current
}
current指的是翻转后图片的索引,从0开始。若current等于状态变量中的maxIndex,则进行请求,但前提当前请求页数currentPage不能大于maxPage,否则就置为0请求第一页数据。
控制台可以看到读取了新一页的数据。
然后就是对评分组件的渲染。
主要工作就是替换template中文字部分,在渲染进度条的时候,使用了customColors的颜色。
const customColors = [
{color: '#f56c6c', percentage: 20},
{color: '#e6a23c', percentage: 40},
{color: '#5cb87a', percentage: 60},
{color: '#1989fa', percentage: 80},
{color: '#6f7ad3', percentage: 100},
]
查看渲染后的网页。
可以看到score和description随着轮播图切换也实时渲染数据。
对描述组件的渲染。
这里就是纯纯的对文本部分进行替换。
这个title的渲染按理说难度也没多大,主要是在 \<el-rate> 的v-model绑定评分数据时,本来想直接绑定研究了一阵子,后来就直接用watch监控变量。
像name是直接渲染,tags是做了一个为空不展示的处理,score需要单独的处理。
const carouselStore = useCarousel()
const score = ref(0.0)
watch(() => carouselStore.state.currentIndex, () => {
score.value = parseFloat(carouselStore.state.cartoonData[carouselStore.state.currentIndex]?.score)
})
watch(() => carouselStore.state.cartoonData, () => {
score.value = parseFloat(carouselStore.state.cartoonData[0]?.score)
})
对score进行了两部分的处理,一是currentIndex改变,即轮播图片切换时,需要修改,但是这样第一个的评分就显示不了,所以再对cartoonData进行监控,每次请求数据都会修改。
这样,一个简单的前端页面就完成了。
这就是我基于python、Java和vue写的一个简单的评分展示系统,虽然很简单,但也将数据采集、数据清洗、后台开发、前端设计融合了起来。前端是我的短板,在很多地方就纠结了很久,不过经历这一次的实践之后也有一丝丝成长。
当然也有很多不足的地方,欢迎大佬们多提出建议多指点,后面也会持续优化一下,例如搜索页等功能。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。