前面的文章我们已经实现完毕客户端的界面,播放器以及数据中心模块与通信模块的预备工作。那么接下来我们便对客户端的业务逻辑进行实现。

一.RESTful API

本项目采用RESTful API是⼀种遵循REST架构风格,基于HTTP协议的应用程序接口设计规范,它提供了⼀种通过标准化的操作和资源访问模式进行客户端与服务器通信的⽅式。
REST是Representational State Transfer的缩写,翻译过来就是表现层状态转化。
资源(Resource)
RESTful API 中的每⼀个对象、实体或数据都被抽象为⼀个资源。例如,用户、文章等都可以作为资源。每个资源都通过⼀个唯⼀的 URI (统⼀资源标识符)标识。
URI(统⼀资源标识)
URI是用于标识资源的地址。 RESTful API 中,通常使用 URL (统⼀资源定位符)作为 URI 。例如:
/users/123 表示 id 为 123 的用户资源
/posts/456 表示 id 为 456 的⽂章资源
HTTP动作(HTTP Methods)
RESTful API 依赖于 HTTP 协议的常见方法来对资源进⾏操作,每个 HTTP 方法对应不同的操作:
GET :获取服务器上的资源。
POST :在服务器上创建新的资源。
PUT :更新服务器的上的资源。
DELETE :删除服务器上的资源。
无状态
RESTful API 是无状态的。每个请求都应该是独立的,服务器不会在请求之间保存客户端的状态。
表现层状态转移(Representational State Transfer)
资源的表现形式可以是 JSON 、 XML 、 HTML 等格式,通常 RESTful API 使用 JSON 作为数据交换格式,因为它轻量且易于解析。
RESTful API的优点可以总结如下:
1.简单与统一接口:使用标准的 HTTP 方法(GET, POST, PUT, DELETE 等)来执行操作,设计直观,学习和使用成本低。
2.可伸缩性:无状态特性(每次请求都包含所有必要信息)使得服务器无需保存客户端状态,更容易通过增加服务器来扩展系统性能。
3.松耦合与独立性:客户端和服务器是分离的,只要接口不变,可以独立地发展和更换技术栈。
4.通用性与互操作性:基于 HTTP 协议,可以被任何支持 HTTP 的客户端(浏览器、移动应用、IoT 设备等)轻松调用,语言无关。
我们之前开发的测试接口(如hello和ping)采用的就是RESTful API设计,这种标准化接口几乎无需额外学习就能快速上手使用。所以我们使用它来实现客户端背后的业务逻辑。

二.启动页-临时用户登录接口

在进行项目实现之前,我们就已经规定好了请求的URL以及请求的参数字段:
请求URL:

POST /HttpService/tempLogin 

请求参数(客户端):

字段名称 字段类型 字段说明
requestId string 请求ID

示例:

{  
	"requestId": "string"
}

返回响应:200 OK

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
result string 响应结果
sessionId string 客户端会话ID

示例:

{
	"requestId": "string",
	"errorCode": 0,
	"errorMsg": "",
	"result": {
		"sessionId": "string"
	}
}

那么首先我们在dataCenter.h之前我们给hello与ping测试接口定义异步请求方法以及完成信号的相同位置定好临时登录请求的异步方法与信号:

/////////////datacenter.h
public:
    //临时登录请求
    void tempLoginAsync();
signals:
    //临时登录成功之后发射的信号
    void tempLoginDone();
/////////////datacenter.cpp
void DataCenter::tempLoginAsync()
{
    netClient.tempLogin();
}

接下来我们去netclient类中根据我们之前定好的请求接口与URL实现tempLogin方法:

//netclient.h
namespace model{
class DataCenter;
}

namespace netclient{
class NetClient : public QObject
{
    Q_OBJECT
public:
    void tempLogin();//临时登录请求
};
}//end netclient
//netclient.cpp
void NetClient::tempLogin()
{
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/tempLogin",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        //设置sessionId
        dataCenter->setSessionId(result["sessionId"].toString());
        LOG() << "sessionId设置成功-" << result["sessionId"].toString();
        emit dataCenter->tempLoginDone();
    });
}

对于session与cookie不了解的读者可以移步至我之前写过的一篇文章:
HTTP协议解析:Session/Cookie机制与HTTPS加密体系的技术演进(二)
这里我们就不再深入介绍了。当服务端收到临时登录请求后,会向客户端发送一个sessionId。由于HTTP协议本身是无状态的,为了保持后续通信的"有状态"特性,客户端需要保存这个sessionId,并在之后与服务端的所有交互中都携带该sessionId。我们在dataCenter中添加一个成员变量记录该sessionId并添加setSessionId方法。
客户端请求处理完毕,接下来我们在mockServer模拟实现一个简单的响应逻辑:
首先新增枚举常量UserType标识本次通信对象的身份-默认为临时登录用户:

enum UserType{
    SuperAdmin = 1, // 超级管理员
    Admin = 2, // 管理员
    User = 3, // 普通⽤⼾
    TempUser = 4 // 临时⽤⼾
};

接下来定义实现临时登录请求的响应接口,同时根据我们之前规定好的URL设置路由:

//mockserver.h
class MockServer : public QWidget
{
    Q_OBJECT

public:
    QHttpServerResponse tempLogin(const QHttpServerRequest &request);//临时登录请求接口
private:
    UserType userType = TempUser;//默认为临时用户
};
//mockserver.cpp
bool MockServer::init()
{
    //监听指定端口
    quint16 port = 8080;
    quint16 ret =  httpServer.listen(QHostAddress::Any,port);
    //设置路由
    httpServer.route("/HttpService/tempLogin",[=](const QHttpServerRequest &request){
        return tempLogin(request);
    });
    return ret == 8080;
}
QHttpServerResponse MockServer::tempLogin(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[tempLogin] 收到 tempLogin 请求, requestId = " <<reqData["requestId"].toString();
    userType = TempUser;//设置当前用户为临时用户
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    result["sessionId"] = QUuid::createUuid().toString().sliced(25,12);//构建sessionId
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

当客户端收到服务端的响应时,会发射一个名为tempLoginDone的信号。我们这里的处理方法是在启动页调用其异步处理方法并处理该信号-当响应处理完毕后,再显示首页界面:

//startpage.h
class StartPage : public QDialog
{
    Q_OBJECT
public:
    void tempLoginDone();
private:
    bool isLogin = false;//默认登录状态为false
};
//startpage.cpp
StartPage::StartPage(QWidget *parent)
    : QDialog(parent)
{
    //绑定临时登录的信号槽
    auto dataCenter = model::DataCenter::getInstance();
    connect(dataCenter,&model::DataCenter::tempLoginDone,this,&StartPage::tempLoginDone);
}

void StartPage::startUp()
{
    QTimer* timer = new QTimer(this);
    auto dataCenter = model::DataCenter::getInstance();
    dataCenter->tempLoginAsync();//发送登录请求
    timer->setSingleShot(true);//允许重复触发定时器
    connect(timer,&QTimer::timeout,this,[=](){
        if(isLogin){
            timer->stop();
            close();
            timer->deleteLater();
        }//当登录成功之后再关闭启动页面
    });
    timer->start(2000);
}

void StartPage::tempLoginDone()
{
    //设置确认登录状态
    isLogin = true;
}

接下来我们启动客户端与服务端就能看到如下效果:
在这里插入图片描述
在这里插入图片描述

三.首页-获取全部视频列表

刚进⼊首页时,在用户还没有进行任何选择情况下,默认先获取所有视频列表,给用户⼀个默认的视频信息页面,默认情况下⼀次性获取20个视频,这些视频按照播放量和点赞量之后降序排列。
请求URL: POST /HttpService/allVideoList
请求参数(客户端):

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客⼾端会话ID
pageIndex integer ⻚码
pageCount integer 每⻚条⽬数量

示例:

{
	"requestId": "string",
	"sessionId": "string",
	"pageIndex": 0,
	"pageCount": 0
}

针对单个分类下视频数量庞大的情况,我们采用分页加载机制来优化性能。系统默认每页加载20个视频数据,当用户滚动至页面底部时自动触发下一页的加载。其中,pageCount参数控制每页加载的视频数量,pageIndex参数则指定当前加载的页码。针对单个分类下视频数量庞大的情况,我们采用分页加载机制来优化性能。系统默认每页加载20个视频数据,当用户滚动至页面底部时自动触发下一页的加载。其中,pageCount参数控制每页加载的视频数量,pageIndex参数则指定当前加载的页码。

返回响应:200 OK

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
result object 响应结果
totalCount integer 总数量
videoList array 视频列表
videoId string 视频ID
userId string 所属用户ID
userAvatarId string 用户头像ID
nickname string 所属用户名称
videoFiled string 视频文件ID
photoFiled string 封面文件ID
likeCount integer 点赞数量
playCount integer 播放数量
videoSize integer 视频大小
videoDesc string 视频简介
videoTitle string 视频标题
videoDuration integer 视频时长
videoUpTime string 视频上架时间

示例:

{
	"requestId": "string",
	"errorCode": 0,
	"errorMsg": "",
	"result": {
		"totalCount":0
		"videoList": [
		{
			"videoId": "string",
			"userId": "string",
			"userAvatarId":"string",
			"nickname": "string",
			"videoFileId": "string",
			"photoFileId": "string",
			"likeCount": 0,
			"playCount": 0,
			"videoSize": 0,
			"videoDesc": "string",
			"videoTitle": "string",
			"videoDuration": 0,
			"videoUpTime": "string"
		}
		]
	}
}

**注意:videoList是⼀个数组,内部存放返回给客⼾端的单个视频JSON对象。**根据响应中单个视频中包含的信息,除此之外客⼾端在解析响应结果时,需要将所有视频信息组织起来,所以定义描述视频信息的结构以及管理这些视频信息的视频列表实现如下:

//data.h
////////////////////////////
/////////单条视频的信息结构
////////////////////////////
class VideoInfo{
public:
    QString videoId;
    QString userId;
    QString userAvatarId;
    QString nickname;
    QString videoFileId;
    QString photoFileId;
    int64_t likeCount;
    int64_t playCount;
    int64_t videoSize;
    QString videoDesc;
    QString videoTitle;
    int64_t videoDuration;
    QString videoUpTime;

    //加载json对象到单个VideoInfo中
    void loadJsonResultToVideoInfo(const QJsonObject& result);
};

////////////////////////////
/////////视频列表的信息结构
////////////////////////////
class VideoList{
public:
    VideoList();
    // 设置或获取下⼀次要获取视频⻚⻚号
    void setPageIndex(int64_t pageIndex);
    int64_t getPageIndex()const;
    // 获取视频列表中:实际视频个数
    int64_t getVideoCount()const;
    // 设置或获取特定条件下(⽐如分类)总视频个数,视频审核⻚⾯⽤来计算分⻚器上总⻚数
    void setVideoTotalCount(int64_t videoTotalCount);
    int64_t getVideoTotalCount()const;
    // 往视频列表中添加视频
    void addVideo(const VideoInfo& videoInfo);
    // 获取排序后的视频列表-按照点赞量播放量及观看量的总和进行排序
    const QList<VideoInfo>& getVideoList()const;
    // 将列表中的所有视频清空
    void clearVideoList();

    const static int PAGE_COUNT = 20;
    QList<VideoInfo> videoInfos; // ⽬前从服务器获取下来的视频数据
    int64_t pageIndex; // ⻚⾯索引
    int64_t videototalCount;// ⽤视频总数和PAGE_COUNT能计算出该分类下总共有多少⻚视频
};

//data.cpp
////////////////////////////
/////////单条视频的信息结构
////////////////////////////
void VideoInfo::loadJsonResultToVideoInfo(const QJsonObject &result)
{
    videoId = result["videoId"].toString();
    userId = result["userId"].toString();
    userAvatarId = result["userAvatarId"].toString();
    nickname = result["nickname"].toString();
    videoFileId = result["videoFileId"].toString();
    photoFileId = result["photoFileId"].toString();
    likeCount = result["likeCount"].toInteger();
    playCount = result["playCount"].toInteger();
    videoSize = result["videoSize"].toInteger();
    videoDesc = result["videoDesc"].toString();
    videoTitle = result["videoTitle"].toString();
    videoDuration = result["videoDuration"].toInteger();
    videoUpTime = result["videoUpTime"].toString();
}


////////////////////////////
/////////视频列表的信息结构
////////////////////////////
VideoList::VideoList()
    :pageIndex(1),
    videototalCount(0)
{}

void VideoList::setPageIndex(int64_t pageIndex)
{
    this->pageIndex = pageIndex;
}

int64_t VideoList::getPageIndex() const
{
    return pageIndex;
}

int64_t VideoList::getVideoCount() const
{
    return videoInfos.size();
}

void VideoList::setVideoTotalCount(int64_t videoTotalCount)
{
    this->videototalCount = videoTotalCount;
}

int64_t VideoList::getVideoTotalCount() const
{
    return videototalCount;
}

void VideoList::addVideo(const VideoInfo &videoInfo)
{
    videoInfos.append(videoInfo);
}

const QList<VideoInfo> &VideoList::getVideoList() const
{
    return videoInfos;
}

void VideoList::clearVideoList()
{
    videoInfos.clear();
    pageIndex = 1;
    videototalCount = 0;
}

因为之后单条视频的信息是要设置到videobox中的,所以我们需要在videobox中处理videoInfo提供的信息:

//videobox.h
namespace Ui {
class VideoBox;
}

class VideoBox : public QWidget
{
    Q_OBJECT

public:
    explicit VideoBox(model::VideoInfo videoInfo,QWidget *parent = nullptr);
    //根据videoInfo更新视频块信息
    void updateVideoUi();
    //设置点赞数播放数为标准的显示格式
    QString setCount(int64_t count);
    //设置时间为标准的显示格式
    QString setTime(int64_t time);
private:
    Ui::VideoBox *ui;
    PlayerPage* videoPlayer;
    //当前视频的播放信息
    model::VideoInfo videoInfo;
};

//videobox.cpp
#include "videobox.h"
#include "ui_videobox.h"
#include "util.h"
#include <QDir>
#include <QString>

VideoBox::VideoBox(model::VideoInfo videoInfo,QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::VideoBox)
    , videoInfo(videoInfo)
{
    //根据videoInfo更新界面
    updateVideoUi();
}

void VideoBox::updateVideoUi()
{
    //设置ui上需要展示的所有视频信息
    ui->userNikeName->setText(videoInfo.nickname);
    ui->likeNum->setText(setCount(videoInfo.likeCount));
    ui->playNum->setText(setCount(videoInfo.playCount));
    ui->videoTitle->setText(videoInfo.videoTitle);
    ui->videoDuration->setText(setTime(videoInfo.videoDuration));
    ui->lodeupTime->setText(videoInfo.videoUpTime);
}

QString VideoBox::setCount(int64_t count)
{
    if(count >= 10000)
    {
        return QString("%1万").arg(QString::number(count / 10000.0));
    }
    return QString::number(count);
}

QString VideoBox::setTime(int64_t time)
{
    QString res;
    if(time/60/60)
    {
        res += QString("%1:").arg(time/60/60,2,10,QChar('0'));
    }
    res += QString("%1:%2").arg(time/60%60,2,10,QChar('0')).arg(time%60,2,10,QChar('0'));
    return res;
}

接下来定义实现异步接口与完成信号-同时当服务端返回响应报文时,将报文中的视频信息添加到DataCenter类中管理起来:

//datacenter.h
class DataCenter : public QObject
{
    Q_OBJECT
public:
    void deleteDataCenter();
    const QString& getSessionId();//获取本次登录服务端返回的sessionId
    void setSessionId(const QString& sessionId);//设置本次登录的sessionId
    void setHomeVideoList(const QJsonObject& result);//通过网络获取的json对象更新当前首页的视频
    VideoList* getHomeVideoListPtr();//获取首页视频列表指针
    //根据jsonresult设置视频列表信息
    void setVideoList(const QJsonObject &videoListJsonObj);
private:
    //本次登录的sessionId
    QString sessionId;
    //管理首页视频的视频列表
    VideoList* homeVideoList = nullptr;

public:
    //获取当前全部视频列表
    void getAllVideoListAsync();
signals:
    void getAllVideoListDone();
};
//datacenter.cpp
#include "datacenter.h"

namespace model{

const QString &DataCenter::getSessionId()
{
    return sessionId;
}

void DataCenter::setSessionId(const QString &sessionId)
{
    this->sessionId = sessionId;
}

VideoList *DataCenter::getHomeVideoListPtr()
{
    if(homeVideoList == nullptr)
    {
        //初始化首页视频列表
        homeVideoList = new VideoList();
    }
    return homeVideoList;
}

void DataCenter::setVideoList(const QJsonObject &result)
{
    // 保证videoList对象先构造了
    getHomeVideoListPtr();
    model::VideoList* homeVideoList = getHomeVideoListPtr();
    homeVideoList->setVideoTotalCount(result["totalCount"].toInteger());
    QJsonArray videoList = result["videoList"].toArray();
    for(int i = 0;i < videoList.size();i++)
    {
        QJsonObject videoInfoObj = videoList[i].toObject();
        model::VideoInfo videoInfo;
        videoInfo.loadJsonResultToVideoInfo(videoInfoObj);
        homeVideoList->addVideo(videoInfo);
    }
}

DataCenter::~DataCenter()
{
    if(kindsAndTags)
        delete kindsAndTags;
    kindsAndTags = nullptr;
    if(homeVideoList)
        delete homeVideoList;
    homeVideoList = nullptr;
}

void DataCenter::getAllVideoListAsync()
{
    netClient.getAllVideoList();
}
}

在netclient类中实现getAllVideoList客户端请求方法:

//netclient.h
namespace netclient{
class NetClient : public QObject
{
    Q_OBJECT
public:
    void getAllVideoList();//全部视频列表获取请求
};
//netclient.cpp
void NetClient::getAllVideoList()
{
    /*
     * "requestId": "string",
     * "sessionId": "string",
     * "pageIndex": 0,
     * "pageCount": 0
     */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["pageIndex"] = dataCenter->getHomeVideoListPtr()->getPageIndex();
    reqbody["pageCount"] = model::VideoList::PAGE_COUNT;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/allVideoList",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        dataCenter->setVideoList(result);
        emit dataCenter->getAllVideoListDone();
    });
}

客户端处理完毕,接下来我们在mockServer中构造一批假数据作为响应返回给客户端:

//mockserver.h
public:
    QHttpServerResponse getAllVideoList(const QHttpServerRequest &request);//获取全部视频列表接口
//mockserver.cpp
/*
 * "result": {
    "totalCount":0
    "videoList": [
    {
        "videoId": "string",
        "userId": "string",
        "userAvatarId":"string",
        "nickname": "string",
        "videoFileId": "string",
        "photoFileId": "string",
        "likeCount": 0,
        "playCount": 0,
        "videoSize": 0,
        "videoDesc": "string",
        "videoTitle": "string",
        "videoDuration": 0,
        "videoUpTime": "string"
    }
    ]
}
 */

QHttpServerResponse MockServer::getAllVideoList(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[getAllVideoList] 收到 getAllVideoList 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    //获取页号与获取视频数目
    int pageCount = reqData["pageCount"].toInt();
    int64_t pageIndex = reqData["pageIndex"].toInteger();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    //总视频个数设置为100
    result["totalCount"] = 100;
    QJsonArray videoList;
    //构造假数据
    int videoId = 10000;
    int userId = 10000;
    int resourceId = 1000;
    int fileId = 10000;
    int maxVideoNum = qMin(100,pageCount * pageIndex);
    for(int i = pageCount * (pageIndex - 1); i < maxVideoNum; ++i){
        QJsonObject videoJsonObj;
        videoJsonObj["videoId"] = QString::number(videoId++);
        videoJsonObj["userId"] = QString::number(userId++);
        videoJsonObj["nickname"] = "咻114514";
        videoJsonObj["userAvatarId"] = QString::number(resourceId++);
        videoJsonObj["photoFileId"] = QString::number(resourceId++);
        videoJsonObj["videoFileId"] = QString::number(fileId++);
        videoJsonObj["likeCount"] = 9867;
        videoJsonObj["playCount"] = 2105000;
        videoJsonObj["videoSize"] = 10240;
        videoJsonObj["videoDesc"] = "『Bad Apple!! feat.SEKAI』\n25時、ナイトコードで。 × 初音ミク\n作詞:Haruka-作曲:ZUN(上海アリス幻樂団)-編曲:ビートまりお×まろん、まらしぃ、Masayoshi Minoshima";
        videoJsonObj["videoTitle"] = "【25時、ナイトコードで。 × 初音ミク】Bad Apple!!【2DMV/世界计划 × 东方project 联动收录曲】";
        videoJsonObj["videoDuration"] = 231;
        videoJsonObj["videoUpTime"] = "2024-05-04 17:11:11";
        videoList.append(videoJsonObj);
    }
    result["videoList"] = videoList;
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

收到视频信息之后,我们需要在首页进行视频的更新,那么就应该在首页类中处理getAllVideoListDone信号,然后将datacenter中记录的所有videoInfo更新到首页中去:

//homepagewidget.h
#ifndef HOMEPAGEWIDGET_H
#define HOMEPAGEWIDGET_H

#include <QWidget>
#include <QPushButton>

namespace Ui {
class HomePageWidget;
}

//记录当前获取视频的方式
enum VideoListStyle{
    AllStyle, // 所有视频列表
    KindStyle, // 分类视频列表
    TagStyle, // 标签视频列表
    SearchStyle // 搜索视频列表
};

class HomePageWidget : public QWidget
{
    Q_OBJECT

public:
    //链接网络部分的所有信号槽函数
    void connectSingalsAndSlotsNet();
    //更新首页当前的视频
    void updateShowVideos();
};

#endif // HOMEPAGEWIDGET_H
//homepagewidget.cpp
#include "homepagewidget.h"
#include "ui_homepagewidget.h"
#include "videobox.h"
#include "util.h"
#include "./model/datacenter.h"
#include <QScrollBar>

HomePageWidget::HomePageWidget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::HomePageWidget)
{
    ui->setupUi(this);
    connectSingalsAndSlotsNet();
    initVideos();
}

void HomePageWidget::initVideos()
{
    //设置视频块对齐规则
    ui->videoGLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
    auto dataCenter = model::DataCenter::getInstance();
    dataCenter->getAllVideoListAsync();
}

void HomePageWidget::connectSingalsAndSlotsNet()
{
    auto dataCenter = model::DataCenter::getInstance();
    //处理视频信息更新好的消息
    connect(dataCenter,&model::DataCenter::getAllVideoListDone,this,&HomePageWidget::updateShowVideos);
}

void HomePageWidget::updateShowVideos()
{
    //更新视频到首页
    auto dataCenter = model::DataCenter::getInstance();
    auto VideoList = dataCenter->getHomeVideoListPtr()->getVideoList();
    int count = ui->videoGLayout->count();
    for(int i = count;i < VideoList.size();i++)
    {
        VideoBox* video = new VideoBox(VideoList[i]);
        ui->videoGLayout->addWidget(video,i/4,i%4);
    }
    LOG() << "视频更新完毕,首页中此时一共有 :" << ui->videoGLayout->count() << "个视频";
    //更新视频页数
    dataCenter->getHomeVideoListPtr()->pageIndex++;
}

四.首页-获取分类,标签,搜索视频列表,顺带实现置顶与刷新以及获取下一页视频的方法

由于点击分类标签之后需要更新dataCenter中的视频列表,所以需要先将原有视频列表清空,这里我们在首页类中新增清空视频列表的方法:

//homepagewidget.cpp
void HomePageWidget::clearShowVideos()
{
    //重置滑动条位置
    ui->videoScroll->verticalScrollBar()->setValue(0);
    //清空dataCenter中的videoList
    auto dataCenter = model::DataCenter::getInstance();
    dataCenter->getHomeVideoListPtr()->clearVideoList();
    //清空界面中的所有视频
    QLayoutItem* VideoItem;
    while ((VideoItem = ui->videoGLayout->takeAt(0)) != nullptr) {
        // 先删除item中的widget(如果有的话)
        if (QWidget* widget = VideoItem->widget()) {
            delete widget;
        }
        // 然后删除item本身
        delete VideoItem;
    }
}

由于获取全部视频列表与分类视频列表,标签视频列表以及搜索视频列表的实现机制几乎类似,同时他们的响应字段均相同,请求字段后三者仅比前者多了一处不同,所以接下来我们实现过程不再赘述对于这三者,直接给出实现的方法。
获取分类视频列表的接口描述如下:
请求URL:
对于分类视频列表: POST /HttpService/allVideoList
对于标签视频列表: POST /HttpService/tagVideoList
对于搜索视频列表: POST /HttpService/keyVideoList
请求参数(客户端):

字段名称 字段类型 字段说明
requestId string 请求ID
sessionId string 客⼾端会话ID
videoTypeId integer 视频分类类型
pageIndex integer ⻚码
pageCount integer 每⻚条⽬数量

对于标签视频列表第三个字段为:
videoTag integer 视频标签类型
对于搜索视频列表第三个字段为:
searchKey string 搜索关键字

示例:

{
	"requestId": "string",
	"sessionId": "string",
	"videoTypeId": 0,
	"pageIndex": 0,
	"pageCount": 0
}

对于标签视频列表第三个字段为:
“videoTag” : 0
对于搜索视频列表第三个字段为:
“searchKey” : “string”

返回响应:200 OK

字段名称 字段类型 字段说明
requestId string 请求ID
errorCode integer 错误码;0-成功
errorMsg string 错误信息
result object 响应结果
totalCount integer 总数量
videoList array 视频列表
videoId string 视频ID
userId string 所属用户ID
userAvatarId string 用户头像ID
nickname string 所属用户名称
videoFiled string 视频文件ID
photoFiled string 封面文件ID
likeCount integer 点赞数量
playCount integer 播放数量
videoSize integer 视频大小
videoDesc string 视频简介
videoTitle string 视频标题
videoDuration integer 视频时长
videoUpTime string 视频上架时间

示例:

{
	"requestId": "string",
	"errorCode": 0,
	"errorMsg": "",
	"result": {
		"totalCount":0
		"videoList": [
		{
			"videoId": "string",
			"userId": "string",
			"userAvatarId":"string",
			"nickname": "string",
			"videoFileId": "string",
			"photoFileId": "string",
			"likeCount": 0,
			"playCount": 0,
			"videoSize": 0,
			"videoDesc": "string",
			"videoTitle": "string",
			"videoDuration": 0,
			"videoUpTime": "string"
		}
		]
	}
}

datacenter中新增这三者的完成信号与异步请求方法:

//datacenter.h
namespace model{

class DataCenter : public QObject
{
    Q_OBJECT
public:
    //获取当前分类视频列表
    void getAlltypeVideoListAsync(int classifyId);
    //获取当前标签视频列表
    void getAlltagVideoListAsync(int tagId);
    //获取搜索视频列表
    void getAllkeyVideoListAsync(const QString& searchKey);
signals:
    void getAlltypeVideoListDone();
    //获取标签下的所有视频成功
    void getAlltagVideoListDone();
    //获取对应搜索key下的所有视频成功
    void getAllkeyVideoListDone();
};

}
//datacenter.cpp
#include "datacenter.h"

namespace model{

void DataCenter::getAlltypeVideoListAsync(int classifyId)
{
    netClient.getAlltypeVideoList(classifyId);
}

void DataCenter::getAlltagVideoListAsync(int tagId)
{
    netClient.getAlltagVideoList(tagId);
}

void DataCenter::getAllkeyVideoListAsync(const QString &searchKey)
{
    netClient.getAllkeyVideoList(searchKey);
}
}

在netclient中实现对应方法:

//netclient.h
#ifndef NETCLIENT_H
#define NETCLIENT_H

#include <QObject>
#include <QNetworkAccessManager>

namespace model{
class DataCenter;
}

namespace netclient{
class NetClient : public QObject
{
    Q_OBJECT
public:
    void getAlltypeVideoList(int classifyId);//当前分类下视频列表的获取请求
    void getAlltagVideoList(int tagId);//当前标签下视频列表的获取请求
    void getAllkeyVideoList(const QString& searchKey);//当前搜索key下视频列表的获取请求
};
}//end netclient

#endif // NETCLIENT_H

//netclient.cpp
void NetClient::getAlltypeVideoList(int classifyId)
{
    /*
     * "requestId": "string",
     * "sessionId": "string",
     * "videoTypeId": 0,
     * "pageIndex": 0,
     * "pageCount": 0
     */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["videoTypeId"] = classifyId;
    reqbody["pageIndex"] = dataCenter->getHomeVideoListPtr()->getPageIndex();
    reqbody["pageCount"] = model::VideoList::PAGE_COUNT;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/typeVideoList",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        dataCenter->setVideoList(result);
        emit dataCenter->getAlltypeVideoListDone();
    });
}

void NetClient::getAlltagVideoList(int tagId)
{
    /*
     * "requestId": "string",
     * "sessionId": "string",
     * "videoTag": 0,
     * "pageIndex": 0,
     * "pageCount": 0
     */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["videoTag"] = tagId;
    reqbody["pageIndex"] = dataCenter->getHomeVideoListPtr()->getPageIndex();
    reqbody["pageCount"] = model::VideoList::PAGE_COUNT;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/tagVideoList",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        dataCenter->setVideoList(result);
        emit dataCenter->getAlltagVideoListDone();
    });
}

void NetClient::getAllkeyVideoList(const QString &searchKey)
{
    /*
     * "requestId": "string",
     * "sessionId": "string",
     * "searchKey": "string",
     * "pageIndex": 0,
     * "pageCount": 0
     */
    //请求报文的body
    QJsonObject reqbody;
    reqbody["requestId"] = makeRequestId();
    reqbody["sessionId"] = dataCenter->getSessionId();
    reqbody["searchKey"] = searchKey;
    reqbody["pageIndex"] = dataCenter->getHomeVideoListPtr()->getPageIndex();
    reqbody["pageCount"] = model::VideoList::PAGE_COUNT;
    QNetworkReply* reply = sendReqToHttpServer("/HttpService/keyVideoList",reqbody);
    //异步处理服务端的response
    connect(reply,&QNetworkReply::finished,this,[=](){
        bool ok = true;
        QString reason;
        QJsonObject respBody = handleRespFromHttpServer(reply,ok,reason);
        if(!ok)
        {
            //说明reply有问题打印错误信息并返回
            LOG() << reason;
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        //解析成功,处理响应信息
        QJsonObject result = respBody["result"].toObject();
        dataCenter->setVideoList(result);
        emit dataCenter->getAllkeyVideoListDone();
    });
}

在mockServer构造假数据响应给客户端:

//mockserver.h
#ifndef MOCKSERVER_H
#define MOCKSERVER_H

#include <QWidget>
#include <QHttpServer>

QT_BEGIN_NAMESPACE
namespace Ui {
class MockServer;
}
QT_END_NAMESPACE

enum UserType{
    SuperAdmin = 1, // 超级管理员
    Admin = 2, // 管理员
    User = 3, // 普通⽤⼾
    TempUser = 4 // 临时⽤⼾
};

class MockServer : public QWidget
{
    Q_OBJECT

public:
    QHttpServerResponse getAlltypeVideoList(const QHttpServerRequest &request);//获取当前分类下所有视频的接口
    QHttpServerResponse getAlltagVideoList(const QHttpServerRequest &request);//获取当前标签下所有视频的接口
    QHttpServerResponse getAllkeyVideoList(const QHttpServerRequest &request);//获取当前搜索文本对应的所有视频的接口
};
#endif // MOCKSERVER_H

//mockserver.cpp
QHttpServerResponse MockServer::getAlltypeVideoList(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[getAlltypeVideoList] 收到 getAlltypeVideoList 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "videoTypeId = " << reqData["videoTypeId"].toInt();
    //获取页号与获取视频数目
    int pageCount = reqData["pageCount"].toInt();
    int64_t pageIndex = reqData["pageIndex"].toInteger();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    //总视频个数设置为100
    result["totalCount"] = 100;
    QJsonArray videoList;
    //构造假数据
    int videoId = 20000;
    int userId = 20000;
    int resourceId = 2000;
    int fileId = 20000;
    int maxVideoNum = qMin(100,pageCount * pageIndex);
    for(int i = pageCount * (pageIndex - 1); i < maxVideoNum; ++i){
        QJsonObject videoJsonObj;
        videoJsonObj["videoId"] = QString::number(videoId++);
        videoJsonObj["userId"] = QString::number(userId++);
        videoJsonObj["nickname"] = "咻114514";
        videoJsonObj["userAvatarId"] = QString::number(resourceId++);
        videoJsonObj["photoFileId"] = QString::number(resourceId++);
        videoJsonObj["videoFileId"] = QString::number(fileId++);
        videoJsonObj["likeCount"] = 9867;
        videoJsonObj["playCount"] = 2105000;
        videoJsonObj["videoSize"] = 10240;
        videoJsonObj["videoDesc"] = "『Bad Apple!! feat.SEKAI』\n25時、ナイトコードで。 × 初音ミク\n作詞:Haruka-作曲:ZUN(上海アリス幻樂団)-編曲:ビートまりお×まろん、まらしぃ、Masayoshi Minoshima";
        videoJsonObj["videoTitle"] = "【25時、ナイトコードで。 × 初音ミク】Bad Apple!!【2DMV/世界计划 × 东方project 联动收录曲】";
        videoJsonObj["videoDuration"] = 231;
        videoJsonObj["videoUpTime"] = "2024-05-04 17:11:11";
        videoList.append(videoJsonObj);
    }
    result["videoList"] = videoList;
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

QHttpServerResponse MockServer::getAlltagVideoList(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[getAlltagVideoList] 收到 getAlltagVideoList 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "videoTag = " << reqData["videoTag"].toInt();
    //获取页号与获取视频数目
    int pageCount = reqData["pageCount"].toInt();
    int64_t pageIndex = reqData["pageIndex"].toInteger();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    //总视频个数设置为100
    result["totalCount"] = 100;
    QJsonArray videoList;
    //构造假数据
    int videoId = 30000;
    int userId = 30000;
    int resourceId = 3000;
    int fileId = 30000;
    int maxVideoNum = qMin(100,pageCount * pageIndex);
    for(int i = pageCount * (pageIndex - 1); i < maxVideoNum; ++i){
        QJsonObject videoJsonObj;
        videoJsonObj["videoId"] = QString::number(videoId++);
        videoJsonObj["userId"] = QString::number(userId++);
        videoJsonObj["nickname"] = "咻114514";
        videoJsonObj["userAvatarId"] = QString::number(resourceId++);
        videoJsonObj["photoFileId"] = QString::number(resourceId++);
        videoJsonObj["videoFileId"] = QString::number(fileId++);
        videoJsonObj["likeCount"] = 9867;
        videoJsonObj["playCount"] = 2105000;
        videoJsonObj["videoSize"] = 10240;
        videoJsonObj["videoDesc"] = "『Bad Apple!! feat.SEKAI』\n25時、ナイトコードで。 × 初音ミク\n作詞:Haruka-作曲:ZUN(上海アリス幻樂団)-編曲:ビートまりお×まろん、まらしぃ、Masayoshi Minoshima";
        videoJsonObj["videoTitle"] = "【25時、ナイトコードで。 × 初音ミク】Bad Apple!!【2DMV/世界计划 × 东方project 联动收录曲】";
        videoJsonObj["videoDuration"] = 231;
        videoJsonObj["videoUpTime"] = "2024-05-04 17:11:11";
        videoList.append(videoJsonObj);
    }
    result["videoList"] = videoList;
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

QHttpServerResponse MockServer::getAllkeyVideoList(const QHttpServerRequest &request)
{
    //构建body信息
    QJsonObject reqData = QJsonDocument::fromJson(request.body()).object();
    LOG() << "[getAllkeyVideoList] 收到 getAllkeyVideoList 请求, requestId = " <<reqData["requestId"].toString() << "sessionId = " << reqData["sessionId"].toString();
    LOG() << "searchKey = " << reqData["searchKey"].toString();
    //获取页号与获取视频数目
    int pageCount = reqData["pageCount"].toInt();
    int64_t pageIndex = reqData["pageIndex"].toInteger();
    QJsonObject respData;
    respData["requestId"] = reqData["requestId"].toString();
    respData["errorCode"] = 0;
    respData["errorMsg"] = "";
    QJsonObject result;
    //总视频个数设置为100
    result["totalCount"] = 100;
    QJsonArray videoList;
    //构造假数据
    int videoId = 40000;
    int userId = 40000;
    int resourceId = 4000;
    int fileId = 40000;
    int maxVideoNum = qMin(100,pageCount * pageIndex);
    for(int i = pageCount * (pageIndex - 1); i < maxVideoNum; ++i){
        QJsonObject videoJsonObj;
        videoJsonObj["videoId"] = QString::number(videoId++);
        videoJsonObj["userId"] = QString::number(userId++);
        videoJsonObj["nickname"] = "咻114514";
        videoJsonObj["userAvatarId"] = QString::number(resourceId++);
        videoJsonObj["photoFileId"] = QString::number(resourceId++);
        videoJsonObj["videoFileId"] = QString::number(fileId++);
        videoJsonObj["likeCount"] = 9867;
        videoJsonObj["playCount"] = 2105000;
        videoJsonObj["videoSize"] = 10240;
        videoJsonObj["videoDesc"] = "『Bad Apple!! feat.SEKAI』\n25時、ナイトコードで。 × 初音ミク\n作詞:Haruka-作曲:ZUN(上海アリス幻樂団)-編曲:ビートまりお×まろん、まらしぃ、Masayoshi Minoshima";
        videoJsonObj["videoTitle"] = "【25時、ナイトコードで。 × 初音ミク】Bad Apple!!【2DMV/世界计划 × 东方project 联动收录曲】";
        videoJsonObj["videoDuration"] = 231;
        videoJsonObj["videoUpTime"] = "2024-05-04 17:11:11";
        videoList.append(videoJsonObj);
    }
    result["videoList"] = videoList;
    respData["result"] = result;
    //构建http响应报文
    QHttpServerResponse response(QJsonDocument(respData).toJson(),QHttpServerResponse::StatusCode::Ok);
    response.setHeader("Content-Type","application/json;charset=utf-8");
    return response;
}

记得不要忘记在init函数中路由我们规定好的URI。
接下来我们在首页调用datacenter中的异步方法并处理完成信号,同时实现置顶与刷新方法以及获取下一页视频的方法,因为比较多,这里我就不像之前那样只给新增的部分了,而是把对应模块的cpp与h文件全部给出,以便读者理解:

//homepagewidget.h
#ifndef HOMEPAGEWIDGET_H
#define HOMEPAGEWIDGET_H

#include <QWidget>
#include <QPushButton>

class HomePageWidget : public QWidget
{
    Q_OBJECT

public:
    explicit HomePageWidget(QWidget *parent = nullptr);
    //初始化分类栏与标签栏
    void initKindsAndTags();
    //初始化刷新和置顶按钮
    void initRefreshAndTop();
    //初始化首页视频列表
    void initVideos();
    //构建一个按钮对象
    QPushButton* buildSelectBtn(QWidget* parent,const QString& color,const QString& text);
    //重新设置标签栏
    void resetTags(const QList<QString>& kindToTags);
    //链接网络部分的所有信号槽函数
    void connectSingalsAndSlotsNet();
    //更新首页当前的视频
    void updateShowVideos();
    //清空当前页面所有视频
    void clearShowVideos();
    //发起搜索视频请求
    void searchRequest(const QString& searchKey);
    //检测滑动条位置决定更新策略
    void onSrcollAreaValueChanged(int value);
    ~HomePageWidget();

private slots:
    //设置分类与标签按钮被点击时的响应函数
    void onKindBtnClicked(QPushButton* kindBtn);
    void onTagBtnClicked(QPushButton* tagBtn);
    //设置置顶和刷新按钮被点击时的响应函数
    void onRefreshBtnClicked();
    void onTopBtnClicked();

private:
    Ui::HomePageWidget *ui;
    QHash<QString,QList<QString>> tags;
    QString currentKind;//辅助curTag找到对应Id
    QString currentTag;
    int curKind = 0;
    int curTag = 0;
    QString currentSearch;
    VideoListStyle videoListStyle = AllStyle;//当前获取视频的方式-默认为全部视频
};

#endif // HOMEPAGEWIDGET_H

//homepagewidget.cpp
#include "homepagewidget.h"
#include "ui_homepagewidget.h"
#include "videobox.h"
#include "util.h"
#include "./model/datacenter.h"
#include <QScrollBar>

HomePageWidget::HomePageWidget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::HomePageWidget)
{
    ui->setupUi(this);
    initKindsAndTags();
    initRefreshAndTop();
    connectSingalsAndSlotsNet();
    initVideos();
}

void HomePageWidget::initKindsAndTags()
{
    //添加首行分类标签
    QPushButton* classify = buildSelectBtn(ui->classify,"#3ECEFF","分类");
    classify->setDisabled(true);
    ui->classifyHLayout->addWidget(classify);
    auto dataCenter = model::DataCenter::getInstance();
    auto kindsAndTags = dataCenter->getKindsAndTags();

    //添加分类按钮顺便初始化tags
    const QList<QString> kinds = kindsAndTags->getAllKinds();
    for(auto& kind : kinds)
    {
        QPushButton* kindBtn = buildSelectBtn(ui->classify,"#222222",kind);
        ui->classifyHLayout->addWidget(kindBtn);
        //连接点击事件信号槽
        connect(kindBtn,&QPushButton::clicked,this,[=](){
            onKindBtnClicked(kindBtn);
        });
        //初始化tags
        tags.insert(kind,kindsAndTags->getAllTagsFromKind(kind));
    }
    //分类栏按钮设置一定间距
    ui->classifyHLayout->setSpacing(8);
    //标签栏按钮设置一定间距
    ui->labelHLayout->setSpacing(4);
    resetTags(tags[kinds[0]]);
}

void HomePageWidget::initRefreshAndTop()
{
    QWidget* widget = new QWidget(this);
    widget->setFixedSize(42, 94);
    widget->setStyleSheet("QPushButton:hover{background-color:#666666}"
                          "QPushButton{"
                          "background-color : #DDDDDD;"
                          "border-radius : 21px;"
                          "border : none;}");
    QPushButton* refresh = new QPushButton();
    refresh->setFixedSize(42, 42);
    refresh->setStyleSheet("border-image :url(:/images/homePage/shuaxin.png);");
    QPushButton* top = new QPushButton();
    top->setFixedSize(42, 42);
    top->setStyleSheet("border-image : url(:/images/homePage/zhiding.png)");

    QVBoxLayout* layout = new QVBoxLayout(widget);
    layout->addWidget(refresh);
    layout->addWidget(top);
    layout->setContentsMargins(0,0,0,0);
    layout->setSpacing(10);
    widget->move(1285, 630);
    widget->show();
    //为两个按钮分别绑定槽函数
    connect(refresh,&QPushButton::clicked,this,&HomePageWidget::onRefreshBtnClicked);
    connect(top,&QPushButton::clicked,this,&HomePageWidget::onTopBtnClicked);
}

void HomePageWidget::initVideos()
{
    //设置视频块对齐规则
    ui->videoGLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
    auto dataCenter = model::DataCenter::getInstance();
    dataCenter->getAllVideoListAsync();
}

QPushButton* HomePageWidget::buildSelectBtn(QWidget *parent, const QString &color, const QString &text)
{
    QPushButton* pushButton = new QPushButton(text,parent);
    pushButton->setMinimumHeight(26);
    pushButton->setFixedWidth(text.size()*16+18+18);//设置按钮宽度按照字体大小及个数动态变化
    pushButton->setStyleSheet("color : " + color + ";");
    return pushButton;
}

void HomePageWidget::onKindBtnClicked(QPushButton *kindBtn)
{
    auto dataCenter = model::DataCenter::getInstance();
    //重复点击不响应
    int kind = dataCenter->getKindsAndTags()->getKindId(kindBtn->text());
    if(curKind == kind)
    {
        return;
    }
    //更新curKind以及重置curTag
    currentKind = kindBtn->text();
    curKind = kind;
    curTag = 0;
    //设置当前按钮的点击颜色,同时移除其他按钮的颜色
    kindBtn->setStyleSheet("background-color: #F1FDFF;"
                           "color:#3ECEFF;");
    QList<QPushButton*> btns = ui->classify->findChildren<QPushButton*>();
    for(int i = 0;i < btns.size();i++)
    {
        if(btns[i] != kindBtn && btns[i]->text() != "分类")
        {
            btns[i]->setStyleSheet("color : #222222;");
        }
    }
    //重置标签栏
    resetTags(tags[kindBtn->text()]);
    //更新界面中的视频
    clearShowVideos();
    videoListStyle = KindStyle;
    dataCenter->getAlltypeVideoListAsync(curKind);
}

void HomePageWidget::onTagBtnClicked(QPushButton *tagBtn)
{
    auto dataCenter = model::DataCenter::getInstance();
    //重复点击不响应-同时正好解决了没有选择分类直接选择标签引起的问题
    int tag = dataCenter->getKindsAndTags()->getTagId(currentKind,tagBtn->text());
    if(curTag == tag)
    {
        return;
    }
    //更新curTag
    currentTag = tagBtn->text();
    curTag = tag;
    //设置当前按钮的点击颜色,同时移除其他按钮的颜色
    tagBtn->setStyleSheet("background-color: #F1FDFF;"
                          "color:#3ECEFF;");
    QList<QPushButton*> btns = ui->labels->findChildren<QPushButton*>();
    for(int i = 0;i < btns.size();i++)
    {
        if(btns[i] != tagBtn && btns[i]->text() != "标签")
        {
            btns[i]->setStyleSheet("color : #222222;");
        }
    }
    //更新界面中的视频
    clearShowVideos();
    videoListStyle = TagStyle;
    dataCenter->getAlltagVideoListAsync(curTag);
}

void HomePageWidget::onRefreshBtnClicked()
{
    //清空当前界面中的所有视频
    clearShowVideos();
    //到服务端获取视频数据
    auto dataCenter = model::DataCenter::getInstance();
    switch(videoListStyle){
    case AllStyle:
        dataCenter->getAllVideoListAsync();
        break;
    case KindStyle:
        dataCenter->getAlltypeVideoListAsync(curKind);
        break;
    case TagStyle:
        dataCenter->getAlltagVideoListAsync(curTag);
        break;
    case SearchStyle:
        dataCenter->getAllkeyVideoListAsync(currentSearch);
        break;
    default:
        LOG()<<"暂不⽀持的数据类型";
    }
}

void HomePageWidget::onTopBtnClicked()
{
    //置顶
    ui->videoScroll->verticalScrollBar()->setValue(0);
}

void HomePageWidget::resetTags(const QList<QString> &kindToTags)
{
    QList<QPushButton*> btns = ui->labels->findChildren<QPushButton*>();
    //移除并释放原来所有的标签
    for(int i = 0;i < btns.size();i++)
    {
        ui->labelHLayout->removeWidget(btns[i]);
        delete btns[i];
    }
    QPushButton* label = buildSelectBtn(ui->labels,"#3ECEFF","标签");
    label->setDisabled(true);
    ui->labelHLayout->addWidget(label);
    for(auto& tag : kindToTags)
    {
        QPushButton* tagBtn = buildSelectBtn(ui->labels,"#222222",tag);
        ui->labelHLayout->addWidget(tagBtn);
        connect(tagBtn,&QPushButton::clicked,this,[=](){
            onTagBtnClicked(tagBtn);
        });
    }
}

void HomePageWidget::connectSingalsAndSlotsNet()
{
    auto dataCenter = model::DataCenter::getInstance();
    //处理视频信息更新好的消息
    connect(dataCenter,&model::DataCenter::getAllVideoListDone,this,&HomePageWidget::updateShowVideos);
    connect(dataCenter,&model::DataCenter::getAlltypeVideoListDone,this,&HomePageWidget::updateShowVideos);
    connect(dataCenter,&model::DataCenter::getAlltagVideoListDone,this,&HomePageWidget::updateShowVideos);
    connect(dataCenter,&model::DataCenter::getAllkeyVideoListDone,this,&HomePageWidget::updateShowVideos);
    connect(ui->search,&SearchLineEdit::searchRequest,this,&HomePageWidget::searchRequest);
    //当竖直滚动条滑动到底部时获取的新视频
    connect(ui->videoScroll->verticalScrollBar(),&QScrollBar::valueChanged,this,&HomePageWidget::onSrcollAreaValueChanged);
}

void HomePageWidget::updateShowVideos()
{
    //更新视频到首页
    auto dataCenter = model::DataCenter::getInstance();
    auto VideoList = dataCenter->getHomeVideoListPtr()->getVideoList();
    int count = ui->videoGLayout->count();
    for(int i = count;i < VideoList.size();i++)
    {
        VideoBox* video = new VideoBox(VideoList[i]);
        ui->videoGLayout->addWidget(video,i/4,i%4);
    }
    LOG() << "视频更新完毕,首页中此时一共有 :" << ui->videoGLayout->count() << "个视频";
    //更新视频页数-辅助视频列表的更新功能
    dataCenter->getHomeVideoListPtr()->pageIndex++;
}

void HomePageWidget::clearShowVideos()
{
    //重置滑动条位置
    ui->videoScroll->verticalScrollBar()->setValue(0);
    //清空dataCenter中的videoList
    auto dataCenter = model::DataCenter::getInstance();
    dataCenter->getHomeVideoListPtr()->clearVideoList();
    //清空界面中的所有视频
    QLayoutItem* VideoItem;
    while ((VideoItem = ui->videoGLayout->takeAt(0)) != nullptr) {
        // 先删除item中的widget(如果有的话)
        if (QWidget* widget = VideoItem->widget()) {
            delete widget;
        }
        // 然后删除item本身
        delete VideoItem;
    }
}

void HomePageWidget::searchRequest(const QString &searchKey)
{
    clearShowVideos();
    videoListStyle = SearchStyle;
    auto dataCenter = model::DataCenter::getInstance();
    currentSearch = searchKey;
    dataCenter->getAllkeyVideoListAsync(searchKey);
}

void HomePageWidget::onSrcollAreaValueChanged(int value)
{
    //当置顶时不触发更新策略
    if(value == 0)
    {
        return;
    }
    //当获取视频数以达到上限时不再获取
    auto dataCenter = model::DataCenter::getInstance();
    auto videoList = dataCenter->getHomeVideoListPtr();
    if(videoList->getVideoCount() == videoList->getVideoTotalCount())
    {
        return;
    }
    //当进度条到达底部时再去进行视频更新
    if(value == ui->videoScroll->verticalScrollBar()->maximum())
    {
        // 继续获取下⼀⻚的视频数据
        // 到服务器获取视频数据
        switch(videoListStyle){
        case AllStyle:
            dataCenter->getAllVideoListAsync();
            break;
        case KindStyle:
            dataCenter->getAlltypeVideoListAsync(curKind);
            break;
        case TagStyle:
            dataCenter->getAlltagVideoListAsync(curTag);
            break;
        case SearchStyle:
            dataCenter->getAllkeyVideoListAsync(currentSearch);
            break;
        default:
            LOG()<<"暂不⽀持的数据类型";
        }
    }
}

HomePageWidget::~HomePageWidget()
{
    delete ui;
}

//searchlineedit.h
#ifndef SEARCHLINEEDIT_H
#define SEARCHLINEEDIT_H

#include <QLineEdit>

class SearchLineEdit : public QLineEdit
{
    Q_OBJECT
public:
    explicit SearchLineEdit(QWidget *parent = nullptr);
    void searchBtnClicked();//处理搜索事件
signals:
    void searchRequest(const QString& searchKey);
};

#endif // SEARCHLINEEDIT_H

//searchlineedit.cpp
#include "searchlineedit.h"
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>

SearchLineEdit::SearchLineEdit(QWidget *parent)
    : QLineEdit{parent}
{
    // 搜索框图标
    QLabel* searchImg = new QLabel(this);
    searchImg->setFixedSize(16, 16);
    searchImg->setPixmap(QPixmap(":/images/homePage/sousuo.png"));
    // 搜索框按钮
    QPushButton* searchBtn = new QPushButton("搜索",this);
    searchBtn->setCursor(QCursor(Qt::PointingHandCursor));//设置手型光标
    searchBtn->setFixedSize(62, 32);
    searchBtn->setStyleSheet("background-color : #3ECEFE;"
                             "border-radius : 16px;"
                             "font-size : 14px;"
                             "color : #FFFFFF;"
                             "font-style : normal;");
    this->setTextMargins(33, 0, 0, 0);//设置搜索框中的文字向右靠一些
    QHBoxLayout* hLayout = new QHBoxLayout(this);
    hLayout->addWidget(searchImg);
    hLayout->addStretch();//添加一个水平弹簧把二者撑到两边
    hLayout->addWidget(searchBtn);
    hLayout->setContentsMargins(11, 0, 2, 0);//左,上,右,下
    //绑定回车与点击事件
    connect(searchBtn, &QPushButton::clicked, this,
            &SearchLineEdit::searchBtnClicked);
    connect(this, &QLineEdit::returnPressed, this,
            &SearchLineEdit::searchBtnClicked);
}

void SearchLineEdit::searchBtnClicked()
{
    emit searchRequest(this->text());
    //清空搜索文本
    this->setText("");
}

五.videoBox页-下载图片接口

我们首先来分析下videoBox需要显示图片的部分,一处是视频的封面,还有一处是上传视频的用户头像,那么我们可以通过之前在videoInfo中存储的userAvatarId-用户头像ID , photoFiled-封面文件ID向服务端发送请求获取图片资源。而这些信息并不是敏感信息,所以我们这里换用GET的方法向服务端发送下载图片的请求:
请求URL : GET /HttpService/downloadPhoto?requestId=xxx&sessionId=xxx&fileId=xxx
请求参数也不用再设计了,因为它已经在URL中包含了。然后需要将Content-Type头部中的application/json更改为application/octet-stream。
客户端定义请求下载图片方法:

//datacenter.h
namespace model{

class DataCenter : public QObject
{
    Q_OBJECT
public:
    // 下载图⽚
    void downloadPhotoAsync(const QString& photoFileId);
signals:
    // 下载图⽚处理完毕
    // 每个VideoBox上都要下载视频封⾯和图⽚,导致下载图⽚完成信号会被绑定多次
    // ⼀个信号被绑定多次时,当该信号触发时会被执⾏多次
    // 添加imageId参数,表明是某控件触发的下载图⽚请求,才处理该次图⽚下载的界⾯显⽰
    void downloadPhotoDone(const QString& imageId, QByteArray imageData);
};
//datacenter.cpp
void DataCenter::downloadPhotoAsync(const QString &photoFileId)
{
    // 下载图⽚
    netClient.downloadPhoto(photoFileId);
}

//netclient.h
    void downloadPhoto(const QString& photoFileId);//下载图片请求
    
//netclient.cpp
void NetClient::downloadPhoto(const QString &photoFileId)
{
    // 1. 构造请求
    QString queryString;
    queryString += "requestId=";
    queryString += makeRequestId();
    queryString += "&";
    queryString += "sessionId=";
    queryString += dataCenter->getSessionId();
    queryString += "&";
    queryString += "fileId=";
    queryString += photoFileId;
    // 2. 发送请求
    QNetworkRequest httpReq;
    httpReq.setUrl(QUrl(HTTP_URL + "/HttpService/downloadPhoto?" + queryString));
    QNetworkReply* httpReply = netClientManager.get(httpReq);
    // 3. 异步处理响应
    connect(httpReply, &QNetworkReply::finished, this, [=](){
        // 解析响应
        if (httpReply->error() != QNetworkReply::NoError) {
            LOG() << httpReply->errorString();
            httpReply->deleteLater();
            return;
        }
        //2.获取图⽚数据
        // 发射信号,通知界⾯更视频显⽰
        httpReply->deleteLater();
        QByteArray imageData = httpReply->readAll();
        emit dataCenter->downloadPhotoDone(photoFileId, imageData);
        LOG() << "downloadPhoto请求结束,图片下载成功";
    });
}

这里也可以使用post,但一般不这样干,因为POST 请求虽然也可以在 URL 中带有查询参数,但这通常用于非核心的、辅助性的参数,而不是用于传递要创建或更新的主体数据。
不过我们现在并没有图片数据,所以只能先造一批假的图片数据了:
在这里插入图片描述
然后根据我们之前在处理视频列表中造的假数据设置相应图片ID与图片的映射关系:

//mockserver.h
class MockServer : public QWidget
{
    Q_OBJECT

public:
    // 构造响应数据
    void buildResponseData();
private:
    // 存放资源id和路径的对应关系
    QMap<int, QString> idPathTable;
};

//mockserver.cpp
void MockServer::buildResponseData()
{
    int resourceId = 1000;
    for(int i = 0;i < 20;i++)
    {
        idPathTable[resourceId++] = "/images/avatar1.png";
        idPathTable[resourceId++] = "/images/photofile1.jpg";
    }
    resourceId = 2000;
    for(int i = 0;i < 20;i++)
    {
        idPathTable[resourceId++] = "/images/avatar2.png";
        idPathTable[resourceId++] = "/images/photofile2.jpg";
    }
    resourceId = 3000;
    for(int i = 0;i < 20;i++)
    {
        idPathTable[resourceId++] = "/images/avatar3.png";
        idPathTable[resourceId++] = "/images/photofile3.png";
    }
    resourceId = 4000;
    for(int i = 0;i < 20;i++)
    {
        idPathTable[resourceId++] = "/images/avatar4.png";
        idPathTable[resourceId++] = "/images/photofile4.jpg";
    }
}

接下来我们利用这一批假数据返回响应给客户端:

//mockserver.h
    QHttpServerResponse downloadPhoto(const QHttpServerRequest &request);//下载图片的接口
//mockserver中的util.h
//将上传的文件转化为二进制流
static inline QByteArray loadFileToByteArray(const QString& filePath)
{
    QFile file(filePath);
    //以只读方式打开
    if(!file.open(QIODevice::ReadOnly))
    {
        LOG() << "文件打开失败";
        return QByteArray();
    }
    QByteArray res = file.readAll();
    file.close();
    return res;
}

// 把 QByteArray 中的内容, 写⼊到某个指定⽂件⾥
static inline void writeByteArrayToFile(const QString& path, const QByteArray& content) {
    QFile file(path);
    bool ok = file.open(QFile::WriteOnly);
    if (!ok) {
        LOG() << "⽂件打开失败!";
        return;
    }
    file.write(content);
    file.flush();
    file.close();
}
//mockserver.cpp
QHttpServerResponse MockServer::downloadPhoto(const QHttpServerRequest &request){
    // 解析查询字符串
    QUrlQuery query(request.url());
    QString requstId = query.queryItemValue("requestId");
    QString fileId = query.queryItemValue("fileId");
    QString sessionId = query.queryItemValue("sessionId");
    LOG() << "[downloadPhoto] 收到 downloadPhoto 请求, requestId=" << requstId << "sessionId =" << sessionId;
    // 构造图⽚路径
    QDir dir(QDir::currentPath());
    dir.cdUp();
    dir.cdUp();
    QString imagePath = dir.absolutePath();
    imagePath += idPathTable[fileId.toInt()];
    LOG()<<"图片ID:"<<fileId<<"--"<<imagePath;
    // 读取图⽚数据
    QByteArray imageData = loadFileToByteArray(imagePath);
    // 构造 HTTP 响应
    QHttpServerResponse httpResp(imageData,QHttpServerResponse::StatusCode::Ok);
    httpResp.setHeader("Content-Type", "application/octet-stream");
    return httpResp;
}

六.videoBox页-更新视频封面

这里我们就可以使用上面实现的下载图片接口来获取视频封面了,但是有一个问题,因为之前我们的imageBox是QWidget类型的,也就意味着我们不能像QLabel那样使用QPixmap加载服务端给的二进制流数据然后直接设置为封面图了。
所以我们有两种解决方法,一种就是重写videoBox的paintEvent的方法,另一种便是将imageBox类型修改为QLabel。后者比较简单,但是考虑到部分读者需要去改之前相关部分的代码,这里我们选用第一种方式,之前写的代码我们也不需要进行修改了:

//videobox.h
namespace Ui {
class VideoBox;
}

class VideoBox : public QWidget
{
    Q_OBJECT

public:
    // 重写paintEvent事件,避免图⽚平铺重叠以正常显示背景图片
    void paintEvent(QPaintEvent *event) override;
private:
    QPixmap videoCoverImage;
};

//videobox.cpp
#include "videobox.h"
#include "ui_videobox.h"
#include "util.h"
#include <QDir>
#include <QString>

void VideoBox::paintEvent(QPaintEvent *event)
{
    // 启用自动填充背景功能
    // 当设置为true时,Qt会在每次绘制控件前自动使用当前调色板中的画刷填充控件背景
    // 这确保了背景内容会在其他绘制操作之前被正确绘制
    ui->imageBox->setAutoFillBackground(true);

    // 获取imageBox控件当前的调色板对象
    // QPalette包含了控件各种状态下的颜色和画刷设置(正常、禁用、激活等状态)
    // 这里我们获取当前调色板以便修改其中的背景画刷设置
    QPalette palette = ui->imageBox->palette();

    // 对原始视频封面图片进行缩放处理以适应控件尺寸
    // 原始图片尺寸可能与界面控件尺寸存在较大差异,需要进行适当的缩放
    QPixmap scaledImage = videoCoverImage.scaled(
        ui->imageBox->size(),          // 目标尺寸:使用imageBox的当前大小
        Qt::KeepAspectRatioByExpanding, // 缩放模式:保持原始宽高比,但尽可能扩展以填满控件
        // 这种模式可能会使图片的一部分超出控件边界,但能确保控件被完全填满
        Qt::SmoothTransformation       // 变换模式:采用高质量的双线性插值算法进行缩放
        // 这能显著提高缩放后图片的视觉质量,避免锯齿和模糊
        );

    // 使用缩放后的图片创建画刷对象
    // QBrush定义了如何填充形状的背景模式,这里使用图片作为填充图案
    QBrush brush(scaledImage);

    // 将创建的画刷设置到调色板的Window角色中
    // QPalette::Window表示控件背景区域的画刷,这会影响整个控件的背景填充
    // 其他常用的角色还包括:Base(文本输入背景)、Text(文本颜色)、Button(按钮背景)等
    palette.setBrush(QPalette::Window, brush);

    // 将修改后的调色板应用回imageBox控件
    // 这会更新控件的视觉表现,使用新的背景画刷进行绘制
    // 注意:此处不能通过qss设置圆角样式,因为样式表会覆盖QPalette的背景设置
    // 导致背景图片无法显示(相反的后者也会覆盖前者,也就是说图片显示与圆角使用这种方法无法兼得),因此需要采用其他方法实现圆角效果,例如:
    // 1. 使用自定义绘制在paintEvent中直接绘制图片和圆角
    // 2. 使用事件过滤器拦截绘制事件
    // 3. 创建自定义QWidget子类重写paintEvent方法
    //因为原来的直角并不难看,为了不增加代码的复杂度便不再添加圆角了
    ui->imageBox->setPalette(palette);
}

接下来便可以编写代码,向服务端发送获取视频封面图片的请求,并处理返回的响应数据。

//videobox.h
namespace Ui {
class VideoBox;
}

class VideoBox : public QWidget
{
    Q_OBJECT
private:
    // 设置视频封⾯
    void setVideoImage(const QString& photoFileId);
private slots:
    void getVideoImageDown(const QString& photoId,QByteArray imageData);
};

//videobox.cpp
#include "videobox.h"
#include "ui_videobox.h"
#include "util.h"
#include <QDir>
#include <QString>

VideoBox::VideoBox(model::VideoInfo videoInfo,QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::VideoBox)
    , videoInfo(videoInfo)
{
    // 获取视频封⾯图⽚成功
    auto dataCenter = model::DataCenter::getInstance();
    connect(dataCenter, &model::DataCenter::downloadPhotoDone, this, &VideoBox::getVideoImageDown);
}

void VideoBox::updateVideoUi()
{
    setVideoImage(videoInfo.photoFileId);//设置视频封面信息
}

void VideoBox::setVideoImage(const QString &photoFileId)
{
    // 向服务器请求视频封⾯图⽚
    auto dataCenter = model::DataCenter::getInstance();
    dataCenter->downloadPhotoAsync(photoFileId);
}

void VideoBox::getVideoImageDown(const QString &photoId, QByteArray imageData)
{
    //当图片不属于当前box的封面拒绝更新
    if(photoId != videoInfo.photoFileId)
    {
        return;
    }
    // 将图⽚更新到界⾯
    videoCoverImage.loadFromData(imageData);
}

七.videoBox页-更新上传视频的用户头像

这就比较简单了,我们直接使用QPixmap加载服务端给的二进制流数据给他设置上去就ok了

//videobox.h
namespace Ui {
class VideoBox;
}

class VideoBox : public QWidget
{
    Q_OBJECT
private:
    //设置用户头像
    void setUserImage(const QString& userAvatarId);
private slots:
    void getAvatarImageDown(const QString& photoId,QByteArray imageData);
private:
    QPixmap userImage;
};

//videobox.cpp
void VideoBox::setUserImage(const QString &userAvatarId)
{
    if(userAvatarId.isEmpty()){
        ui->userIcon->setStyleSheet("border-image :url(:/images/myself/defaultAvatar.png);");
    }else{
        auto dataCenter = model::DataCenter::getInstance();
        dataCenter->downloadPhotoAsync(userAvatarId);
    }
}

void VideoBox::getAvatarImageDown(const QString &photoId, QByteArray imageData)
{
    if(photoId != videoInfo.userAvatarId)
    {
        return;
    }
    userImage = makeCircleIcon(imageData,ui->userIcon->width() / 2).pixmap(ui->userIcon->size());
    ui->userIcon->setPixmap(userImage);
}

这些业务逻辑处理完毕之后,我们能看到如下效果:
全部视频列表:
在这里插入图片描述
分类视频列表:
在这里插入图片描述
标签视频列表:
在这里插入图片描述
搜索视频列表:
在这里插入图片描述
在这里插入图片描述

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐