HarmonyOS 实战开发案例-仿抖音短视频应用

这篇具有很好参考价值的文章主要介绍了HarmonyOS 实战开发案例-仿抖音短视频应用。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前段时间看到一篇文章,但是没有源码,是一个仿写抖音的文章,最近也在看这块,顺便写个简单的短视频小应用。

技术点拆分

1、http请求数据;

2、measure计算文本宽度;

3、video播放视频;

4、onTouch上滑/下拉切换视频;

5、List实现滚动加载;

效果展示

HarmonyOS 实战开发案例-仿抖音短视频应用,鸿蒙,Harmony OS,OpenHarmony,harmonyos,音视频,移动开发,IT互联网,实战开发,华为

http请求数据

通过对@ohos.net.http进行二次封装,进行数据请求。

1、封装requestHttp;


import http from '@ohos.net.http';

// 1、创建RequestOption.ets 配置类
export interface RequestOptions {
  url?: string;
  method?: RequestMethod; // default is GET
  queryParams ?: Record<string, string>;
  extraData?: string | Object | ArrayBuffer;
  header?: Object; // default is 'content-type': 'application/json'
}

export enum RequestMethod {
  OPTIONS = "OPTIONS",
  GET = "GET",
  HEAD = "HEAD",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE",
  TRACE = "TRACE",
  CONNECT = "CONNECT"
}

/**
 * Http请求器
 */
export class HttpCore {
  /**
   * 发送请求
   * @param requestOption
   * @returns Promise
   */
  request<T>(requestOption: RequestOptions): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      this.sendRequest(requestOption)
        .then((response) => {
          if (typeof response.result !== 'string') {
            reject(new Error('Invalid data type'));

          } else {
            let bean: T = JSON.parse(response.result);
            if (bean) {
              resolve(bean);
            } else {
              reject(new Error('Invalid data type,JSON to T failed'));
            }

          }
        })
        .catch((error) => {
          reject(error);
        });
    });
  }

  private sendRequest(requestOption: RequestOptions): Promise<http.HttpResponse> {
    // 每一个httpRequest对应一个HTTP请求任务,不可复用
    let httpRequest = http.createHttp();

    let resolveFunction, rejectFunction;
    const resultPromise = new Promise<http.HttpResponse>((resolve, reject) => {
      resolveFunction = resolve;
      rejectFunction = reject;
    });

    if (!this.isValidUrl(requestOption.url)) {
      return Promise.reject(new Error('url格式不合法.'));
    }

    let promise = httpRequest.request(this.appendQueryParams(requestOption.url, requestOption.queryParams), {
      method: requestOption.method,
      header: requestOption.header,
      extraData: requestOption.extraData, // 当使用POST请求时此字段用于传递内容
      expectDataType: http.HttpDataType.STRING // 可选,指定返回数据的类型
    });

    promise.then((response) => {
      console.info('Result:' + response.result);
      console.info('code:' + response.responseCode);
      console.info('header:' + JSON.stringify(response.header));

      if (http.ResponseCode.OK !== response.responseCode) {
        throw new Error('http responseCode !=200');
      }
      resolveFunction(response);

    }).catch((err) => {
      rejectFunction(err);
    }).finally(() => {
      // 当该请求使用完毕时,调用destroy方法主动销毁。
      httpRequest.destroy();
    })
    return resultPromise;
  }


  private appendQueryParams(url: string, queryParams: Record<string, string>): string {
    // todo 使用将参数拼接到url上
    return url;
  }

  private isValidUrl(url: string): boolean {
    //todo 实现URL格式判断
    return true;
  }
}

// 实例化请求器
const httpCore = new HttpCore();


export class HttpManager {
  private static mInstance: HttpManager;

  // 防止实例化
  private constructor() {
  }

  static getInstance(): HttpManager {
    if (!HttpManager.mInstance) {
      HttpManager.mInstance = new HttpManager();
    }
    return HttpManager.mInstance;
  }


  request<T>(option: RequestOptions): Promise<T> {
    return new Promise(async (resolve, reject) => {
      try {
        const data: any = await httpCore.request(option)
        resolve(data)
      } catch (err) {
        reject(err)
      }
    })
  }
}

export default HttpManager;

2、使用requestHttp请求视频接口;

import httpManager, { RequestMethod } from '../../utils/requestHttp';

measure计算文本宽度


import httpManager, { RequestMethod } from '../../utils/requestHttp';

@State total: number = 0
@State listData: Array<ResultType> = []
private url: string = "https://api.apiopen.top/api/getHaoKanVideo?size=10";
private page: number = 0

private httpRequest() {
    httpManager.getInstance()
    .request({
      method: RequestMethod.GET,
      url: `${this.url}&page=${this.page}` //公开的API
    })
      .then((res: resultBean) => {
        this.listData = [...this.listData, ...res.result.list];
        this.total = res.result.total;
        this.duration = 0;
        this.rotateAngle = 0;
      })
      .catch((err) => {
        console.error(JSON.stringify(err));
      });
  }

video播放视频

1、通过videoController控制视频的播放和暂停,当一个视频播放结束,播放下一个

import measure from '@ohos.measure'

@State textWidth : number = measure.measureText({
  //要计算的文本内容,必填
  textContent: this.title,
})
  
// this.textWidth可以获取this.title的宽度

2、Video的一些常用方法

属性:

名称 参数类型 描述
muted boolean 是否静音。
默认值:false
autoPlay boolean 是否自动播放。
默认值:false
controls boolean 控制视频播放的控制栏是否显示。
默认值:true
objectFit ImageFit 设置视频显示模式。
默认值:Cover
loop boolean 是否单个视频循环播放。
默认值:false

事件:

名称 功能描述
onStart(event:() => void) 播放时触发该事件。
onPause(event:() => void) 暂停时触发该事件。
onFinish(event:() => void) 播放结束时触发该事件。
onError(event:() => void) 播放失败时触发该事件。
onPrepared(callback:(event: { duration: number }) => void) 视频准备完成时触发该事件。
duration:当前视频的时长,单位为秒(s)。
onSeeking(callback:(event: { time: number }) => void) 操作进度条过程时上报时间信息。
time:当前视频播放的进度,单位为s。
onSeeked(callback:(event: { time: number }) => void) 操作进度条完成后,上报播放时间信息。
time:当前视频播放的进度,单位为s。
onUpdate(callback:(event: { time: number }) => void) 播放进度变化时触发该事件。
time:当前视频播放的进度,单位为s。
onFullscreenChange(callback:(event: { fullscreen: boolean }) => void) 在全屏播放与非全屏播放状态之间切换时触发该事件。
fullscreen:返回值为true表示进入全屏播放状态,为false则表示非全屏播放。

onTouch上滑/下拉切换视频

通过手指按压时,记录Y的坐标,移动过程中,如果移动大于50,则进行上一个视频或者下一个视频的播放。


private onTouch = ((event) => {
  switch (event.type) {
    case TouchType.Down: // 手指按下
      // 记录按下的y坐标
      this.lastMoveY = event.touches[0].y
      break;
    case TouchType.Up: // 手指按下
      this.offsetY = 0
      this.isDone = false
      break;
    case TouchType.Move: // 手指移动
      const offsetY = (event.touches[0].y - this.lastMoveY) * 3;
      let isDownPull = offsetY < -80
      let isUpPull = offsetY > 80
      this.lastMoveY = event.touches[0].y
      if(isUpPull || isDownPull) {
        this.offsetY = offsetY
        this.isDone = true
      }

      console.log('=====offsetY======', this.offsetY, isDownPull, isUpPull)

      if (isDownPull && this.isDone) {
        this.playNext()
      }
      if (isUpPull && this.isDone) {
        this.playNext()
      }
      break;
  }
})

List实现滚动加载

1、由于视频加载会比较慢,因此List中仅展示一个视频的图片,点击播放按钮即可播放;

2、通过onScrollIndex监听滚动事件,如果当前数据和滚动的index小于3,则进行数据下一页的请求;


List({ scroller: this.scroller, space: 12 }) {
  ForEach(this.listData, (item: ResultType, index: number) => {
    ListItem() {
      Stack({ alignContent: Alignment.TopStart }) {
        Row() {
          Image(item.userPic).width(46).height(46).borderRadius(12).margin({ right: 12 }).padding(6)
          Text(item.title || '标题').fontColor(Color.White).width('80%')
        }
        .width('100%')
        .backgroundColor('#000000')
        .opacity(0.6)
        .alignItems(VerticalAlign.Center)
        .zIndex(9)

        Image(item.coverUrl)
          .width('100%')
          .height(320)
          .alt(this.imageDefault)

        Row() {
          Image($rawfile('play.png')).width(60).height(60)
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(VerticalAlign.Center)
        .opacity(0.8)
        .zIndex(100)
        .onClick(() => {
          this.currentPlayIndex = index;
          this.coverUrl = item.coverUrl;
          this.playUrl = item.playUrl;
          this.videoController.start()
        })
      }
      .width('100%')
      .height(320)
    }
  })
}
.divider({ strokeWidth: 1, color: 'rgb(247,247,247)', startMargin: 60, endMargin: 0 })
.onScrollIndex((start, end) => {
  console.log('============>', start, end)
  if(this.listData.length - end < 3) {
    this.page = this.page++
    this.httpRequest()
  }
})

完整代码


import httpManager, { RequestMethod } from '../../utils/requestHttp';
import measure from '@ohos.measure'
import router from '@ohos.router';

type ResultType = {
  id: number;
  title: string;
  userName: string;
  userPic: string;
  coverUrl: string;
  playUrl: string;
  duration: string;
}

interface resultBean {
  code: number,
  message: string,
  result: {
    total: number,
    list: Array<ResultType>
  },
}

@Entry
@Component
export struct VideoPlay {
  scroller: Scroller = new Scroller()
  private videoController: VideoController = new VideoController()
  @State total: number = 0
  @State listData: Array<ResultType> = []
  private url: string = "https://api.apiopen.top/api/getHaoKanVideo?size=10";
  private page: number = 0

  private httpRequest() {
    httpManager.getInstance()
    .request({
      method: RequestMethod.GET,
      url: `${this.url}&page=${this.page}` //公开的API
    })
      .then((res: resultBean) => {
        this.listData = [...this.listData, ...res.result.list];
        this.total = res.result.total;
        this.duration = 0;
        this.rotateAngle = 0;
      })
      .catch((err) => {
        console.error(JSON.stringify(err));
      });
  }

  aboutToAppear() {
    this.httpRequest()
  }

  @State currentPlayIndex: number = 0
  @State playUrl: string = ''
  @State coverUrl: string = ''
  @State imageDefault: any = $rawfile('noData.svg')

  @State offsetY: number = 0
  private lastMoveY: number = 0

  playNext() {
    const currentItem = this.listData[this.currentPlayIndex + 1]
    this.currentPlayIndex = this.currentPlayIndex + 1;
    this.coverUrl = currentItem?.coverUrl;
    this.playUrl = currentItem?.playUrl;
    this.videoController.start()
    this.scroller.scrollToIndex(this.currentPlayIndex - 1)

    if(this.listData.length - this.currentPlayIndex < 3) {
      this.page = this.page++
      this.httpRequest()
    }
  }

  playPre() {
    const currentItem = this.listData[this.currentPlayIndex - 1]
    this.currentPlayIndex = this.currentPlayIndex +- 1;
    this.coverUrl = currentItem?.coverUrl;
    this.playUrl = currentItem?.playUrl;
    this.videoController.start()
    this.scroller.scrollToIndex(this.currentPlayIndex - 2)
  }

  private title: string = 'Harmony短视频';
  @State screnWidth: number = 0;
  @State screnHeight: number = 0;
  @State textWidth : number = measure.measureText({
    //要计算的文本内容,必填
    textContent: this.title,
  })
  @State rotateAngle: number = 0;
  @State duration: number = 0;

  private isDone: boolean = false

  @State isPlay: boolean = true

  build() {
    Stack({ alignContent: Alignment.TopEnd }) {
      Row() {
        Stack({ alignContent: Alignment.TopStart }) {
          Button() {
            Image($r('app.media.ic_public_arrow_left')).width(28).height(28).margin({ left: 6, top: 3, bottom: 3 })
          }.margin({ left: 12 }).backgroundColor(Color.Transparent)
          .onClick(() => {
            router.back()
          })
          Text(this.title).fontColor(Color.White).fontSize(18).margin({ top: 6 }).padding({ left: (this.screnWidth - this.textWidth / 3) / 2 })

          Image($r('app.media.ic_public_refresh')).width(18).height(18)
            .margin({ left: this.screnWidth - 42, top: 8 })
            .rotate({ angle: this.rotateAngle })
            .animation({
              duration: this.duration,
              curve: Curve.EaseOut,
              iterations: 1,
              playMode: PlayMode.Normal
            })
            .onClick(() => {
              this.duration = 1200;
              this.rotateAngle = 360;
              this.page = 0;
              this.listData = [];
              this.httpRequest();
            })
        }
      }
      .width('100%')
      .height(60)
      .backgroundColor(Color.Black)
      .alignItems(VerticalAlign.Center)

      if(this.playUrl) {
        Column() {
          Text('')
        }
        .backgroundColor(Color.Black)
        .zIndex(997)
        .width('100%')
        .height('100%')
        if(!this.isPlay) {
          Image($r('app.media.pause')).width(46).height(46)
            .margin({
              right: (this.screnWidth - 32) / 2,
              top: (this.screnHeight - 32) / 2
            })
            .zIndex(1000)
            .onClick(() => {
              this.isPlay = true
              this.videoController.start()
            })
        }

        Image($rawfile('close.png')).width(32).height(32).margin({
          top: 24,
          right: 24
        })
          .zIndex(999)
          .onClick(() => {
            this.videoController.stop()
            this.playUrl = ''
          })
        Video({
          src: this.playUrl,
          previewUri: this.coverUrl,
          controller: this.videoController
        })
          .zIndex(998)
          .width('100%')
          .height('100%')
          .borderRadius(3)
          .controls(false)
          .autoPlay(true)
          .offset({ x: 0, y: `${this.offsetY}px` })
          .onFinish(() => {
            this.playNext()
          })
          .onClick(() => {
            this.isPlay = false
            this.videoController.stop()
          })
          .onTouch((event) => {
            switch (event.type) {
              case TouchType.Down: // 手指按下
                // 记录按下的y坐标
                this.lastMoveY = event.touches[0].y
                break;
              case TouchType.Up: // 手指按下
                this.offsetY = 0
                this.isDone = false
                break;
              case TouchType.Move: // 手指移动
                const offsetY = (event.touches[0].y - this.lastMoveY) * 3;
                let isDownPull = offsetY < -80
                let isUpPull = offsetY > 80
                this.lastMoveY = event.touches[0].y
                if(isUpPull || isDownPull) {
                  this.offsetY = offsetY
                  this.isDone = true
                }

                console.log('=====offsetY======', this.offsetY, isDownPull, isUpPull)

                if (isDownPull && this.isDone) {
                  this.playNext()
                }
                if (isUpPull && this.isDone) {
                  this.playNext()
                }
                break;
            }
          })
      }
      List({ scroller: this.scroller, space: 12 }) {
        ForEach(this.listData, (item: ResultType, index: number) => {
          ListItem() {
            Stack({ alignContent: Alignment.TopStart }) {
              Row() {
                Image(item.userPic).width(46).height(46).borderRadius(12).margin({ right: 12 }).padding(6)
                Text(item.title || '标题').fontColor(Color.White).width('80%')
              }
              .width('100%')
              .backgroundColor('#000000')
              .opacity(0.6)
              .alignItems(VerticalAlign.Center)
              .zIndex(9)

              Image(item.coverUrl)
                .width('100%')
                .height(320)
                .alt(this.imageDefault)

              Row() {
                Image($rawfile('play.png')).width(60).height(60)
              }
              .width('100%')
              .height('100%')
              .justifyContent(FlexAlign.Center)
              .alignItems(VerticalAlign.Center)
              .opacity(0.8)
              .zIndex(100)
              .onClick(() => {
                this.currentPlayIndex = index;
                this.coverUrl = item.coverUrl;
                this.playUrl = item.playUrl;
                this.videoController.start()
              })
            }
            .width('100%')
            .height(320)
          }
          .padding({
            left: 6,
            right: 6,
            bottom: 6
          })
        })
      }
      .width('100%')
      .margin(6)
      .position({ y: 66 })
      .divider({ strokeWidth: 1, color: 'rgb(247,247,247)', startMargin: 60, endMargin: 0 })
      .onScrollIndex((start, end) => {
        console.log('============>', start, end)
        if(this.listData.length - end < 3) {
          this.page = this.page++
          this.httpRequest()
        }
      })
    }
    .onAreaChange((_oldValue: Area, newValue: Area) => {
      this.screnWidth = newValue.width as number;
      this.screnHeight = newValue.height as number;
    })
  }
}

为了能让大家更好的学习鸿蒙 (OpenHarmony) 开发技术,这边特意整理了《鸿蒙 (OpenHarmony)开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙 (OpenHarmony)开发学习手册》

入门必看:https://qr21.cn/FV7h05

  1. 应用开发导读(ArkTS)
  2. ……

HarmonyOS 实战开发案例-仿抖音短视频应用,鸿蒙,Harmony OS,OpenHarmony,harmonyos,音视频,移动开发,IT互联网,实战开发,华为

HarmonyOS 概念:https://qr21.cn/FV7h05

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全

HarmonyOS 实战开发案例-仿抖音短视频应用,鸿蒙,Harmony OS,OpenHarmony,harmonyos,音视频,移动开发,IT互联网,实战开发,华为

如何快速入门?:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. 构建第一个JS应用
  4. ……

HarmonyOS 实战开发案例-仿抖音短视频应用,鸿蒙,Harmony OS,OpenHarmony,harmonyos,音视频,移动开发,IT互联网,实战开发,华为

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

HarmonyOS 实战开发案例-仿抖音短视频应用,鸿蒙,Harmony OS,OpenHarmony,harmonyos,音视频,移动开发,IT互联网,实战开发,华为

基于ArkTS 开发:https://qr21.cn/FV7h05

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

HarmonyOS 实战开发案例-仿抖音短视频应用,鸿蒙,Harmony OS,OpenHarmony,harmonyos,音视频,移动开发,IT互联网,实战开发,华为文章来源地址https://www.toymoban.com/news/detail-836257.html

到了这里,关于HarmonyOS 实战开发案例-仿抖音短视频应用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 仿抖音短视频小程序APP组件(超高性能)自动预加载-uniapp

    注:组件使用 vue3+typescript 开发 全局仅渲染 3 个 swiper-item 实测,不管加载多少数据也能丝滑滚动 适用于 vue3,vue2 请自行修 自动预加载视频 首次渲染优化 安装 nodejs: https://nodejs.org/en/ 安装依赖: npm i 运行项目: npm run dev:mp-weixin 构建项目资源: npm run build:mp-weixin 打开小程序开发

    2024年02月09日
    浏览(16)
  • uniapp 开发之仿抖音,上下滑动切换视频、点击小爱心效果

    效果图:   功能描述: 上下滑动视频,双击暂停,然后第一个视频再往上滑显示”已经滑到顶了“ 开始代码: 首先视频接口使用的公开的视频测试接口 开放API-2.0  官网展示                      Swagger UI  接口文档 一开始编写如下:  注解: autoplay=\\\"true\\\" :设置视频在

    2024年02月09日
    浏览(32)
  • 抖音短视频seo源码矩阵系统开发

    抖音SEO源码矩阵系统开发是一项专为抖音平台设计的SEO优化系统,能够帮助用户提升抖音视频的搜索排名和曝光度。为了确保系统运行正常,需要安装FFmpeg和FFprobe工具。FFmpeg是一个用于处理多媒体数据的开源工具集,而FFprobe则是FFmpeg的一部分,用于分析多媒体文件的信息。

    2024年02月14日
    浏览(35)
  • 【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)

    需要全部代码请点赞关注收藏后评论区留言私信~~~ 与传统的影视行业相比,诞生于移动互联网时代的短视频是个全新行业,它制作方便又容易传播,一出现就成为大街小巷的时髦潮流。 各行各业的人们均可通过短视频展示自己,短小精悍的视频片段原来能够容纳如此丰富的

    2024年02月03日
    浏览(16)
  • 抖音短视频矩阵系统源码开发搭建技术开源分享

    抖音短视频矩阵系统源码开发采用模块化设计,包括账号分析、营销活动、数据监控、自动化管理等功能。通过综合分析账号数据,快速发现账号的优势和不足,并提供全面的营销方案,以提高账号曝光率和粉丝数量。同时,系统还支持多账号管理和自动化操作,有效降低账

    2024年02月19日
    浏览(21)
  • 抖音短视频账号矩阵seo分发系统--开发源代

             短视频获客系统开发原型支持短视频智能批量剪辑、短视频多账号管理定时发布,短视频排名查询及优化,智能客服私信回复等,那么短视频seo系统开发时需要开发哪些功能呢?今天我就跟大家分享一下我们的开发思路。 目前我们是开发了手机版小程序端和网页版

    2024年02月10日
    浏览(35)
  • 抖音短视频矩阵管理系统源码开发部署(开源定制)

      短视频矩阵管理系统是专门为企业号商家、普通号商家提供帐号运营从流量 到转化成交的一站式服务方案,具体包含:点赞关注评论主动私信 ,评论区回复,自动潜客户挖掘,矩阵号营销,自动化营销,粉丝 管理等功能,可以帮助企业或商家快速批量制作高质量短视频,

    2024年02月13日
    浏览(18)
  • 抖音短视频矩阵系统源码:技术开发与实践

    一. 短视频账号矩阵管理系统囊括的技术 1. 开发必备的开发文档说明: 二. 技术文档分享: 1.底层框架 系统架构: 2.数据库接口设计 1.1系统架构: 抖音SEO排名系统主要由以下几个模块组成: 1. 数据采集模块:负责采集抖音上的相关数据,包括视频、用户、话题等。 2. 数据

    2024年02月11日
    浏览(26)
  • 抖音短视频seo矩阵系统源码开发部署技术分享

    抖音短视频的SEO矩阵系统是一个非常重要的部分,它可以帮助视频更好地被搜索引擎识别和推荐。以下是一些关于开发和部署抖音短视频SEO矩阵系统的技术分享: 研究:在开发抖音短视频SEO矩阵系统之前,需要进行研究。这包括了解用户搜索的,以及了解

    2024年02月15日
    浏览(24)
  • 抖音短视频SEO矩阵系统源码开发及开发者思路分享......

    抖音矩阵号/抖音短视频SEO矩阵系统源码开发及开发者思路分享:          短视频获客系统支持短视频智能剪辑、短视频定时发布,短视频排名查询及优化,智能客服等,那么短视频seo系统开发时需要开发哪些功能呢?今天我就跟大家分享一下我们的开发思路。 首先,目前公

    2024年02月11日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包