onPressed: null,
),
),
Row(
//主轴方向中心对齐
mainAxisAlignment: MainAxisAlignment.center,
children: [
new RaisedButton(
textColor: Colors.black,
child: new Text(‘增’),
onPressed: null),
new RaisedButton(
textColor: Colors.black,
child: new Text(‘删’),
onPressed: null),
new RaisedButton(
textColor: Colors.black,
child: new Text(‘改’),
onPressed: null),
],
),
Row(
//主轴方向中心对齐
mainAxisAlignment: MainAxisAlignment.center,
children: [
new RaisedButton(
textColor: Colors.black,
child: new Text(‘查条数’),
onPressed: null),
new RaisedButton(
textColor: Colors.black,
child: new Text(‘查信息’),
onPressed: null),
],
),
Padding(
padding: const EdgeInsets.all(16.0),
child: new Text(‘具体结果是:$_data’),
),
],
);
}
}
在上面_SqliteHandleWidgetState
赋值数据库名字为usermessage.db
,创建数据库表user
语句很简单,就三个字段,分别是主键,用户名,用户密码,界面如下:
界面弄好了,下面就一步一步来。
1.创建数据库和数据表
首先添加依赖:可以到Dart包管理网站去查找sqlite依赖最新版本。
sqflite: ^1.1.0
并在文件引入:
import ‘package:path_provider/path_provider.dart’;
import ‘dart:io’;
import ‘package:sqflite/sqflite.dart’;
import ‘package:path/path.dart’;
注意:对于数据库的操作都是耗时操作,都要通过异步来处理。
//创建数据库
Future createDataBase(String db_name) async {
//在文档目录建立
var document = await getApplicationDocumentsDirectory();
//获取路径 join是path包下的方法,就是将两者路径连接起来
String path = join(document.path, db_name);
//逻辑是如果数据库存在就把它删除然后创建
var _directory = new Directory(dirname(path));
bool exists = await _directory.exists();
if (exists) {
//必存在 这里是为了每次创建数据库表先表删除则删除数据库表
await deleteDatabase(path);
} else {
try {
//不存在则创建目录 如果[recursive]为false,则只有路径中的最后一个目录是
//创建。如果[recursive]为真,则所有不存在的路径
//被创建。如果目录已经存在,则不执行任何操作。
await new Directory(dirname(path)).create(recursive: true);
} catch (e) {
print(e);
}
}
return path;
}
//创建数据库表方法
cratedb_table() async {
//得到数据库的路径
myDataBasePath = await createDataBase(myDataBase);
//打开数据库
Database my_db = await openDatabase(myDataBasePath);
//创建数据库表
await my_db.execute(sql_createUserTable);
//关闭数据库
await my_db.close();
setState(() {
_data = “创建usermessage.db成功,创建user表成功~”;
});
}
给按钮添加点击方法:
child: RaisedButton(
textColor: Colors.black,
child: Text(“创建数据库表”),
onPressed: cratedb_table,
),
运行,安装完apk,用Device File Exploder
来看看内部存储文件:
下面点击创建数据库,后synchronize
来刷新一下:
发现在app_flutter
下多了usermessage.db
文件,确实数据库创建成功了,那继续下面的操作。
2.增加数据
下面实现增加数据,可以用rawInsert
或者db.insert
方式对数据库表数据进行增加(插入),实际上都是通过insert into
方式来插入数据表,下面就用rawInsert
方式来增加一条数据:
//增加方法
addData() async {
//首先打开数据库
Database my_db = await openDatabase(myDataBasePath);
//插入数据
String add_sql = “INSERT INTO user(username,password) VALUES(‘
u
s
e
r
n
a
m
e
′
,
′
username','
username′,′password’)”;
await my_db.transaction((tran) async{
await tran.rawInsert(add_sql);
});
//关闭数据库
await my_db.close();
setState(() {
_data = “增加一条数据成功,名字是:
u
s
e
r
n
a
m
e
,
密码是:
username,密码是:
username,密码是:password”;
});
}
3.查询具体数据
为了配合增加数据,把查询数据库表的功能实现:
//查询具体数值
queryDetail() async{
//打开数据库
Database my_db = await openDatabase(myDataBasePath);
//将数据放到集合里面显示
List
查询数据表很简单,实际上只用rawQuery
这个方法,把增加和查询方法绑定到按钮点击上:
new RaisedButton(
textColor: Colors.black, child: new Text(‘改’), onPressed: null),
…
new RaisedButton(
textColor: Colors.black,
child: new Text(‘查信息’),
onPressed: queryDetail),
验证结果,流程是:
- 先输入用户名和密码
- 点击增加
- 点击查信息: 运行结果如下:
4.删除数据
下面实现删除数据:
//删除一条数据
delete() async {
Database my_db = await openDatabase(myDataBasePath);
//根据id来删除 也可以根据其他信息来删除 例如名字
String delete_ssql = “DELETE FROM user WHERE id = ?”;
//返回所更改的数目
int delete_count = await my_db.rawDelete(delete_ssql,[‘1’]);
//关闭数据库
await my_db.close();
//状态更新
setState(() {
if(delete_count == 1){
_data = “删除成功~”;
} else {
_data = “删除失败,请看错误日志~”;
}
});
}
记得给删除按钮绑定方法,运行结果就不贴了。
5.修改数据
修改数据我相信在平时开发中是用的最频繁的操作了,直接上实现例子:
//修改数据方法
update() async{
//数据库
Database my_db = await openDatabase(myDataBasePath);
String update_sql = “UPDATE user SET username = ? WHERE id = ?”;
await my_db.rawUpdate(update_sql,[‘paul’,‘1’]);
await my_db.close();
setState(() {
_data = “数据修改成功,请查阅~”;
});
}
上面用了rawUpdate
对数据库表进行内容数据更新,也可以用db.update
来更新,自己可以根据需求变更去修改固定字段或者整条数据。上面我是根据id
这个条件来修改一条数据,将id
为1的数据的名字改为paul
。
6.查询条数
//查询有几条
query_num() async{
//数据库
Database my_db = await openDatabase(myDataBasePath);
//用sqflite包的方法firstInValue
int data_count = Sqflite.firstIntValue(await my_db.rawQuery(sql_queryCount));
await my_db.close();
setState(() {
_data = “数据条数:$data_count”;
});
}
对本地数据库的基本操作实现了一遍,下面学习网络请求操作。
六、网络请求操作
Flutter
的请求网络有多种方式,一种是使用dart io
中的HttpClient
发起的请求,一种是使用dio
库,另一种是使用http
库,先学一下get
和post
,put
、delete
就等后面用到在学。下面就实践:
1.dart io发起的请求
1.1.get请求
import ‘dart:io’;//导IO包
import ‘dart:convert’;//解码和编码JSON
void main() {
_get();
}
_get() async{
var responseBody;
//1.创建HttpClient
var httpClient = new HttpClient();
//2.构造Uri
var requset = await httpClient.getUrl(Uri.parse(“http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1”));
//3.关闭请求,等待响应
var response = await requset.close();
//4.进行解码,获取数据
if(response.statusCode == 200){
//拿到请求的数据
responseBody = await response.transform(utf8.decoder).join();
//先不解析打印数据
print(responseBody);
}else{
print(“error”);
}
}
结果如下:
1.2.post请求
_post() async{
var responseBody;
//1.创建HttpClient
var httpClient = new HttpClient();
//2.构造Uri
var requset = await httpClient.postUrl(Uri.parse(“http://www.wanandroid.com/user/login?username=1&password=123456”));
//3.关闭请求,等待响应
var response = await requset.close();
//4.进行解码,获取数据
if(response.statusCode == 200){
//拿到请求的数据
responseBody = await response.transform(utf8.decoder).join();
//先不解析打印数据
print(responseBody);
}else{
print(“error”);
}
}
返回结果如下:
2.dio请求
dio是一个强大的Dart Http
请求库,支持Restful API
、FormData
、拦截器、错误处理、转换器、设置Http代理、请求取消、Cookie
管理、文件上传和下载、超时等。在pub.flutter-io.cn/packages搜最新的依赖包,这个网址太好用,你想搜一些三方库里面都有:
在pubspec.yaml
添加依赖:
dio: ^2.0.14
导入依赖:
import ‘package:dio/dio.dart’;
2.1.get请求
//dio get请求
dio_get() async{
try{
Response response;
//等待返回response
response = await Dio().get(“http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1”);
if(response.statusCode == 200){
print(response);
}else{
print(“error”);
}
}catch(e){
print(e);
}
}
2.2.post请求
dio_post() async{
try{
Response response;
response = await Dio().post(“http://www.wanandroid.com/user/login?username=1&password=123456”);
if(response.statusCode == 200){
print(response);
}else{
print(“error”);
}
}catch(e){
print(e);
}
}
效果同样是ok的。
3.http库
继续去上面链接搜最新的包,是http 0.12.0+1
,在pubspec.yaml
下添加依赖,在文件导入包:
import ‘package:http/http.dart’ as my_http;
上面这次导入库的方式有一点点区别,多了as
这个关键字,这是什么意思呢?通过as
是为了解决变量名冲突的方法,因为导入不同的库有可能遇到不同库之间因为导入变量名冲突的问题。
3.1.get请求
//http库的get请求方式
http_get() async{
try{
//因为导入http 用了as xxx方式,所以对象请求都用xxx.get方式
var response = await my_http.get(“http://gank.io/api/data/%E7%A6%8F%E5%88%A9/10/1”);
if(response.statusCode == 200){
//打印返回的数据
print(response.body);
}else{
print(“error”);
}
}catch(e){
print(e);
}
}
3.2.post请求
//http库的post请求方式
http_post() async{
try{
//因为导入http 用了as xxx方式,所以对象请求都用xxx.get方式
var response = await my_http.post(“http://www.wanandroid.com/user/login?username=1&password=123456”);
if(response.statusCode == 200){
//打印返回的数据
print(response.body);
}else{
print(“error”);
}
}catch(e){
print(e);
}
}
以上三种库的get
和psot
方式都实践了一遍,在平时开发中最好用dio
库和http
库,因为dart io
中是使用HttpClient
发起的请求,HttpClient
本身功能较弱,很多常用功能不支持。
七、JSON
现在很难想象移动应用程序不需要与后台交互或者存储结构化数据。现在开发,数据传输方式基本都是用JSON
,在Flutter
中是没有GSON/Jackson/Moshi
这些库,因为这些库需要运行时反射,在Flutter
是禁用的。运行时反射会干扰Dart
的_tree shaking_。使用_tree shaking_,可以在发版时"去除"未使用的代码,来优化软件的大小。由于反射会默认使用所有代码,因此_tree shaking_会很难工作,这些工具无法知道哪些widget
在运行时未被使用,因此冗余代码很难剥离,使用反射时,应用尺寸无法轻松进行优化,虽然不能在Flutter
使用运行时反射,但有些库提供了类型简单易用的API
,但它们是基于代码生成的。下面学学在Flutter
中如何操作JSON
数据的使用JSON
有两个常规策略:
- 手动序列化和反序列化
- 通过代码生成自动序列化和反序列化 不同的项目有不同的复杂度和场景,针对于小的项目,使用代码生成器可能会杀猪用牛刀了。对于具有多个
JSON model
的复杂应用程序,手动序列化可能会比较繁琐,且容易出错。
1.手动序列化JSON
Flutter
中基本的JSON序列化非常简单,Flutter
有一个内置的dart:convert
库,其中包含一个简单的JSON解码器和编码器。下面简单实现一下:
1.1.内连序列化JSON
首先记得导库:
import ‘dart:convert’;
然后根据字符串解析:
//内连序列化JSON
decodeJson() {
var data= ‘{“name”: “Knight”,“email”: “Knight@163.com”}’;
Map<String,dynamic> user = json.decode(data);
//输出名字
print(“Hello,my name is ${user[‘name’]}”);
//输出邮箱
print(“Hello,This is my email ${user[‘email’]}”);
}
结果输出:
I/flutter ( 5866): Hello,my name is Knight
I/flutter ( 5866): Hello,This is my email Knight@163.com
这样,可以获得我们想要的数据了,我觉得这种方法很实用又能简单理解,但是不幸的是,JSON.decode()
仅返回一个Map<String,dynamci>
,这意味着当直到运行才知道值的类型,这种方法会失去大部分静态类型语言特性:类型安全、自动补全和编译时异常。这样的话,代码变得非常容易出错,就好像上面我们访问name
字段,打字打错了,打成namr
。但是这个JSON
在map结构中,编译器不知道这个错误的字段名(编译时不会报错)。为了解决所说的问题,模型类中序列化JSON的作用出来了。
1.2.模型类中序列化JSON
通过引入一个简单的模型类(model class)来解决前面提到的问题,建立一个User
类,在类内部有两个方法:
User.fromJson
构造函数,用于从一个map构造出一个User
实例map structuretoJson
方法,将User
实例化一个map 这样调用的代码就具有类型安全、自动补全和编译时异常,当拼写错误或字段类型视为其他类型,程序不会通过编译,那就避免运行时崩溃。
1.2.1.user.dart
新建一个model文件夹,用来放实体,在其文件下新建User.dart
:
class User {
final String name;
final String email;
User(this.name, this.email);
-
User.fromJson(Map<String, dynamic> json)
-
name = json[‘name’],
email = json[‘email’];
Map<String, dynamic> toJson() =>
{
‘name’: name,
‘email’: email,
};
}
调用如下:
import ‘model/User.dart’;//记得添加
…
//使用模型类反序列化
decodeModelJson(){
var data= ‘{“name”: “Knight”,“email”: “Knight@163.com”}’;
Map userMap = json.decode(data);
var user = new User.fromJson(userMap);
//打印出名字
print(“Hello,my name is ${user.name}”);
//打印出邮箱
print(“Hello,my name is ${user.email}”);
}
把序列化逻辑到移到模型本身内部,采用这种方法,反序列化数据就很简单了。序列化一个user,只是将User
对象传递给该JSON.encode
方法:
//序列化一个user
encodeModelJson(){
var user = new User(“Knight”,“Knight163.com”);
String user_json = json.encode(user);
print(user_json);
}
结果输出:
I/flutter ( 6684): {“name”:“Knight”,“email”:“Knight163.com”}
2.使用代码生产库序列化JSON
下面使用json_serializable package
包,它是一个自动化的源代码生成器,可以为开发者生成JSON序列化模板。
2.1.添加依赖
要包含json_serializable
到项目中,需要一个常规和两个开发依赖项,开发依赖项是不包含在应用程序源代码中的依赖项:
dependencies:
Your other regular dependencies here
json_annotation: ^2.0.0
dev_dependencies:–>开发依赖项
Your other dev_dependencies here
build_runner: ^1.1.3 -->最新版本1.2.8 因为我sdk版本比较低 所以用低版本
json_serializable: ^2.0.2
2.2.代码生成
有两种运行代码生成器的方法:
- 一次性生成,在项目根目录运行
flutter packages pub run build_runner build
,可以在需要为我们的model
生成json
序列化代码。这触发一次性构建,它通过源文件,挑选相关的并为它们生成必要的序列化代码。这个非常方便,但是如果我们不需要每次在model类中进行更改都要手动运行构建命令的话会更好。 - 持续生成,使用_watcher_可以使源代码生成的过程更加方便,它会监视项目中文化的变化,并在需要时自动构建必要的文件,通过
flutter packages pub run build_runner watch
在项目根目录运行启动_watcher_,只需启动一次观察器,然后并让它在后台运行,这是安全的。
将上面的User.dart
修改成下面:
import ‘package:json_annotation/json_annotation.dart’;
part ‘User.g.dart’;–>一开始爆红
//这个标注是告诉生成器,这个类是需要生成Model类的
@JsonSerializable()
class User{
User(this.name, this.email);
String name;
String email;
factory User.fromJson(Map<String, dynamic> json){—>一开始爆红
return _$UserFromJson(json);
}
Map<String, dynamic> toJson() { —>一开始爆红
return _$UserToJson(this);
}
}
下面就用一次性生成命令,在项目根目录打开命令行执行:
最后发现会在当前目录生成User.g.dart
文件:
里面的内容可以自己去看看看,就是反序列化/序列化的操作。注意:没生成User.g.dart
执行多几次命令即可。 最后通过json_serializable
方式反序列化JSON
字符串,不需要对先前代码修改:
2.3.反序列化
var data= ‘{“name”: “Knight”,“email”: “Knight@163.com”}’;
Map userMap = json.decode(data);
var user = new User.fromJson(userMap);
//打印出名字
print(“Hello,my name is ${user.name}”);
//打印出邮箱
print(“Hello,my name is ${user.email}”);
2.4.序列化
var user = new User(“Knight”,“Knight163.com”);
String user_json = json.encode(user);
print(user_json);
结果是跟上面一样,不过这种方式额外多了生成一个文件…
八、例子
下面实现一个简单例子,效果图如下:
返回的json格式是如下:
{
“error”: false,
“results”: [{
“_id”: “5c6a4ae99d212226776d3256”,
“createdAt”: “2019-02-18T06:04:25.571Z”,
“desc”: “2019-02-18”,
“publishedAt”: “2019-02-18T06:05:41.975Z”,
“source”: “web”,
“type”: “\u798f\u5229”,
“url”: “https://ws1.sinaimg.cn/large/0065oQSqly1g0ajj4h6ndj30sg11xdmj.jpg”,
“used”: true,
“who”: “lijinshanmx”
}, {
“_id”: “5c6385b39d21225dd7a417ce”,
“createdAt”: “2019-02-13T02:49:23.946Z”,
“desc”: “2019-02-13”,
“publishedAt”: “2019-02-13T02:49:33.16Z”,
“source”: “web”,
“type”: “\u798f\u5229”,
“url”: “https://ws1.sinaimg.cn/large/0065oQSqly1g04lsmmadlj31221vowz7.jpg”,
“used”: true,
“who”: “lijinshanmx”
}]
}
上面是一个内嵌数组,需要增加两个实体类,如下: ViewResult类如下:
import ‘ResultModel.dart’;
class ViewResult{
bool error;
List list;
ViewResult(joinData){
//获得返回的error值
error = joinData[‘error’];
list = [];
print(joinData[‘results’]);
//获得"results"里的内容
if(joinData[‘results’] != null){
for(var dataItem in joinData[‘results’]){
list.add(new ResultModel(dataItem));
}
}
}
ResultModel类如下:
class ResultModel{
String _id;
String createdAt;
String desc;
String publishedAt;
String source;
String type;
String url;
bool used;
String who;
ResultModel(jsonData){
_id = jsonData[‘_id’];
createdAt = jsonData[‘createdAt’];
desc = jsonData[‘desc’];
publishedAt = jsonData[‘publishedAt’];
source = jsonData[‘source’];
type = jsonData[‘type’];
url = jsonData[‘url’];
used = jsonData[‘used’];
who = jsonData[‘who’];
}
}
ListView的Item布局:
//需要传list 和对应下标
Widget photoWidget(List resultLists,int index){
return Card(
child: Container(
height: 300,
child: Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: Image.network(resultLists[index].url,
fit:BoxFit.fitWidth,
//scale: 2.5,
),
),
),
],
),
),
);
}
所有代码如下:
import ‘package:flutter/material.dart’;
import ‘dart:convert’;//解码和编码JSON
import ‘package:http/http.dart’ as my_http;
import ‘model/ViewResult.dart’;
import ‘model/ResultModel.dart’;
//app入口
void main() {
runApp(MyApp());
}
//用无状态控件显示
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//主题色
theme: ThemeData(
//设置为蓝色
primarySwatch: Colors.red),
//这是一个Widget对象,用来定义当前应用打开的时候,所显示的界面
home: BigPhotoWidget(),
);
}
}
//主框架
class BigPhotoWidget extends StatefulWidget {
@override
State createState() {
return new _BigPhotoState();
}
}
class _BigPhotoState extends State {
ViewResult viewresult;
//具体的数据集合
List resultLists = [];
@override
void initState(){
super.initState();
getData();
}
getData() async{
try{
//因为导入http 用了as xxx方式,所以对象请求都用xxx.get方式
//方式一
// await my_http.get(“http://gank.io/api/data/福利/10/1”)
// .then((response){
// if(response.statusCode == 200){
// var ViewData = json.decode(response.body);
// viewresult = ViewResult(ViewData);
// if(!viewresult.error){
// //继续解析
// for(int i = 0;i < viewresult.list.length;i++){
// resultLists.add(viewresult.list[i]);
// }
// //记得调用刷新
// setState(() {
//
// });
// }
// }else{
// print(“error”);
// }
// });
//方式二 请求
var response = await my_http.get(“http://gank.io/api/data/福利/10/1”);
//判断状态
if(response.statusCode == 200){
//解析
var ViewData = json.decode(response.body);
viewresult = ViewResult(ViewData);
if(!viewresult.error){
//继续解析
for(int i = 0;i < viewresult.list.length;i++){
resultLists.add(viewresult.list[i]);
}
//记得调用刷新
setState(() {
});
}
}else{
print(“error”);
}
}catch(e){
print(e);
}
}
@override
Widget build(BuildContext context) {
return new Scaffold(
//appBar
appBar: AppBar(
title: Text(“妹子图”),
//标题居中
centerTitle: true,
),
body: ListView.builder(
itemCount: resultLists.length,
itemBuilder: (BuildContext context,int index){
return Column(
children: [
photoWidget(resultLists,index),
],
);
},
),
);
}
}
//需要传list 和对应下标
Widget photoWidget(List resultLists,int index){
return Card(
child: Container(
height: 300,
child: Row(
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4.0),
child: Image.network(resultLists[index].url,
fit:BoxFit.fitWidth,
//scale: 2.5,
),
),
),
],
),
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
文末
当你打算跳槽的时候,应该把“跳槽成功后,我能学到什么东西?对我的未来发展有什么好处”放在第一位。这些东西才是真正引导你的关键。在跳槽之前尽量“物尽其用”,把手头上的工作做好,最好是完成了某个项目或是得到提升之后再走。跳槽不是目的,而是为了达到最终职业目标的手段
最后祝大家工作升职加薪,面试拿到心仪Offer.
为此我在文末整理了一些关于移动开发者需要的资料,欢迎大家免费领取
领取方式:点击我的GitHub
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-3MxrJ7fj-1711395058701)]
[外链图片转存中…(img-W3OiRcPW-1711395058701)]
[外链图片转存中…(img-n0ZBKlTr-1711395058701)]
[外链图片转存中…(img-Xaw5EWw7-1711395058702)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-LHPlboPv-1711395058702)]
文末
当你打算跳槽的时候,应该把“跳槽成功后,我能学到什么东西?对我的未来发展有什么好处”放在第一位。这些东西才是真正引导你的关键。在跳槽之前尽量“物尽其用”,把手头上的工作做好,最好是完成了某个项目或是得到提升之后再走。跳槽不是目的,而是为了达到最终职业目标的手段
最后祝大家工作升职加薪,面试拿到心仪Offer.
为此我在文末整理了一些关于移动开发者需要的资料,欢迎大家免费领取
领取方式:点击我的GitHub
[外链图片转存中…(img-XLSutrCO-1711395058702)]
[外链图片转存中…(img-gSg4EsqQ-1711395058703)]