黑马程序员HarmonyOS4+NEXT星河版入门到企业级实战教程【18~31】

视频0~17的笔记

页面路由

在这里插入图片描述
实现不同页面之间的跳转和数据传递。
页面栈。上限是32个页面,使用router.clear()可以清空页面栈释放内存。
两种跳转模式: 参数一致,都是三个参数

  • router.pushUrl(),新页面压入页面栈,可以用router.back()返回当前页
  • router.replaceUrl()目标页替换当前页,当前页被销毁,无法返回当前页

登录页基本只访问一次,无需返回。适合用第二种。
首页->搜索页应当使用第一种。
两种页面示例模式:
-Standard 默认,每次跳转都新建一个目标压入栈顶

  • Single 单示例模式,如果目标页已在栈中,则离栈顶最近的URL页面会被移动到栈顶并重新加载

导入HarmonyOS提供的Router模块:

import router from 'ohos.router'
router.pushUrl(
	{ //RouterOptions对象,含有url和params(可选)
		url:'page/ImagePage',
		params:{id:1} //可选
	},
	router.RouterMode.Single,//页面实例模式,枚举类型
	err=>{ //异常回调函数,错误码,100001:内部错误,可能是渲染失败,100002:路由地址错误,100003:栈>32
		if(err){
			console.log('fail')
		}
	}
)
//获取传递来的参数
params:any=router.getParams()
//返回上一页
router.back()
//返回到指定页,并携带参数
router.back(
	{
		url:'pages/Index',
		params:{id:10}
	}
)

想要跳转页面,必须在resource-base-profile-main_pages.json里配置,在新建文件的时候直接新建page,编译器会自动在上面的文件里加入此页面,比较方便。

{
  "src": [
    "pages/Index",
    "pages/ImagePage",
    "pages/loopPractice",
    "pages/stateManagement"
  ]
}

首页

import router from '@ohos.router'
class RouterInfo{
  url:string
  title:string

  constructor(url:string,title:string) {
    this.url=url
    this.title=title
  }
}

@Entry
@Component
struct Index {
  private routers:RouterInfo[]=[
    new RouterInfo('pages/ImagePage','图片查看样例'),
    new RouterInfo('pages/loopPractice','商品列表案例'),
    new RouterInfo('pages/stateManagement','任务列表案例')
    ]
  build() {
    Column(){
      Text('页面列表')
      List(){
        ForEach(this.routers,
          (router,index)=>{
            ListItem(){
              this.RouterItem(router,index+1)
            }
          }
        )
      }
      .layoutWeight(1)
    }
  }
  @Builder
  RouterItem(r:RouterInfo,i:number){
    Row(){
      Text(i+'.')
      Blank()
      Text(r.title)
    }
    .onClick(()=>{
      router.pushUrl(
        {
          url: r.url,
          params: {id:i}
        },
        router.RouterMode.Single,
        err=>{
          if(err){
            console.log(`路由失败,错误码:${err.code} errMsg:${err.message}`)
          }
        }
      )
    })
  }
}

图片展示案例页

Header({title:"图片查看样例"})
        .onClick(()=>{
          //返回前的警告
          router.showAlertBeforeBackPage({
            message:'确定要返回吗?'
          })
          router.back()
        })

效果
在这里插入图片描述

动画

可以提高用户交互时的体验
arkui会帮忙填充每一帧的画面,只需要写初始和结束的状态,以及播放时长等。
x从左到右,y从上到下

属性动画

animation

显式动画

组件转场动画

Stage模型

应用模型

概述

不同模块放到不同module里开发,具有通用的一些东西,把这些抽取出来,放到单独的依赖类型的模块里
在这里插入图片描述

可以new一个module,有empty ability,也有library module,核心是这两大类。
每一个模块都可以独立编译运行,HAP=harmony ability package,HSP=harmony shared package,HAP可以引用HSP包。
entry是项目的入口,是核心部分,作为入口的HAP只有一个,其他HAP是Feature类型的。
Bundle有一个Bundle name,是整个应用的唯一标识。
打包成app,就是安装包了。
Ability翻译成应用组件
UIAbility是主流的,ExtensionAbility是拓展的
好处:降低耦合;选择性安装feature模块,降低空间占用;跨设备

应用配置文件

在这里插入图片描述
AppScope/app.json5

{
  "app": {
    "bundleName": "com.example.hellloworld", //应用的唯一标识
    "vendor": "example",
    "versionCode": 1000000, //数字格式的版本
    "versionName": "1.0.0", //字符串格式的版本
    "icon": "$media:app_icon", //app图标
    "label": "$string:app_name" //应用的描述 并没有直接指定,而是读取AppScope/resource/media下的文件和element/string.json
    //这是在应用列表里展示的信息
  }
}

entry/scr/main/module.json5

{
  "module": {
    "name": "entry", //模块的名字
    "type": "entry", //模块的类型,ability细分为entry和feature;library但是叫做"shared"类型
    "description": "$string:module_desc", //"$string:module_desc"
    "mainElement": "EntryAbility", //当前模块的入口 打开以后会发现继承了UIAbility的东西
    "deviceTypes": [ //设备类型 还有电视,手表等
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true, //安装时是否必须,feature可以按需安装
    "installationFree": false,
    "pages": "$profile:main_pages", //entry/src/main/resources/base/profile/main_pages.json
    "abilities": [ //ability数组,一个模块可以包含多个ability
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ts",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon", //即桌面上的图标和名称,可以和应用列表里的不一样
        "label": "$string:EntryAbility_label", //去改base/element/string.json,可以打开右上角的编辑器,一起改
        "startWindowIcon": "$media:icon", //启动时展示的东西
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "requestPermissions":[
      {
        "name": "ohos.permission.INTERNET"//name必填,请求网络
      }
    ]
  }
}

UIAbility 生命周期

在这里插入图片描述

创建-前台
每一个任务是一个独立的ability
应用可以切换到后台、前台,可以销毁
在这里插入图片描述
在这里插入图片描述
//hilog是一个导入的,可以带上日志级别,domain域(16进制),tag标记(方便日志查询),日志内容%s是占位符,后面是参数,public指可以公开(部署)
在这里插入图片描述
通过testTag过滤查看日志,发现其过程和理论一致
在这里插入图片描述
ability创建->windowStage创建->foreground前台,展示->background后台->windowStageDestroy销毁->Ability 销毁

页面及组件生命周期

在这里插入图片描述

页面被销毁,则组件被销毁
但页面存在,组件不一定存在,也可能被销毁
提供了一些钩子来帮助完成渲染。
页面的展示从入口组件开始,页面生命周期钩子只能在加了entry的组件里使用,不会在普通组件里被触发,另外两个属于组件的生命周期钩子,可以在任意自定义组件中使用。
这些钩子可以在ets里用,
在这里插入图片描述
aboutToAppear()在页面/组件创造时出现,可以做一下数据的初始化和准备
onPageShow() 在页面展示时出现
onBackPress() 页面返回(系统提供的)
onPageHide() 页面隐藏
aboutToDisappear() 页面/组件销毁,关键性数据可以去做保存,或资源释放
组件不存在隐藏,只会被销毁。
用foreach渲染的话,在每次元素变化时,会销毁所有元素,再创造所有元素。如果加了foreach的第三个参数(唯一标识)就不会这样。
页面路由のpush会使得页面hide,而replace会使得页面disappear

UIAbility的启动模式

单实例模式、标准模式(standard多个实例并存,multition旧实例会销毁)、指定模式(按key决定是否创建新实例,用途:文档编辑软件,同一个文档可以保留,不同文档可以多开。api较为复杂)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • entry/EntryAbility.ts 默认是Singleton,要用模拟器来看ability的日志,用onCreate等来验证。回到桌面:Ability onBackground,还存在,再次启动访问,发现:Ability onForeground,从后台切到了前台。
    在这里插入图片描述

改变启动模式,要在module.json5修改,多写一个"launchType":standard,发现其实有四种类型,并不是三种。
在这里插入图片描述

  • 当模式修改为multition时,日志为:
    可见再次创建了ability,这是一个新的ability,任务列表里只存在一个实例,旧的被干掉了
    在这里插入图片描述
  • 当模式修改为standard时,日志和multition类似,但任务列表中存在两个ability
    在这里插入图片描述
  • specified模式
    在这里插入图片描述
    startAbility来唤醒(拉起)
    want(意图)是传入的key,成员包括:divice ID(不传指本设备,意味着可以跨设备),bundlename包名,abilityName实例名,moduleName模块名,parameters参数(UIAbility实例的Key)
    在这里插入图片描述
    取得参数,还可以拼接一些生成一个key
    在json5文件中配置

例子:跨Ability的跳转,不能用router做,得用这个
new一个ability,和entryability是平行的
在这里插入图片描述
此ability一启动,加载的是文档编辑器模块。
在module.json5里修改launchType为specified
在这里插入图片描述
需要导包,
在这里插入图片描述
填写的want参数为:在这里插入图片描述

其他都是本项目、本模块,只是跳转到另一ability。
接下来创建stage,需要自己新建目录:myabilitystage,新建ts文件MyAbilitystage.ts,实现生命周期钩子。
在这里插入图片描述
还需要在module.json5里配置srcEntry,指stage舞台的文件
在这里插入图片描述
这样去就做好了。返回的逻辑和去一致,修改一下abilityName,此时跳转不需要参数,因为它是单实例模式在这里插入图片描述
必须在模拟机上测试,测试结果:
在这里插入图片描述
entry进入了background,文档onCreate,点击返回,document进入后台,entry进入前台。再创建一个文档,同时存在三个ability。
在这里插入图片描述

网络连接

在这里插入图片描述
http:客户端向服务端发起的单向的连接方式
webSocket:客户端和服务端的双向连接
socket:套接字,底层,实现UDP,TCP等通讯协议

http请求数据

底层是UDP,需要三次握手建立连接
在这里插入图片描述
在这里插入图片描述

  1. http的请求需要每次使用的时候创建,不能复用。
  2. 调用request方法,第一个参数是url路径(协议,主机名,端口号,也可以拼接请求参数),第二个参数是请求选项,字段有很多,可以选择。method,查询get,新增post,修改put,删除delete;extraData的string类型一般在get时用,Object复杂类型在提交、修改时使用,是键值对格式,多组用&拼接,也可以不在extraData里,而拼到url后面,Object要装在{}里;header放一些特殊信息,比如用户身份凭证,connectTimeout最长建立连接的时间,默认是60s,单位是毫秒,readTimeout读取超时,同上
  3. 处理响应结果,网络请求会有延迟,往往要通过异步任务处理。凡异步都会返回一个Promise,存放未来会完成的结果,先添加成功和失败的回调函数。
    Promise提供了then成功和catch失败两个方法。返回的是HttpResponse,而非直接的数据。包含:responseCode状态码,200成功,400错误等,需要判断是否成功;header;cookies存放身份凭证;result我们要的响应的数据,有string类型(最常见),json字符串,还有Object数据流等较为少见;resultType响应结果的类型。

上课的例子是用nodejs模拟服务器
npm install安装依赖
在这里插入图片描述
在这里插入图片描述
默认端口是3000,此处可以改
启动服务端npm start
在这里插入图片描述

pageNo=1&pageSize=2,分页查询,第一页,每页两个。查出来了:
在这里插入图片描述
图片的路径是相对服务器的路径
在这里插入图片描述
ShopPages.ets

import { Header } from '../common/components/CommonComponents'
import ShopInfo from '../viewmodel/ShopInfo'
import ShopItem from '../views/ShopItem'
import ShopModel from '../model/ShopModel'

@Entry
@Component
struct ShopPage {
  @State shops: ShopInfo[] = []
  isLoading: boolean = false //判断是否正在加载
  isMore: boolean = true //判断是否还有更多数据

  aboutToAppear() {
    // 加载商品数据
    this.loadShopInfo()
  }

  build() {
    Column({ space: 10 }) {
      Header({ title: '商铺列表' })

      List({ space: 10 }) {
        ForEach(this.shops, shop => {
          ListItem() {
            ShopItem({ shop: shop })
          }
        })
      }
      .width('100%')
      .layoutWeight(1)
      .onReachEnd(() => {  //List刷到底了
        console.log('触底了!')
        if(!this.isLoading && this.isMore){
          this.isLoading = true //因为回弹动画会导致触底两次,为了避免请求两次,增加isLoading的判断
          // 翻页
          ShopModel.pageNo++
          this.loadShopInfo() //实现边划边加载
        }
      })

    }
    .width('100%')
    .height('100%')
    .padding(10)
    .backgroundColor('#e1e2e3')
  }

  loadShopInfo(){
    // 加载数据
    ShopModel.getShopList() //返回的是Promise,这是一个将来的结果,那边日志已经打印过了,这边就不catch了
      .then(shops => {
        // 给图片加上服务器地址前缀
        shops.forEach(s => {
          s.images.forEach((src, i) => { //此处是为了修改端口号时方便因此拼接
            s.images[i] = 'http://localhost:3000' + src
          })
        })

        this.shops = this.shops.concat(shops) //把新数据拼接到到旧数据的后面,实现滑动时请求到新数据
        this.isLoading = false //此时要把isLoading改回false
        if(!shops || shops.length === 0){ //说明没有数据了
          this.isMore = false //没有更多数据了
        }
      })
  }
}

ShopModel.ts 数据模型类

import http from '@ohos.net.http'; //导入http模块
import ShopInfo from '../viewmodel/ShopInfo';

class ShopModel{
  baseURL: string = 'http://localhost:3000'
  pageNo: number = 1

  getShopList(): Promise<ShopInfo[]>{
    return new Promise((resolve, reject) => { //整个大函数的return,拿到这个Promise的人也去then和catch
      // 1.创建http的请求对象
      let httpRequest = http.createHttp() //这一对象不可复用
      // 2.发送请求,异步调用,异步返回,要返回一个将来的结果Promise
      httpRequest.request(
        `${this.baseURL}/shops?pageNo=${this.pageNo}&pageSize=3`,
        {
          method: http.RequestMethod.GET //请求发送代码
        }
      )
        .then(resp => { //加上回调,then指成功
          if(resp.responseCode === 200){ //response是一个http的对象,结果在resp.result里,转换为string类型,json.parse可以把json字符串转成对象
            // 查询成功
            console.log('查询商铺成功!', resp.result)
            resolve(JSON.parse(resp.result.toString()))//成功的通知
          }else{
            console.log('查询商铺信息失败!error:', JSON.stringify(resp))
            reject('查询商铺失败') //失败的通知
          }
        })
        .catch(error => { //catch代表出现异常,记录日志,error是一个对象,要转换成json的字符串
          console.log('查询商铺信息失败!error:', JSON.stringify(error))
          reject('查询商铺失败')
        })
    })
  }
}
const shopModel = new ShopModel();
export default shopModel as ShopModel; //导出给别人使用

正确的日志:

第三方库axios

是最流行的发送请求的第三方库,不在鸿蒙中集成,需要用到ohpm来管理第三方库
ohpm=open harmony package manager

下载安装ohpm

在这里插入图片描述
在这里插入图片描述
按照提示配置了node.js的环境变量,进入命令行运行init.bat
在这里插入图片描述
在这里插入图片描述
表示成功。
此时还需要把ohpm目录配置到环境变量中去,环境变量中加入
在这里插入图片描述
此时可以在命令行的任意位置使用ohpm
在这里插入图片描述

下载安装axios

在这里插入图片描述
所有网络模块都需要配置这个权限。
其他第三方库:https://ohpm.openharmony.cn/#/cn/home 可以看到一些文档
在这里插入图片描述
需要回到开发工具去安装,项目目录下的oh-package.json5是依赖管理文件,下图是正确的
在这里插入图片描述
我的报错了:
在这里插入图片描述
解决方法,powershell安全策略
在这里插入图片描述
再次打开IDE,成功:
在这里插入图片描述
而且dependencies也出现了axios库:
在这里插入图片描述
在项目目录下可以看到oh_modules,里面就是下载的包
在这里插入图片描述

使用axios

在这里插入图片描述
get,put等各种方法,请求支持的参数非常多,params和data非常常见,params是get的参数,data是put等的参数,拿到Promise结果。axios会自动帮我们把json换成对象。
把之前的shopModel进行修改:不需要创建新对象,可见axios非常强大,而且简洁

import ShopInfo from '../viewmodel/ShopInfo';
import axios from '@ohos/axios'

class ShopModel{
  baseURL: string = 'http://localhost:3000'
  pageNo: number = 1

  getShopList(): Promise<ShopInfo[]>{
    return new Promise((resolve, reject) => {
      axios.get(
        `${this.baseURL}/shops`,
        {
          params: {pageNo: this.pageNo, pageSize: 3} //在这里,可以传参,不需要拼到url里
        }
      )
        .then(resp => {
          if(resp.status === 200){
            // 查询成功
            console.log('查询商铺成功!', JSON.stringify(resp.data))
            resolve(resp.data) //不需要像http一样转换
          }else{
            console.log('查询商铺信息失败!error:', JSON.stringify(resp))
            reject('查询商铺失败')
          }
        })
        .catch(error => {
          console.log('查询商铺信息失败!error:', JSON.stringify(error))
          reject('查询商铺失败')
        })
    })
  }
}

const shopModel = new ShopModel();
export default shopModel as ShopModel;

预览一下,和视频一模一样,太Nb了
在这里插入图片描述

数据持久化

之前的数据都在内存里,必须要存起来

用户首选项 preference

在这里插入图片描述

数据结构key-value,支持轻量级(如小说的页面设置,结构简单,数据少)。
首先要创建用户首选项的实例,可以创建多个,每个对应一个持久化文件(不需要关注底层,只需要调用接口)。
在这里插入图片描述

harmonyos提供了dataPreference不需要安装
参数:context是上下文,字符串是首选项的名称(需要唯一)。
读写磁盘上的持久化文件,是异步操作,会返回Promise对象,仍然是调用then,catch
key需要保持唯一,如果已存在,会覆盖。
then为什么要flush?因为只是写到了实例里,还没有刷到磁盘里。
get需要传一个默认值,以防找不到。

例子:
黑马写了一个字体面板,用来设置字体大小,并做用户首选项的持久化。
在这里插入图片描述entry/src/main/ets/common/util/PreferencesUtil.ts工具类
异步写法:
在这里插入图片描述
改造成同步写法:加了异步标记,仍然会被异步包装。下面是同步写法。
在这里插入图片描述
没有写删除

import preferences from '@ohos.data.preferences'; //导入包

class PreferencesUtil{

  prefMap: Map<string, preferences.Preferences> = new Map() //preference名字:实例 --map

  async loadPreference(context, name: string){ //先定义一个方法,用来加载实例,async标记为异步
    try { // 加载preferences
      let pref = await preferences.getPreferences(context, name) //get得到的是将来的结果,await等待结果
      this.prefMap.set(name, pref) //把key-value放进映射里
      console.log('testTag', `加载Preferences[${name}]成功`)
    } catch (e) { //同步也要捕获异常
      console.log('testTag', `加载Preferences[${name}]失败`, JSON.stringify(e))
    }
  }

    //新增操作,value的类型比较自由,有专门的一个类型:preferences.valueType
  async putPreferenceValue(name: string, key: string, value: preferences.ValueType){
    if (!this.prefMap.has(name)) { //判断map里有没有这个值
      console.log('testTag', `Preferences[${name}]尚未初始化!`)
      return
    }
    //map里有这个值,可以用map取到Preference对象
    try {
      let pref = this.prefMap.get(name)
      // 写入数据
      await pref.put(key, value)
      // 刷盘
      await pref.flush()
      console.log('testTag', `保存Preferences[${name}.${key} = ${value}]成功`)
    } catch (e) {
      console.log('testTag', `保存Preferences[${name}.${key} = ${value}]失败`, JSON.stringify(e))
    }
  }

  //读取
  async getPreferenceValue(name: string, key: string, defaultValue: preferences.ValueType){
    if (!this.prefMap.has(name)) {
      console.log('testTag', `Preferences[${name}]尚未初始化!`)
      return
    }
    try {
      let pref = this.prefMap.get(name)
      // 读数据,不需要刷盘
      let value = await pref.get(key, defaultValue)
      console.log('testTag', `读取Preferences[${name}.${key} = ${value}]成功`)
      return value //返回结果
    } catch (e) {
      console.log('testTag', `读取Preferences[${name}.${key} ]失败`, JSON.stringify(e))
    }
  }
}

const preferencesUtil = new PreferencesUtil()

export default preferencesUtil as PreferencesUtil

工具类写好了,来使用它。load在何时调用?项目启动时调用(和生命周期有关),在entryAbility onCreate时加载preference:src/main/ets/entryability/EntryAbility.ets

import PreferencesUtil from '../common/util/PreferencesUtil'
export default class EntryAbility extends UIAbility {
  async onCreate(want, launchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate running');
    // 加载Preferences ,异步
    await PreferencesUtil.loadPreference(this.context, 'MyPreferences')
    // 初始化任务表
    TaskModel.initTaskDB(this.context)
  }
}

在首页indext.ets中使用,页面一加载就载入,用到页面的生命周期函数

  @Provide fontSize: number = 16

  async aboutToAppear(){
    this.fontSize = await PreferencesUtil.getPreferenceValue('MyPreferences', 'IndexFontSize', 16) as number //value转换成number
  }

修改时改变,src/main/ets/views/IndexFontSizePanel.ets中:

import PreferencesUtil from '../common/util/PreferencesUtil'

@Component
export default struct IndexFontSizePanel {
  @Consume fontSize: number //父子组件双向绑定
  fontSizLabel: object = {
    14: '小',
    16: '标准',
    18: '大',
    20: '特大',
  }

  build() {
    Column() {
      Text(this.fontSizLabel[this.fontSize]).fontSize(20)
      Row({ space: 5 }) {
        Text('A').fontSize(14).fontWeight(FontWeight.Bold)
        Slider({
          min: 14,
          max: 20,
          step: 2,
          value: this.fontSize
        })
          .showSteps(true)
          .trackThickness(6)
          .layoutWeight(1)
          .onChange(val => {
            // 修改字体大小
            this.fontSize = val
            // 写入Preferences
            PreferencesUtil.putPreferenceValue('MyPreferences', 'IndexFontSize', val)
          })
        Text('A').fontSize(20).fontWeight(FontWeight.Bold)
      }.width('100%')
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#fff1f0f0')
    .borderRadius(20)
  }
}

测试需要在模拟器里做
在这里插入图片描述
preferenceMap里包含多个preference实例,每个实例的数据结构是key-value。返回的都是异步,需要then,catch,可以做同步编程,加async,await,不需要写回调函数,用try-catch进行异常捕获。

关系型数据库

在这里插入图片描述
在这里插入图片描述

这种数据库不需要网络请求,同时有各种高级特性。
一个应用可以创建多个数据库。
安全级别有四级,S1是最低级别
异步
在这里插入图片描述

不需要手写sql语句,简化:
where部分由predicates构建,需要指定表名,where的详细条件可以用predicates的函数写,如equalTo()

在这里插入图片描述

query 先传入条件,再传入需要查询的字段,返回的是result结果集
控制指针循环读取(自带指针),最开始是-1。每列也有名字和编号,得到名字对应的编号,然后从编号里取数据,封装到结果数组里返回。
前端页面src/main/ets/pages/TaskManagePage.ets,其他都抽取为组件了,位置在view/task中

import { Header } from '../common/components/CommonComponents'
import TaskList from '../views/task/TaskList'
import TaskStatistics from '../views/task/TaskStatistics'

@Entry
@Component
struct TaskManagePage {
  @State totalTask: number = 0
  @State finishTask: number = 0

  build() {
    Column({space: 10}){
      // 1.顶部导航
      Header()
      // 2.任务进度卡片
      TaskStatistics({totalTask: this.totalTask, finishTask: this.finishTask})
      // 3.任务列表
      TaskList({totalTask: $totalTask, finishTask: $finishTask})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F2F3')
  }
}

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值