Header

开场

终将与生活对线.

前言

💻 学习Flutter这些时间了,只能说这个框架后劲挺大的.最近由于个人原因,很多 想做事情都耽搁了,确实非常遗憾.虽然学习了Flutter有一段时间了,但是距离能不能找到一份和这个接近的工作 真的像个While(True)语句;多余的话就不罗嗦了,直接进入正题:

🔘首先,Flutter实战小猿网盘一共 30个文件,7个文件夹, 写完这个项目真的感觉自己还需要提升,写的过程中真的有遇见了很多的困难,同时也很感谢在这个项目中帮助过我的贵人.

⚫其次, 这个项目很多地方的逻辑真的很夸张,完全是超出了我的负荷和认知, 有些人的逻辑真的很缜密,让我深深的认识到了......(此感悟放结尾)

⚪最后, 最关心的问题就是此软件实现的重要功能:

  1. 登录
  2. 文件上传至网盘
  3. 文件下载至电脑

等等还有许多的功能... 当然你想节约时间请直接使用电脑下载:

软件下载网址:https://wwii.lanzouq.com/iU0sp1dvoxyh
密码: 8888
视频学习地址:https://www.bilibili.com/video/BV1Bc41197te/?vd_source=8a2d2437f3c68308d1c5fb038d646aeb

项目整体设计思路

  • 构造RP原型
  • 写静态前端代码
  • 渲染接口数据

开发工具

  • Android Studio 2022 3.1
  • Visual Studio Code

实现过程

1 main文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import 'package:fluent_ui/fluent_ui.dart';
import 'package:provider/provider.dart';
import "./router/router.dart";
import 'package:bitsdojo_window/bitsdojo_window.dart';
import './services/tary.dart';
import './services/userService.dart';
import './provider/count.dart';
import './provider/transfersList.dart';
void main() async {
// 处理原生通信的桥梁
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());

// 初始化系统托盘
initSystemTray();

bool isLogin = await UserService.isLogin();
if (isLogin) {
// 窗口大小
doWhenWindowReady(() {
final win = appWindow;
const initialSize = Size(1000, 600);
win.minSize = initialSize;
win.size = initialSize;
win.alignment = Alignment.center;
win.title = "小猿网盘";
win.show();
});
} else {
doWhenWindowReady(() {
final win = appWindow;
const initialSize = Size(400, 500);
win.minSize = initialSize;
win.size = initialSize;
win.alignment = Alignment.center;
win.title = "小猿网盘";
win.show();
});
}
}

class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MultiProvider(
providers:[
// 挂载数据状态
ChangeNotifierProvider(create: (_)=>Counter()),
ChangeNotifierProvider(create: (_)=>TransfersProvider()),
] ,
child:FluentApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: FluentThemeData(
fontFamily: "微软雅黑",
accentColor: Colors.green, //主题颜色
scaffoldBackgroundColor: Colors.white, //背景颜色
navigationPaneTheme: const NavigationPaneThemeData(
// 左侧导航栏颜色
backgroundColor: Color(0xFFF5FFFA))),
//挂载路由
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
));
}
}

这个文件代码主要是为了管理我们后续文件和各个数据经过的地方,也可以理解为一个中转设备(或者理解为:“中央空调”);

2 登录功能

2.1 发送验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
_sendCode() async {
var reg = RegExp(r'^1d{10}$');
if (_photo.length == 11 || reg.hasMatch(_photo)) {
var result =
await httpsClient.post("api/sendCode", data: {"phone": _photo});
if (result != null) {
if (result.data["success"]) {
// 实际项目是在手机上面查看验证码 这里是模拟
CherryToast.success(
animationType: AnimationType.fromLeft,
animationDuration: Duration(milliseconds: 500),
toastDuration: Duration(milliseconds: 1500),
title: Text("验证码:${result.data["code"]}",
style: TextStyle(color: Colors.black)))
.show(context);
// 发送验证码成功的时候 倒计时
_showtimer();
} else {
// 请求网络失败
CherryToast.error(
animationType: AnimationType.fromLeft,
animationDuration: Duration(milliseconds: 500),
toastDuration: Duration(milliseconds: 1500),
title: Text("${result.data["message"]}",
style: TextStyle(color: Colors.black)))
.show(context);
}
} else {
// 请求失败
CherryToast.error(
animationType: AnimationType.fromLeft,
animationDuration: Duration(milliseconds: 500),
toastDuration: Duration(milliseconds: 1500),
title:
Text("请求失败,请检查网络", style: TextStyle(color: Colors.black)))
.show(context);
}
} else {
// 请求失败
CherryToast.error(
animationType: AnimationType.fromLeft,
animationDuration: Duration(milliseconds: 500),
toastDuration: Duration(milliseconds: 1500),
title: Text("手机号格式不合法", style: TextStyle(color: Colors.black)))
.show(context);
}
}

登录的功能主要是:自己首要要有一个接口,通过自己封装的Dio库去对自己的接口发起请求, 当服务器返回的验证码输入之后我们会执行登录,执行登录的时候又要对输入的数据进行和 服务器接口中的数据进行判断,当输入的验证码和服务器值相匹配时候,就会保存用户信息;

2.2 执行登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  if (_photo != "" || _code != "") {
var result = await httpsClient
.post("api/doLogin", data: {"phone": _photo, "code": _code});

if (result != null) {
if (result.data["success"]) {
//执行登录 保存用户信息 执行跳转
print(result);
var phoneNum = result.data["userinfo"][0]["phone"];
print(phoneNum);
Storage.setData("userinfo", result.data["userinfo"]);
context.go("/file");
appWindow.hide();
sleep(const Duration(milliseconds: 50));
appWindow.minSize = const Size(1000, 600);
appWindow.size = const Size(1000, 600);
appWindow.alignment = Alignment.center;
appWindow.show();
} else {
CherryToast.error(
animationType: AnimationType.fromLeft,
animationDuration: const Duration(milliseconds: 500),
toastDuration: const Duration(milliseconds: 1500),
title: Text(result.data["message"]))
.show(context);
}
}
}

执行登录:通过输入的数据和服务器的进行验证,如果相匹配跳转到文件页面并且保存用户的信息 实现用户信息的持久化,当跳转到新的页面的时候,数据又要继续加载.

3 File文件

这个文件代码1115行 我只能说夸张

code1115

因此:此处只放最重要的核心代码

3.1 软件头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Widget _headerWidget(transfersProvider) {
return Column(
children: [
Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
GestureDetector(
//点击文件回到根目录
onTap: () {
_folderName = "";
_diskPathList = [];
_getFilesData();
},
child: const MouseRegion(
cursor: SystemMouseCursors.click,
child: Text(
"文件",
style: TextStyle(fontSize: 18, fontFamily: "微软雅黑"),
)),
),
..._diskPathList.map((v) {
return GestureDetector(
onTap: () {
/*
_folderName: golang桌面软件开发实战/runner/Release/xxxx
比如说点击了runner 路径需要变成: golang桌面软件开发实战/runner
比如说点击了Release 路径需要变成: golang桌面软件开发实战/runner/Release

*/
_folderName = _folderName.split(v)[0] + v;
_diskPathList = _folderName.split("/");
_getFilesData();
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Text(
">$v",
style: const TextStyle(
fontSize: 18, fontFamily: "微软雅黑"),
)),
);
}).toList()
],
),
SizedBox(
width: 80,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
icon: const Icon(
FluentIcons.search,
size: 16,
),
onPressed: () {}),
_addButtonWidget(transfersProvider)
],
),
)
],
),
),
Container(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//左侧
SizedBox(
width: 100,
child: Row(
children: [
Checkbox(checked: false, onChanged: (v) {}),
const Text("共10项")
],
),
),
//右侧
SizedBox(
width: 160,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_sortButtonWidget(),
MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.only(right: 4),
child: _isGridView
? IconButton(
icon: const Icon(
FluentIcons.collapse_menu,
size: 20,
),
onPressed: () {
setState(() {
_isGridView = !_isGridView;
});
},
)
: IconButton(
icon: const Icon(
FluentIcons.table,
size: 20,
),
onPressed: () {
setState(() {
_isGridView = !_isGridView;
});
},
),
),
)
],
),
)
],
),
)
],
);
}

如图 所示:

flutterNetdiskHeader

这里主要的功能就是上传和下载文件以及文件的排序,接着往下面看详细的实现.

3.2 文件下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
ListTile(
title: const Text(
'下载',
style: TextStyle(fontSize: 14),
),
onPressed: () async {
context.pop(); //隐藏GoRouter
String? outputFile = await FilePicker.platform.saveFile(
dialogTitle: '请选择保存文件目录:',
fileName: _filesList[index].title, //保存的文件名
);

List userInfo = await UserService.getUserInfo();

if (outputFile != null) {
downloadToast();
// 判断下载的是文件还是文件夹
if (_filesList[index].isFile == true) {
var sign = SignServices.getSign({
"filename": _filesList[index].title,
"uid": userInfo[0]["uid"],
"salt": userInfo[0]["salt"]
});

var apiUrl =
'/api/downlodFile?fileName=${_filesList[index].title}&uid${userInfo[0]["uid"]}=&sign=$sign}';
print("apiurl:$apiUrl");
httpsClient.downLoad(transfersProvider, apiUrl,
outputFile, _filesList[index].fullPath);
} else {
// 文件夹 循环文件夹中的所有
String downloadFilesStr;
if (_folderName == '') {
downloadFilesStr = _filesList[index].title!;
} else {
downloadFilesStr =
'$_folderName/${_filesList[index].title!}';
}
List<OssModelItems> listFiles =
await _getDownloadFiles(downloadFilesStr);

for (OssModelItems value in listFiles) {
//判断value是文件还是文件夹 (根据后缀名判断是文件还是目录)
if (path.extension(value.fullPath!) != "") {
//获取后缀名,如果有后缀名就是文件
var sign = SignServices.getSign({
"fileName": value.fullPath,
"uid": userInfo[0]["uid"],
"salt": userInfo[0]["salt"],
});
String apiUrl =
"api/downlodFile?fileName=${value.fullPath}&uid=${userInfo[0]["uid"]}&sign=$sign";
print("api/downlodFile:$apiUrl");
if (_folderName == "") {
//一级目录
httpsClient.downLoad(
transfersProvider,
apiUrl,
outputFile.replaceAll(
_filesList[index].title!, "") +
value.fullPath!,
value.fullPath);
} else {
//子目录
httpsClient.downLoad(
transfersProvider,
apiUrl,
outputFile.replaceAll(
_filesList[index].title!, "") +
value.fullPath!
.replaceAll("$_folderName/", ""),
value.fullPath);
}
} else {
//目录 空目录 aaa/ ccc/
Directory(outputFile.replaceAll(
_filesList[index].title!, "") +
value.fullPath!.substring(
0, value.fullPath!.length - 1))
.create(recursive: true);
}
}
}
}
},
),

通过对下载的文件进行判断,如果是文件夹就会遍历文件夹中的所有内容且判断 所有文件的类型,从而达到下载成功;如果是单个文件就不会这循环在,直接判断文件 的类型,直接下载并且赋予对应的Icon ;同时,当我们在下载文件的时候需要像请求的接口 进行一定的校验,当我们从服务器下载文件的时候,需要去判断服务器上是否存在这个文件 如果存在就需要更新文件和更新文件的时间,如果没有则会像服务器中添加文件.

3.3 文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
() async {
print(_folderName);
List userInfo = await UserService.getUserInfo();

// 上传文件
FilePickerResult? result =
await FilePicker.platform.pickFiles();

if (result != null) {
uploadToast();
//文件在远程服务器上保存的路径
String dst = _folderName == ""
? result.files.single.name
: "$_folderName/${result.files.single.name}";
var sign = SignServices.getSign({
"dst": dst,
"uid": userInfo[0]["uid"],
"salt": userInfo[0]["salt"], //私钥
});
// 服务器的文件路径
var response = await httpsClient.uploadFile(
transfersProvider,
"api/uploadFile",
{
"dst": dst,
"uid": userInfo[0]["uid"],
"sign": sign
},
result.files.single.path!);
// print("response:$response");

if (response.data["success"] == true) {
_getFilesData();
}
} else {
// User canceled the picker
print("已取消操作");
}
},

上传文件的逻辑和下载文件差不多,但是上传文件的时候需要用户的个人信息作为身份标识 然后在用dio发起请求,请求成功后就需要向Model中获取对应的数据,当服务器返回值成功时候则会 向网盘中添加数据.

3.4 持久化用户信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 //保存用户信息
static setData(String key, dynamic value) async {
final prefs = await SharedPreferences.getInstance();
prefs.setString(key, jsonEncode(value));
}

//获取用户信息
static getData(String key) async {
final prefs = await SharedPreferences.getInstance();
String? tempData = prefs.getString(key);
if (tempData != null) {
// 判断新增
return json.decode(tempData);
}
return null;
}

为什么要持久化用户信息? 当我们在日常使用软件的时候,我们都会有一个用户唯一的ID作为标识 ,这样才可以对不同用户的个人信息不同,就好比 大家的VX (嘿嘿) 里面的联系人或者聊天记录等很多信息是 不同的.当然这里还有清楚用户信息,这里就不献丑了.

3.5 MD5数字签名

1
2
3
案列:(留言给完整加密代码)
String str = '很多东西今生只可给你,别人没法看透.';
var sign = md5.convert(utf8.encode(str));

对数据进行加密这个东西的重要性小猿就不做介绍了,我也相信大家都知道这个的Vital;

3.6 传输数据条

这一部分底部留言给你.

总结

🚲首先,这个项目很多地方的逻辑真的很夸张,完全是超出了我的负荷和认知, 有些人的逻辑真的很缜密,让我深深的认识到了眼界的重要性; 这个练手项目写完只能说很多东西都进一步的理解,下次更新文章也不知道会是多久了,就像开头所说, 我们总将会去和生活对线,大部分人都逃避不了,我也一样;

🚗其次,就是服务器和域名也快要到期了,我也不知道续不续,自己觉得我这个服务器的性能不是很强, 当你们看到这个的时候其实也感觉到了,有时候你们点击某个按钮的时候就会有延迟,如果你网络好 ,就没有什么影响;但凡网络差点味道,就会很明显感觉网站有卡顿效果,主要就是我给网站加了一些动画效果 ;所以后续我打算重新构造网站的底层和改一下网站的风格以及很多动画都会减去,同时也会去弄一弄CDN,提高速度;

🚄最后......

文件地址:
网址:https://wwii.lanzouq.com/iU0sp1dvoxyh
密码: 8888
视频学习地址:https://www.bilibili.com/video/BV1Bc41197te/?vd_source=8a2d2437f3c68308d1c5fb038d646aeb

结尾

Widen one's horizon