前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我攻克的技术难题 - 因为一部遮天,我用三种语言实现了腾讯国漫评分系统

我攻克的技术难题 - 因为一部遮天,我用三种语言实现了腾讯国漫评分系统

原创
作者头像
叫我阿柒啊
修改2024-02-05 07:50:09
1.9K20
代码可运行
修改2024-02-05 07:50:09
举报
运行总次数:0
代码可运行

前言

”仙路尽头谁为峰,一见无始道成空。“

2013年初读遮天,十年后遮天的动漫正式上线。依稀记得高中记忆力“三十年河东三十年河西”的萧炎和独断万古的荒天帝。不知道当年经典绝伦的小说,如今改成动漫口碑如何。

于是就打开腾讯视频看看评分,每个视频都要点开才能看到评分和介绍。随即就萌生了用技术整合国漫评分内容的想法。历经一周,使用python采集国漫评分数据、Java搭建后台,以及VUE + ElementPlus构建前端页面,最后完成了一个简单的评分展示系统。

看看动图:

一. 国漫数据采集

分析评分数据

首先进入一个动漫的播放页,页面主要有左侧的评分数据,和右侧的简介数据。

评分数据获取

代码语言:json
复制
POST https://pbaccess.video.qq.com/trpc.message.grade_adapter.GradeService/GetGradeDetail?video_appid=3000010&vplatform=2

{
  "cid": "mcv8hkc8zk8lnov"
}

先研究一下评分数据如何获取,在控制台可以找到从后台请求的数据内容。

从请求返回的数据可以看到,可以获取到评分、点评人数、推荐比例等数据。接着对url进行分析,看如何才能获取到这些数据。

可以看到GetGradeDetail 的url,只有一个cid参数。

简介数据获取

代码语言:json
复制
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。

国漫列表

进入腾讯视频的国漫列表,看一下国漫列表。

这种有侧边栏的网站,基本上都是异步请求数据,然后渲染到展示区域。下拉动漫列表:

可以看到动漫区域一直在刷新,这样就肯定了之前的想法。

cid

1. 分析请求

F12进入开发者工具,通过搜索能功能找到对应的url。

这里可以看到每部动漫的cid。然后对playload进行分析,查看请求的参数信息。

参数是一个json串,有很多参数,我们通过查看js源码,来确定这些参数是如何生成的。

2. 分析参数

通过搜索来找到对应的请求部分源码:

可以看到请求参数里面有: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。

3. 获取cid

上面已经分析完请求了,接着就是利用python的requests模块,构建请求.通过对返回的json分析,获取目标数据。

从最后一行代码可以看到,数据在CardList1中获取,然后层层解构,遍历获取cid。

这样就将第一页前30个国漫的cid获取到了。当我修改变量获取第二页数据,即index = 1的时候,程序开始报下标越界的错误,那么应该是没有获取到数据,我们debug一下。

可以看到第一页数据,是从CardList1 获取,第二页数据就变成了CardList0。这是因为请求第一页的时候,需要返回筛选条件列表,放在了CardList0中。到了第二页就不需要了,所以这里要修改代码做判断。

然后就是对爬取的index做一个限制,目前设置为20次,即爬取20页,爬取每一页sleep(3)。这样就可以获取所有动漫的cid。

vid

我们在国漫列表页点击连接进入播放页的时候,是先进入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中可以下载封面图片,图片存储我准备了三种方案:

  1. 将image_url直接存入,通过url直接引用
  2. 将图片下载到本地目录,然后通过命名的方式与动漫信息关联
  3. 将图片转换成base64存入到MySQL中

方案一可能会在请求的时候出现跨域等问题,而且必须联网,从而请求失败。方案二将图片下载到本地,比较方便。方案三就是会对服务器网络和MySQL的IO造成压力,这里是测试,所以问题不大,这里我选用了方案三。

从image_url中获取图片bytes,然后经过一些工具类转换成base64字符串。

代码语言:python
代码运行次数:0
复制
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中,这里先根据定义表、数据字段。

建表
代码语言:sql
复制
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开发数据存储模块,这里一共简单实现了两个功能:

  1. 根据cid判断数据库中有没有这条数据存在
代码语言:python
代码运行次数:0
复制
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跳出循环,然后采集下一条数据。在程序的运行过程中,如果出现异常,重新启动程序,这些数据就可以避免再重新获取。

  1. 数据存储到MySQL
代码语言:python
代码运行次数:0
复制
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设置:

代码语言: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的微调:

代码语言: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组件,

代码语言:javascript
复制
<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先不填充内容。

1. svg图标定义

评分两侧的两个svg是wheat图标,先去icons网站下载wheat svg。

因为想找个一模一样的比较麻烦,我就用Adobe Illustrator做了一个水平镜像。这样就获取了评分两侧的wheat,然后放到@/assets/svg下面。

接着在项目中定义Icon组件,用来引用svg图标。

代码语言:javascript
复制
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。

代码语言:javascript
复制
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图标里面都有写,这里不做赘述,有兴趣的话可以参考之前的文章。

这样就能实现完成图标的引用了:

代码语言:javascript
复制
<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进行布局。

2. 评分推荐区域设计

从整体分布来看,因为既有垂直又有水平分布,所以这里要用flex弹性布局,即display:flex。

从左到右,分为评分区域和推荐区域两个div。按照这个思路,这里先将html部分写出来。

代码语言: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设置为弹性分布:

代码语言:css
复制
.user-rating-col__container {
    display: flex;
    flex-wrap: nowrap;
}

默认是row水平分布,所以这里可以不定义flex-direction

评分区域

left评分div从上到下分为垂直分布的三个部分,所以是布局方向flex-directioncolumn垂直分布。中间部分是由两个svg,一个评分span构成,使用默认水平分布。

user-rating-card__left中定义html:

代码语言: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作垂直分布的设置。

代码语言:css
复制
.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-itemsjustify-content实现水平和垂直居中分布。

代码语言:css
复制
.card-wheat {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin: 9px 0 15px;
}

水平分布就不用设置flex-direction,因为默认就是row。最后就是对评分和span文字的css定义:

代码语言: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水平布局。

代码语言:css
复制
.user-rating-card__right {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
}

再对user-rating-card__right布局分析。文字垂直靠右对齐,比例条部分垂直分布,所以分左右两个div水平布局,div内垂直布局。

左侧就是文字lable,右侧就是比例展示,左侧就是一个简单的div:

代码语言:html
复制
<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样式:

代码语言: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进度条组件,还可以设置各种颜色。

代码语言:html
复制
<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垂直布局:

代码语言:css
复制
.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进行了微调。最后展示效果:

3. 介绍页设计

定义了一个description组件,展示了动漫简介、title、热度、剧集等基本信息。

这里分成了两个部分,动漫简介其实就是一个div。

动漫简介

这部分的html两三行,没什么好说的。主要实现就是当文本过长是,如何限制住文本,我这里用css设置,最多只显示4行,多余的就用...表示。

代码语言:css
复制
.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;
}
热度、title

同样使用的是el-card卡片进行的展示,左边的图标是从网上下载的svg,右侧是简单的span文本展示,这里看看html。

代码语言: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弹性分布,用来调整居中。

代码语言:css
复制
.playlist-intro-info__item {
  display: flex;
  justify-content: center;
  margin-top: 12px;
}

本来我将justify-content设置成了flex-start靠左分布,后来觉得设置成center更有趣些。

接着就是对svg和span进行css定义:

代码语言:css
复制
span {
  color: white;
  font-size: 12px;
  opacity: 0.6;
  margin-left: 8px;
}

svg {
  margin-top: 2px;
}

基本上都是对细节和位置的细微调整。至此的页面设计就完成了就下来就是在main区域进行布局排版。最后效果如下:

感觉右边空荡荡的,我直接反手修改布局。设置了左右两个el-aside。

这样就有两个炫酷的轮播框了...

轮播图优化

1. 镜像

但是这时候问题就又来了,左右轮播图是镜像关系。左侧轮播框指示器在右侧,右侧轮播框的也在右侧,这样就不对称了,调了一阵儿也没成功,后来索性直接使用indicator-position将指示器去掉了。

代码语言:html
复制
<el-carousel indicator-position="none" />

但是图片的box-shadow阴影都在右侧,也不是镜像关系,所以我直接复制了一个carousel-right.vue

然后修改box-shadow,让其在左侧显示阴影。

代码语言:css
复制
img {
  box-shadow: -1px 1px 1px #888888;
}

这样就完成进行两个轮播图的镜像。

2. 轮播同步

这时候两侧轮播图是各玩各的,在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组件中做以下修改。

代码语言:html
复制
<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

springboot

已经两三年没有写过后台接口了,写的时候有点生疏感。

1. 数据源

使用的阿里Druid,来定义数据源。

使用 @Value来读取application.properties中的数据库配置信息。

2. mapper

定义了两个mapper,一个是分页查询,使用评分排序;另一个就是count统计。

代码语言:java
复制
@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实体类。

3. service

然后实现service,来调用mapper中的dao。

代码语言:java
复制
@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;
    }
}

4. controller

就实现了一个controller。

代码语言:java
复制
@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

首先就是安装axios

封装axios工具

在src/utils目录中封装一个请求工具类axios.ts

代码语言:JavaScript
复制
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就可以直接发起请求。

实现请求api

然后在src下新建一个api目录,在api中新增cartoon.ts用来封装请求。

代码语言:javascript
复制
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,然后发起请求。

调用api

接下来就是调用getCartoonList请求后获取数据。在哪里获取数据,因为数据是渲染在carousel、title的组件上的,所以可以在他们的父组件进行请求,即布局组件。

代码语言:javascript
复制
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

carousel主要是图片,这里要注意的一定就是img的src属性,必须要用require来加载图片。

在layout中获取了第一页数据,那么后面如何获取后面的数据,这个就在carousel中实现,在轮播图中有一个change事件,当切换图片时,就会自动调用此方法,所以思路就是当轮播到第10张图片时,就进行下一页请求。

代码语言:typescript
复制
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请求第一页数据。

控制台可以看到读取了新一页的数据。

渲染score.vue

然后就是对评分组件的渲染。

主要工作就是替换template中文字部分,在渲染进度条的时候,使用了customColors的颜色。

代码语言:javascript
复制
const customColors = [
    {color: '#f56c6c', percentage: 20},
    {color: '#e6a23c', percentage: 40},
    {color: '#5cb87a', percentage: 60},
    {color: '#1989fa', percentage: 80},
    {color: '#6f7ad3', percentage: 100},
  ]

查看渲染后的网页。

可以看到score和description随着轮播图切换也实时渲染数据。

渲染description.vue

对描述组件的渲染。

这里就是纯纯的对文本部分进行替换。

渲染title.vue

这个title的渲染按理说难度也没多大,主要是在 \<el-rate> 的v-model绑定评分数据时,本来想直接绑定研究了一阵子,后来就直接用watch监控变量。

像name是直接渲染,tags是做了一个为空不展示的处理,score需要单独的处理。

代码语言:javascript
复制
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写的一个简单的评分展示系统,虽然很简单,但也将数据采集、数据清洗、后台开发、前端设计融合了起来。前端是我的短板,在很多地方就纠结了很久,不过经历这一次的实践之后也有一丝丝成长。

当然也有很多不足的地方,欢迎大佬们多提出建议多指点,后面也会持续优化一下,例如搜索页等功能。

我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一. 国漫数据采集
    • 分析评分数据
      • 评分数据获取
      • 简介数据获取
    • 国漫列表
      • cid
      • vid
      • 封面图片处理
      • 数据存储
      • 数据采集优化
  • 二. 前端设计
    • 项目布局
      • 轮播图
        • 国漫名称展示
          • 评分数据展示
            • 1. svg图标定义
            • 2. 评分推荐区域设计
            • 3. 介绍页设计
          • 轮播图优化
            • 1. 镜像
            • 2. 轮播同步
        • 三. 后台接口
          • springboot
            • 1. 数据源
            • 2. mapper
            • 3. service
            • 4. controller
          • 前端:axios
            • 封装axios工具
            • 实现请求api
            • 调用api
            • 处理响应数据
            • 渲染carousel
            • 渲染score.vue
            • 渲染description.vue
            • 渲染title.vue
        • 结语
        相关产品与服务
        云数据库 MySQL
        腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档