华为HarmonyOS打造开放、合规的广告生态 - 贴片广告

场景介绍

贴片广告是一种在视频播放前、视频播放中或视频播放结束后插入的视频或图片广告。

接口说明

接口名

描述

loadAd(adParam: AdRequestParams, adOptions: AdOptions, listener: AdLoadListener): void

请求单广告位广告,通过AdRequestParams、AdOptions进行广告请求参数设置,通过AdLoadListener监听广告请求回调。

AdComponent(ads: advertising.Advertisement[], displayOptions: advertising.AdDisplayOptions, interactionListener: advertising.AdInteractionListener, @BuilderParam adRenderer?: () => void): void

展示广告,通过AdDisplayOptions进行广告展示参数设置,通过AdInteractionListener监听广告状态回调。

开发步骤

  1. 获取OAID。

    如果想要为用户更精准的推送广告,可以在请求参数AdRequestParams中添加oaid属性。

    如何获取OAID参见获取OAID信息。

    说明

    使用以下示例中提供的测试广告位必须先获取OAID信息。

  2. 请求单广告位广告。

    需要创建一个AdLoader对象,通过AdLoader的loadAd方法请求广告,最后通过AdLoadListener来监听广告的加载状态。

    在请求贴片广告时,需要在AdOptions中设置两个参数:totalDuration和placementAdCountDownDesc。

    请求广告关键参数如下所示:

    请求广告参数名

    类型

    必填

    说明

    adType

    number

    请求广告类型,贴片广告类型为60。

    adId

    string

    广告位ID。

    • 如果仅调测广告,可使用测试广告位ID:testy3cglm3pj0。
    • 如果要接入正式广告,则需要申请正式的广告位ID。可在应用发布前进入流量变现官网,点击“开始变现”,登录鲸鸿动能媒体服务平台进行申请,具体操作详情请参见展示位创建。

    oaid

    string

    开放匿名设备标识符,用于精准推送广告。不填无法获取到个性化广告。

    示例代码如下所示:
     
    1. import { advertising, identifier } from '@kit.AdsKit';
    2. import { common } from '@kit.AbilityKit';
    3. import { hilog } from '@kit.PerformanceAnalysisKit';
    4. import { BusinessError } from '@kit.BasicServicesKit';
    5. import { router } from '@kit.ArkUI';
    6. @Entry
    7. @Component
    8. struct Index {
    9. private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
    10. // 获取到的OAID
    11. private oaid: string = '';
    12. aboutToAppear() {
    13. try {
    14. // 使用Promise回调方式获取OAID
    15. identifier.getOAID().then((data: string) => {
    16. this.oaid = data;
    17. hilog.info(0x0000, 'testTag', '%{public}s', 'Succeeded in getting adsIdentifierInfo by promise');
    18. }).catch((error: BusinessError) => {
    19. hilog.error(0x0000, 'testTag', '%{public}s',
    20. `Failed to get adsIdentifierInfo, code: ${error.code}, message: ${error.message}`);
    21. })
    22. } catch (error) {
    23. hilog.error(0x0000, 'testTag', '%{public}s', `Catch err, code: ${error.code}, message: ${error.message}`);
    24. }
    25. }
    26. build() {
    27. Row() {
    28. Button('加载广告', { type: ButtonType.Normal, stateEffect: true })
    29. .onClick(() => {
    30. // 调用加载广告方法
    31. requestAd(this.context, this.oaid);
    32. })
    33. .borderRadius(8)
    34. .backgroundColor(0x317aff)
    35. .width(90)
    36. .height(40)
    37. }
    38. .height('100%')
    39. }
    40. }
    41. /**
    42. * 加载广告
    43. *
    44. * @param context 上下文环境
    45. * @param oaid OAID信息
    46. */
    47. function requestAd(context: common.Context, oaid: string): void {
    48. const adRequestParam: advertising.AdRequestParams = {
    49. // 广告类型
    50. adType: 60,
    51. // 'testy3cglm3pj0'为测试专用的广告位ID,App正式发布时需要改为正式的广告位ID
    52. adId: 'testy3cglm3pj0',
    53. // 在AdRequestParams中添加oaid参数
    54. oaid: oaid,
    55. // 用于区分普通请求和预加载请求,默认值false代表普通请求,true代表预加载请求
    56. isPreload: false
    57. };
    58. const adOptions: advertising.AdOptions = {
    59. // 在AdOptions中添加totalDuration参数,用于设置贴片广告展示时长(贴片广告必填)
    60. totalDuration: 30,
    61. // 在AdOptions中添加placementAdCountDownDesc参数,设置贴片广告倒计时文案(可选,填写了则展示文案,不填写则只展示倒计时)
    62. placementAdCountDownDesc: encodeURI('VIP免广告'),
    63. // 是否允许流量下载 0不允许 1允许,不设置以广告主设置为准
    64. allowMobileTraffic: 0,
    65. // 是否希望根据 COPPA 的规定将您的内容视为面向儿童的内容: -1默认值,不确定 0不希望 1希望
    66. tagForChildProtection: -1,
    67. // 是否希望按适合未达到法定承诺年龄的欧洲经济区 (EEA) 用户的方式处理该广告请求: -1默认值,不确定 0不希望 1希望
    68. tagForUnderAgeOfPromise: -1,
    69. // 设置广告内容分级上限: W: 3+,所有受众 PI: 7+,家长指导 J:12+,青少年 A: 16+/18+,成人受众
    70. adContentClassification: 'A'
    71. };
    72. // 广告请求回调监听
    73. const adLoaderListener: advertising.AdLoadListener = {
    74. // 广告请求失败回调
    75. onAdLoadFailure: (errorCode: number, errorMsg: string) => {
    76. hilog.error(0x0000, 'testTag', '%{public}s',
    77. `Failed to request single ad, errorCode is: ${errorCode}, errorMsg is: ${errorMsg}`);
    78. },
    79. // 广告请求成功回调
    80. onAdLoadSuccess: (ads: Array<advertising.Advertisement>) => {
    81. hilog.info(0x0000, 'testTag', '%{public}s', 'Succeeded in requesting single ad!');
    82. // 保存请求到的广告内容用于展示
    83. const returnAds = ads;
    84. // 路由到广告展示页面
    85. routePage('pages/PlacementAdPage', returnAds);
    86. }
    87. };
    88. // 创建AdLoader广告对象
    89. const load: advertising.AdLoader = new advertising.AdLoader(context);
    90. // 调用广告请求接口
    91. hilog.info(0x0000, 'testTag', '%{public}s', 'Request single ad!');
    92. load.loadAd(adRequestParam, adOptions, adLoaderListener);
    93. }
    94. /**
    95. * 路由跳转
    96. *
    97. * @param pageUri 要路由到的页面
    98. */
    99. async function routePage(pageUri: string, ads: Array<advertising.Advertisement | null>) {
    100. let options: router.RouterOptions = {
    101. url: pageUri,
    102. params: {
    103. ads: ads
    104. }
    105. }
    106. try {
    107. hilog.info(0x0000, 'testTag', '%{public}s', `RoutePage: ${pageUri}`);
    108. router.pushUrl(options);
    109. } catch (error) {
    110. hilog.error(0x0000, 'testTag', '%{public}s',
    111. `Failed to routePage callback, code: ${error.code}, msg: ${error.message}`);
    112. }
    113. }

  3. 展示广告。

    在您的页面中使用AdComponent组件展示贴片广告,由媒体判断流量场景下,可以自动播放则展示广告,反之则不展示。以前贴广告为例,前贴广告播放完成后进入正片播放。您需要在entry/src/main/resources/base/profile/main_pages.json文件中添加页面,如下图所示。

    您需要在media和rawfile目录下分别指定正片未播放时的预览图video_preview.PNG和对应的正片文件videoTest.mp4,如下图所示。

    示例代码如下所示:
     
    1. import { router, window } from '@kit.ArkUI';
    2. import { BusinessError } from '@kit.BasicServicesKit';
    3. import { advertising, AdComponent } from '@kit.AdsKit';
    4. import { hilog } from '@kit.PerformanceAnalysisKit';
    5. @Entry
    6. @Component
    7. export struct PlacementAdPage {
    8. // 是否竖屏
    9. private portrait: boolean = true;
    10. // 请求到的广告内容
    11. private ads: Array<advertising.Advertisement> = [];
    12. // 广告展示参数
    13. private adDisplayOptions: advertising.AdDisplayOptions = {
    14. // 是否静音,默认不静音
    15. mute: false
    16. }
    17. // 广告参数
    18. private adOptions: advertising.AdOptions = {
    19. // 设置贴片广告展示时长(贴片广告必填)
    20. totalDuration: 30,
    21. // 设置贴片广告倒计时文案,文案需要使用encodeURI编码(可选,填写了则展示文案,不填写则只展示倒计时)
    22. placementAdCountDownDesc: encodeURI('VIP免广告'),
    23. // 是否希望根据 COPPA 的规定将您的内容视为面向儿童的内容: -1默认值,不确定 0不希望 1希望
    24. tagForChildProtection: -1,
    25. // 是否希望按适合未达到法定承诺年龄的欧洲经济区 (EEA) 用户的方式处理该广告请求: -1默认值,不确定 0不希望 1希望
    26. tagForUnderAgeOfPromise: -1,
    27. // 设置广告内容分级上限: W: 3+,所有受众 PI: 7+,家长指导 J:12+,青少年 A: 16+/18+,成人受众
    28. adContentClassification: 'A'
    29. }
    30. // 已经播放的贴片广告数量
    31. private playedAdSize: number = 0;
    32. // 是否播放正片
    33. @State isPlayVideo: boolean = false;
    34. // 视频播放控制器
    35. private controller: VideoController = new VideoController();
    36. // 指定视频未播放时的预览图片路径
    37. private previewUris: Resource = $r('app.media.video_preview');
    38. // 指定视频播放源的路径,这里取本地视频资源
    39. private innerResource: Resource = $rawfile('videoTest.mp4');
    40. // 用于渲染右上角倒计时
    41. private countDownTxtPlaceholder: string = '%d | %s';
    42. @State countDownTxt: string = '';
    43. aboutToAppear() {
    44. const params: Record<string, Object> = router.getParams() as Record<string, Object>;
    45. if (params && params.ads as Array<advertising.Advertisement>) {
    46. this.ads = params.ads as Array<advertising.Advertisement>;
    47. this.adOptions = params.adOptions as advertising.AdOptions;
    48. this.initData();
    49. }
    50. }
    51. build() {
    52. Stack({ alignContent: Alignment.TopEnd }) {
    53. // AdComponent组件用于展示非全屏广告
    54. AdComponent({
    55. ads: this.ads, displayOptions: this.adDisplayOptions,
    56. interactionListener: {
    57. // 广告状态变化回调
    58. onStatusChanged: (status: string, ad: advertising.Advertisement, data: string) => {
    59. switch (status) {
    60. case 'onPortrait':
    61. hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onPortrait');
    62. // 设置屏幕方向为竖屏或返回上一页
    63. this.setWindowPortrait();
    64. break;
    65. case 'onLandscape':
    66. hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onLandscape');
    67. // 设置屏幕方向为横屏
    68. this.setWindowLandscape();
    69. break;
    70. case 'onMediaProgress':
    71. hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onMediaProgress');
    72. break;
    73. case 'onMediaStart':
    74. hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onMediaStart');
    75. break;
    76. case 'onMediaPause':
    77. hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onMediaPause');
    78. break;
    79. case 'onMediaStop':
    80. hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onMediaStop');
    81. break;
    82. case 'onMediaComplete':
    83. hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onMediaComplete');
    84. // 所有广告都播放完毕后,开始播放正片
    85. this.playedAdSize++;
    86. if (this.playedAdSize === this.ads.length) {
    87. this.isPlayVideo = true;
    88. }
    89. break;
    90. case 'onMediaError':
    91. hilog.error(0x0000, 'testTag', '%{public}s', 'Status is onMediaError');
    92. break;
    93. case 'onMediaCountdown':
    94. try {
    95. hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onMediaCountdown');
    96. const parseData: Record<string, number> = JSON.parse(JSON.stringify(data));
    97. this.updateCountDownTxt(parseData.countdownTime);
    98. } catch (e) {
    99. hilog.error(0x0000, 'testTag', '%{public}s',
    100. `Failed to parse data, code: ${e.code}, msg: ${e.message}`);
    101. }
    102. break;
    103. }
    104. }
    105. }
    106. })
    107. .visibility(!this.isPlayVideo ? Visibility.Visible : Visibility.None)
    108. .width('100%')
    109. .height('100%')
    110. Row() {
    111. if (this.countDownTxt) {
    112. Text(this.countDownTxt.split('').join('\u200B'))
    113. .fontSize(12)
    114. .textAlign(TextAlign.Center)
    115. .maxLines(1)
    116. .fontColor(Color.White)
    117. .lineHeight(12)
    118. .textOverflow({ overflow: TextOverflow.Ellipsis })
    119. .maxLines(1)
    120. .backgroundColor('#66000000')
    121. .border({ radius: 25 })
    122. .padding({
    123. left: 8,
    124. right: 8,
    125. top: 6,
    126. bottom: 6
    127. })
    128. .margin({ right: 16, top: 16 })
    129. .height(24)
    130. .constraintSize({ minWidth: 60, maxWidth: 100 })
    131. .onClick((event: ClickEvent) => {
    132. hilog.info(0x0000, 'testTag', '%{public}s', 'OnVipClicked, do something...');
    133. })
    134. }
    135. }
    136. .alignItems(VerticalAlign.Top)
    137. .justifyContent(FlexAlign.End)
    138. Video({
    139. src: this.innerResource,
    140. previewUri: this.previewUris,
    141. controller: this.controller
    142. })
    143. .visibility(this.isPlayVideo ? Visibility.Visible : Visibility.None)
    144. .autoPlay(this.isPlayVideo ? true : false)
    145. .controls(false)
    146. .width('100%')
    147. .height('100%')
    148. }.width('100%').height('100%')
    149. }
    150. /**
    151. * 设置竖屏或返回上一页
    152. */
    153. private setWindowPortrait() {
    154. hilog.info(0x0000, 'testTag', '%{public}s', `Set WindowPortrait, portrait: ${this.portrait}`);
    155. if (!this.portrait) {
    156. window.getLastWindow(getContext(this), (err: BusinessError, win) => {
    157. win.setPreferredOrientation(window.Orientation.PORTRAIT)
    158. });
    159. this.portrait = true;
    160. } else {
    161. router.back();
    162. }
    163. }
    164. /**
    165. * 设置横屏(正向)
    166. */
    167. private setWindowLandscape() {
    168. hilog.info(0x0000, 'testTag', '%{public}s', `Set WindowLandscape, portrait: ${this.portrait}`);
    169. if (this.portrait) {
    170. window.getLastWindow(getContext(this), (err: BusinessError, win) => {
    171. win.setPreferredOrientation(window.Orientation.LANDSCAPE)
    172. });
    173. this.portrait = false;
    174. }
    175. }
    176. private initData() {
    177. this.initCountDownText();
    178. }
    179. private initCountDownText() {
    180. const decodeText = this.decodeString(this.adOptions?.placementAdCountDownDesc as string);
    181. if (!this.isBlank(decodeText)) {
    182. this.countDownTxtPlaceholder = this.countDownTxtPlaceholder.replace('%s', decodeText);
    183. } else {
    184. this.countDownTxtPlaceholder = '%d';
    185. }
    186. }
    187. private updateCountDownTxt(leftTime: number) {
    188. hilog.info(0x0000, 'testTag', '%{public}s', `Show LeftTime: ${leftTime}`);
    189. this.countDownTxt = this.countDownTxtPlaceholder.replace('%d', leftTime + '');
    190. }
    191. private decodeString(str: string): string {
    192. if (!str) {
    193. return str;
    194. }
    195. let decodeUrl = str;
    196. try {
    197. decodeUrl = decodeURIComponent(str.replace(/\+/g, '%20'));
    198. } catch (e) {
    199. hilog.error(0x0000, 'testTag', '%{public}s', `Failed to decodeURIComponent, code:${e.code}, msg: ${e.message}`);
    200. }
    201. return decodeUrl;
    202. }
    203. private isBlank(str: string): boolean {
    204. if (str === null || str === undefined) {
    205. return true;
    206. }
    207. if (typeof str === 'string') {
    208. return str.trim().length === 0;
    209. }
    210. return false;
    211. }
    212. }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/59225.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode137只出现一次的数字|| 及其拓展

简述&#xff1a; 虽然标题是这么描述的&#xff0c;但是我们不是一上来就解这道题&#xff0c;先看一下他的子题和扩展 子题&#xff1a;136. 只出现一次的数字 - 力扣&#xff08;LeetCode&#xff09; 扩展题&#xff1a; 所以我们由易到难&#xff0c;先来看第一道&#x…

【解决办法】无法使用右键“通过VSCode打开文件夹”

个人博客&#xff1a;苏三有春的博客 前言 作者的编程环境为VScode&#xff0c;工作时常使用VScode打开整个工程文件夹。如果先打开VScode再从VScode中选择文件夹打开效率太慢&#xff0c;作者一般使用的方式是右键文件夹&#xff0c;直接选择"通过code打开文件夹"…

图文深入介绍Oracle DB link(二)

1. 引言&#xff1a; 本文承接上篇继续深入介绍Oracle DB link&#xff0c;主要介绍如何配置DB link的连接及确保连接的安全。 2. 创建 DB Link 时配置连接参数 1. 配置连接参数 1. 使用 TNSNAMES.ORA 文件配置连接参数&#xff08;常用方式&#xff09; TNSNAMES.ORA 文件…

数据揭秘:掌握K-means聚类算法的精髓与实践

数据揭秘&#xff1a;掌握K-means聚类算法的精髓与实践 在机器学习领域&#xff0c;聚类是一种探索性的数据挖掘技术&#xff0c;用于将数据集中的样本划分成若干个簇&#xff0c;使得同一簇内的样本相似度高&#xff0c;而不同簇之间的样本相似度低。本文将深入探讨聚类分析的…

ADNI蛋白质数据集下载

&#xff08;我发现这个网站最近又更新了界面&#xff0c;现在变得很好看很简洁&#xff0c;但是有一些入口变了&#xff09; 1.官网链接 https://ida.loni.usc.edu/home/projectPage.jsp?projectADNI 2.登录 选择ADNI&#xff08;其实PPMI数据也是这样下的&#xff09;&a…

【数据分享】2024年我国省市县三级的生活服务设施数量(46类设施/Excel/Shp格式)

人才市场、售票处、旅行社等生活服务设施的配置情况是一个城市公共基础设施完善程度的重要体现&#xff0c;一个城市生活服务设施种类越丰富&#xff0c;数量越多&#xff0c;通常能表示这个城市的公共服务水平越高&#xff01; 本次我们为大家带来的是我国各省份、各地级市、…

彻底解决idea不识别java项目

需求背景 下载了一个java swing的项目,通过idea导入后,项目无法识别。打开java文件,也不会报错,也不编译。 无法识别效果图 可以看到左侧的菜单,项目是没有被识别。 打开java文件,可以看到没有识别,java的图标也没有出现。 解决方法 1、打开Project Structure 2、修改…

【力扣 + 牛客 | SQL题 | 每日5题】牛客SQL热题216,217,223

也在牛客力扣写了一百来题了&#xff0c;个人感觉力扣的SQL题要比牛客的高三档的难度。&#xff08;普遍来说&#xff09; 1. 牛客SQL热题216&#xff1a;统计各个部门的工资记录数 1.1 题目&#xff1a; 描述 有一个部门表departments简况如下: dept_nodept_named001Marke…

跨平台开发对于不同平台调用接口的抽象层设计

我已经受够无处不在的分支语句了&#xff01; 长痛不如短痛&#xff0c;趁现在的底层交互代码还没有这么多&#xff0c;赶紧重构吧。毕竟不是 demo 时期了&#xff0c;要讲究一些了 设计思路 刚开始的想法是整一个抽象类 AbstractAPIs 声明所有待实现的函数&#xff0c;然后让…

HTMLCSS:打造酷炫下载安装模拟按钮

效果演示 这段代码通过HTML和CSS创建了一个具有交互效果的下载按钮&#xff0c;当复选框被选中时&#xff0c;会触发一系列动画和样式变化&#xff0c;模拟了一个下载和安装的过程&#xff0c;包括圆形的动画、文本的显示和隐藏等。 HTML <div class"container&quo…

Multi Agents协作机制设计及实践

01 多智能体协作机制的背景概述 在前述博客中&#xff0c;我们利用LangChain、AutoGen等开发框架构建了一个数据多智能体的平台&#xff0c;并使用了LangChain的Multi-Agents框架。然而&#xff0c;在实施过程中&#xff0c;我们发现现有的框架存在一些局限性&#xff0c;这些…

ML2001-1 机器学习/深度学习 Introduction of Machine / Deep Learning

图片说明来自李宏毅老师视频的学习笔记&#xff0c;如有侵权&#xff0c;请通知下架 影片参考 【李宏毅】3.第一节 - (上) - 机器学习基本概念简介_哔哩哔哩_bilibili 1. 机器学习的概念与任务类型 概念&#xff1a;机器学习近似于寻找函数&#xff0c;用于处理不同类型的任…

90%会展主办方都会用的6款数字化工具

在会展行业&#xff0c;数字化转型已成为提升竞争力的关键。面对日益增长的运营成本和收入增长的瓶颈&#xff0c;主办方需要借助数字化工具来实现效率提升和成本控制。 今天介绍几种常见的数字化工具和应用方式。 一、线上展览平台 构建线上展览平台是会展主办方拓展线上销…

技术速递|GitHub Copilot upgrade assistant for Java 技术预览发布!

作者&#xff1a;Nick Zhu - Senior Program Manager 排版&#xff1a;Alan Wang 随着人工智能和大型语言模型&#xff08;LLMs&#xff09;的不断发展&#xff0c;Agent&#xff08;“智能代理”&#xff09;和智能代理化工作流程正在迅速成为AI领域的下一个前沿。这些自主系统…

Python常用脚本集锦

收集了一些常用Python脚本&#xff0c;作为平时练手使用&#xff0c;也可以作为自己的笔记&#xff0c;用到哪个功能可以自己查询一下即可。 文件和目录管理 复制文件 import shutil# 复制源文件到目标文件 shutil.copy(source.txt, destination.txt)移动文件 import shuti…

redis源码系列--(二)--multi/exec/eval命令执行流程

本文主要记录multi/exec、eval、redis执行lua脚本的源码流程 redis在exec之前&#xff0c;所有queued的命令是没有执行的&#xff0c;&#xff01;&#xff01;&#xff01;在执行时会通过检测client是否被打上CLIENT_DIRTY_CAS标记来判断[watch后,exec时]时间段内是否有key被…

什么是安全威胁情报?

SANS的定义 针对安全威胁、威胁者、利用、恶意软件、漏洞和危害指标、所收集的用于评估的应用数据集。 iSight的定义 iSight&#xff1a;网络威胁情报是关于已收集、分析、分发的&#xff0c;针对攻击者和其动机的目的手段&#xff0c;用于帮助所有安全级别的&#xff0c;和…

JMeter快速造数之数据导入导出

导入数据 输入表格格式如下 创建CSV Data Set Config 在Body Data中调用 { "username": "${email}", "password": "123456", "client_id": "00bb9dbfc67439a5d42e0e19f448c7de310df4c7fcde6feb5bd95c6fac5a5afc"…

渗透测试-快速获取目标中存在的漏洞(小白版)

《Java代码审计》http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247484219&idx1&sn73564e316a4c9794019f15dd6b3ba9f6&chksmc0e47a67f793f371e9f6a4fbc06e7929cb1480b7320fae34c32563307df3a28aca49d1a4addd&scene21#wechat_redirect 《Web安全》h…

[免费]基于Python的Django+Vue3在线考试系统【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的基于Python的DjangoVue3在线考试系统&#xff0c;分享下哈。 项目视频演示 【免费】基于Python的DjangoVue3在线考试系统 Python毕业设计_哔哩哔哩_bilibili 项目介绍 本论文提出并实现了一种基于Python…