-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmainwindow.cpp
More file actions
543 lines (469 loc) · 29.5 KB
/
mainwindow.cpp
File metadata and controls
543 lines (469 loc) · 29.5 KB
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
#include "mainwindow.h"
#include "sitemanagerdialog.h"
#include "ui_mainwindow.h"
#include <QLineEdit>
#include <QLabel>
#include <QPushButton>
#include <QToolBar>
#include <QStandardItem>
#include <QDebug>
#include <QStandardPaths>
#include <QDir>
#include <QMessageBox>
//构造函数
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//创建并设置模型
//1.创建模型对象
localModel = new QFileSystemModel(this);
//2.设置模型的根路径,空字符串""代表"我的电脑",显示所有驱动器
localModel->setRootPath("");
//3.设置过滤器,显示所有的目录和文件,不显示"."和".."这两个特殊目录
localModel->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
//将模型连接到视图
ui->localTree->setModel(localModel);
ui->localTable->setModel(localModel);
//两个视图共享一个模型,目录树和表格就是一致的。
sftpManager = new SftpManager(this);
setupQuickConnectBar();
sftpThread = new QThread();
sftpManager->moveToThread(sftpThread); // 将sftpManager对象“移动”到新线程
sftpThread->start(); // 启动新线程的事件循环
connect(this, &MainWindow::connectRequested, sftpManager, &SftpManager::connectToHost);
//经理下达指令(主线程->后台线程)
connect(sftpManager, &SftpManager::connected, this, &MainWindow::onSftpConnected);
//厨师报告成功(后台线程->主线程) onSftpConnected函数是在主界面线程中执行的,所以它可以在这里安全地更新界面元素
connect(sftpManager, &SftpManager::error, this, &MainWindow::onSftpError);
//厨师报告失败(后台线程->主线程)
/*创建模型并关联视图*/
remoteModel = new QStandardItemModel(this);//创建一个空的目录柜,通用空白的模型,需要手动填充数据
ui->remoteTree->setModel(remoteModel);
ui->remoteTable->setModel(remoteModel);
//remoteModel数据仓库,两个视图共享同一个模型实例,上下窗格联动的基础
/*数据请求的"双向通道"(信号与槽)*/
connect(this, &MainWindow::listDirectoryRequested, sftpManager, &SftpManager::listDirectory);//建立一条从主窗口到后台的指令通道
/*逻辑:当前MainWindow需要获取(/home/user/)的内容时,它会发射listDirectoryRequest信号,并把路径"/home/user/"作为参数
2.SftpManager接收到这个信号,它的listDirectory槽函数被调用
3.SftpManager于是开始在后台线程中,通过libssh2向服务器请求/home/user/目录的内容
*/
connect(sftpManager, &SftpManager::directoryListed, this, &MainWindow::onSftpDirectoryListed);//建立一条从后台到主窗口的结果回报通道
/*1.当SftpManager在后台成功获取并解析完/home/user/的所有文件信息后,它会将这些信息打包成一个QList<RemoteFile>列表
2。然后,它发射directoryLIsted信号,并将这个文件列表作为参数
3.MainWindow接收到这个信号,它的onSftpDirectoryListed槽函数被调用。
4.在onSftpDiretoryListed函数中,最终将收到的文件列表数据 ,一条条地手动填充到remoteModel中,由于视图和模型关联,界面就会自动更新,向用户显示出文件列表
*/
connect(ui->remoteTree, &QTreeView::expanded, this, &MainWindow::on_remoteTree_expanded);//新增
//实现双击进入目录
connect(ui->remoteTree, &QTreeView::doubleClicked, this, &MainWindow::handleRemoteDoubleClick);
connect(ui->remoteTable, &QTableView::doubleClicked, this, &MainWindow::handleRemoteDoubleClick);
/*异步数据加载与显示流程:
* 小结:界面准备: 创建好空的模型,并让视图(展柜)盯住这个模型。
数据请求: 主界面通过 listDirectoryRequested 信号发出获取数据的指令。
后台处理: SftpManager 执行实际的网络请求。
数据返回: SftpManager 通过 directoryListed 信号将结果返回。
界面更新: 主界面在槽函数中接收数据,填充到模型中,视图随之自动更新。*/
// 将前台的下载请求信号连接到后台的下载执行槽
connect(this, &MainWindow::downloadFileRequested, sftpManager, &SftpManager::downloadFile);
// 将后台的进度汇报信号连接到前台的进度处理槽
connect(sftpManager, &SftpManager::transferProgress, this, &MainWindow::onTransferProgress);
// 将后台的下载完成信号连接到前台的完成处理槽
connect(sftpManager, &SftpManager::downloadFinished, this, &MainWindow::onDownloadFinished);
// 将前台的上传请求信号连接到后台的上传执行槽
connect(this, &MainWindow::uploadFileRequested, sftpManager, &SftpManager::uploadFile);
// 将后台的上传完成信号连接到前台的完成处理槽
connect(sftpManager, &SftpManager::uploadFinished, this, &MainWindow::onUploadFinished);
//将本地视图的doubleClicked信号 连接到处理本地视图双击事件的槽
connect(ui->localTree, &QTreeView::doubleClicked, this, &MainWindow::handleLocalDoubleClick);
connect(ui->localTable, &QTableView::doubleClicked, this, &MainWindow::handleLocalDoubleClick);
//==========队列传输日志===========
// 创建成功和失败传输的模型
successfulTransfersModel = new QStandardItemModel(this);
failedTransfersModel = new QStandardItemModel(this);
// 将模型设置给对应的表格视图
ui->successfulTable->setModel(successfulTransfersModel); //
ui->failedTable->setModel(failedTransfersModel); //
// 为这两个模型设置好表头
successfulTransfersModel->setHorizontalHeaderLabels({"本地文件", "方向", "远程文件", "大小", "状态"});
failedTransfersModel->setHorizontalHeaderLabels({"本地文件", "方向", "远程文件", "大小", "错误信息"});
// 1. 将 "删除" 按钮的触发信号连接到我们的处理槽上
connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDeleteActionTriggered);
// 2. 将前台的删除请求信号连接到后台的删除执行槽
connect(this, &MainWindow::deleteFileRequested, sftpManager, &SftpManager::deleteRemoteFile);
// 3. 将后台的删除完成信号连接到前台的完成处理槽
connect(sftpManager, &SftpManager::remoteFileDeleted, this, &MainWindow::onRemoteFileDeleted);
// 将“消息记录”动作的 toggled 信号,直接连接到 messageLogTextEdit 的 setVisible 槽
connect(ui->actionToggleMessageLog, &QAction::toggled, ui->messageLogTextEdit, &QWidget::setVisible);
// 将“本地目录树”动作的 toggled 信号,直接连接到 localTree 的 setVisible 槽
connect(ui->actionToggleLocalTree, &QAction::toggled, ui->localTree, &QWidget::setVisible);
// 将“远程目录树”动作的 toggled 信号,直接连接到 remoteTree 的 setVisible 槽
connect(ui->actionToggleRemoteTree, &QAction::toggled, ui->remoteTree, &QWidget::setVisible);
// 将“传输队列”动作的 toggled 信号,直接连接到 transferQueueTabWidget 的 setVisible 槽
connect(ui->actionToggleQueue, &QAction::toggled, ui->transferQueueTabWidget, &QWidget::setVisible);
//将"刷新"动作的triggered信号,直接连接到onRefreshActionTriggered槽
connect(ui->actionRefresh, &QAction::triggered, this, &MainWindow::onRefreshActionTriggered);
//站点管理器的槽
connect(ui->actionSiteManager, &QAction::triggered, this, &MainWindow::onSiteManagerActionTriggered);
}
MainWindow::~MainWindow()
{
delete ui;
}
//左侧栏目录树
void MainWindow::on_localTree_clicked(const QModelIndex &index)//槽函数名遵循on_控件名_信号名的命名约定
//QModelIndex是一个临时的"坐标"或"指针",由视图(QTreeView)创建,用来唯一地标识模型(QFileSystemModel)中的某个特定数据项的位置。
//当用鼠标点击目录树中的某一项时,QTreeView 就会把代表那一项的“坐标”——也就是一个 QModelIndex 对象——通过 clicked 信号发射出来。
//这个槽函数接收一个对QModelIndex对象的只读引用,这个对象就是在视图中点击的那一项的坐标。
{
//1.从模型中获取被点击项的文件路径显示下拉框的内容
ui->localTable->setRootIndex(index);//逻辑:将根目录设置为用户在localTree中点击的那一项
//setRootIndex(index):作用是改变视图观察模型的方式。它告诉视图不要从模型的顶层根节点开始展示,而是从指定的index开始。
//index是从槽函数中的参数传递进来的
//ui->localPathComboBox->setCurrentText(localModel->filePath(index));
//首先要获取被点击项的文件路径
const QString path = localModel->filePath(index);
//然后将这个路径显示到上方的路径栏(下拉框)中
ui->localPathComboBox->lineEdit()->setText(path);
}
void MainWindow::setupQuickConnectBar(){
//在Qt Designer中确保主窗口MainWindow有一个ToolBar
//以纯C++代码的方式,动态地创建"快速连接"工具栏上所有界面元素(标签,输入框,按钮),组装起来,最后为按钮绑定点击事件
auto hostLabel = new QLabel(QString::fromUtf8("主机(H):"),this);//auto 关键字(自动推断出变量的类型) auto hostLabel相当于 QLabel *hostLbel
auto hostLineEdit = new QLineEdit(this);
hostLineEdit->setObjectName("hostLineEdit");//设置对象名以便查找
auto userLabel = new QLabel(QString::fromUtf8(" 用户名(U):"), this);
auto userLineEdit = new QLineEdit(this);
userLineEdit->setObjectName("userLineEdit");
auto passLabel = new QLabel(QString::fromUtf8(" 密码(W):"), this);
auto passLineEdit = new QLineEdit(this);
passLineEdit->setEchoMode(QLineEdit::Password);
passLineEdit->setObjectName("passLineEdit");
auto portLabel = new QLabel(QString::fromUtf8(" 端口(P):"), this);
auto portLineEdit = new QLineEdit(this);
portLineEdit->setObjectName("portLineEdit");
auto quickConnectButton = new QPushButton(QString::fromUtf8("快速连接(Q)"), this);
ui->quickConnectToolBar->addWidget(hostLabel);
ui->quickConnectToolBar->addWidget(hostLineEdit);
ui->quickConnectToolBar->addWidget(userLabel);
ui->quickConnectToolBar->addWidget(userLineEdit);
ui->quickConnectToolBar->addWidget(passLabel);
ui->quickConnectToolBar->addWidget(passLineEdit);
ui->quickConnectToolBar->addWidget(portLabel);
ui->quickConnectToolBar->addWidget(portLineEdit);
ui->quickConnectToolBar->addWidget(quickConnectButton);
// 将按钮的点击信号连接到槽函数
connect(quickConnectButton, &QPushButton::clicked, this, &MainWindow::handleQuickConnect);
//connect(发送者,&发送者类::信号,接收者,&接收者::槽);
/*1.程序启动,connect语句被执行,一条从"按钮被点击"到"执行handleQuickConnect"的连接被注册在Qt的事件系统中
2.用户将鼠标移动到"快速连接"按钮上,然后按下鼠标左键
3.quickConnectButton 对象侦测到这个点击事件。
4.quickConnectButton 发射 (emit) 它的 clicked() 信号,就像客人在大声宣布:“我按门铃了!”
5.Qt的事件系统捕获到这个信号,并查找所有与它连接的槽
6.系统发现MainWindow的handleQuickConnect槽函数与这个信号相连,于是调用handleQuickConnect()函数
7.handleQuickConnect函数内部的代码被执行,从界面上获取主机,用户名等信息,并发射connectRequest信号,启动了整个SFTP连接流程*/
}
void MainWindow::handleQuickConnect()
{
QString host = findChild<QLineEdit*>("hostLineEdit")->text();
//作用:从快速连接栏的四个输入框中,准确地找到它们并读取用户输入地文本数据。
//findChild<...>:在当前的MainWindow的所有子孙控件中,递归搜索一个类型和对象名都匹配地控件
//在MainWindow的所有子孙控件中,寻找一个类型是QLineEdit*(行编辑器指针)并且objectName(对象名)是"hostLineEdit"的控件
QString user = findChild<QLineEdit*>("userLineEdit")->text();//text()是获取QLine中当前显示的文本内容,返回一个QString
QString pass = findChild<QLineEdit*>("passLineEdit")->text();
int port = findChild<QLineEdit*>("portLineEdit")->text().toInt();//.toInt将一个字符串(如"22")转换成一个整数(如22)
ui->messageLogTextEdit->append("正在尝试连接... 界面将保持响应!");
// **直接的、阻塞式的调用!**
//这段代码在主界面线程中执行,但是connectToHost函数内部包含了多个耗时,阻塞式的网络操作(如::connect,libssh2_session_handshake)
//在此期间,整个应用程序的主事件循环被阻塞
//sftpManager->connectToHost(host, port, user, pass);
emit connectRequested(host, port, user, pass);//
//所以,不能直接调用函数,而是发射一个信号(emit),发射信号这个动作本身是非阻塞的,瞬间完成的
//工作流程:MainWindow在主线程发射connectRequested信号(瞬间完成)
//2.在后台线程中运行的SftpManager对象,通过之前建立的connect连接,接收到这个信号。
//3.SftpManager在不影响主界面的后台线程里,慢慢地执行耗时地connectToHost函数
//4.当connectToHost执行完毕后,SftpManager再通过发射connected或error信号,将结果异步地通知回主界面线程进行显示。
//小结:主线程发信号,子线程做工作。
}
void MainWindow::onSftpConnected()
{
ui->messageLogTextEdit->append("连接成功! (消息来自工作线程)");
// 之后我们会在这里触发列出目录的操作
emit listDirectoryRequested(QModelIndex(),"/");//连接成功后自动请求根目录地文件列表
}
void MainWindow::onSftpError(const QString &message)
{
ui->messageLogTextEdit->append("错误: " + message + " (消息来自工作线程)");
}
//onSftpDirectoryListed这个槽函数是后台与前台交互的终点,也是数据最终呈现给用户的关键环节
/*核心任务:接收由后台SftpManager线程发送过来的,已经整理好的远程文件信息列表files,然后 将这些信息填充到远程内容的数据模型(remoteModel)
一旦模型数据被更新,所有与该模型关联的视图(remoteTree和remoteTable)都会自动刷新 ,从而将远程目录的内容显示给用户。*/
void MainWindow::onSftpDirectoryListed(const QModelIndex &parentIndex,const QList<RemoteFile> &files)
{
//打印日志
qDebug() << "=======================================================";
qDebug() << "[DEBUG] onSftpDirectoryListed CALLED. Received" << files.count() << "files.";
qDebug() << "[DEBUG] Parent index is valid:" << parentIndex.isValid();
QStandardItem *parentItem;//新增
//步骤1:根据parentIndex判断是在填充根目录还是子目录
if(!parentIndex.isValid()){
qDebug() << "[DEBUG] Populating ROOT directory...";
//如果parentIndex无效,说明是对根目录的请求
remoteModel->clear(); // 清空旧数据
remoteModel->setHorizontalHeaderLabels({"文件名", "大小", "所有者/组", "最后修改", "权限"}); // 设置表头
//setHorizontalHeaaderLabels是一个成员函数,是QStandardItemModel的功能之一
// **关键修复点 1**: 必须获取模型的“不可见根项目”作为父项
parentItem = remoteModel->invisibleRootItem();
}else{
// --- 子目录逻辑 (有改动) ---
// 这是对子目录的请求,从index获取父项
qDebug() << "[DEBUG] Populating SUB-DIRECTORY for index at row:" << parentIndex.row() << "column:" << parentIndex.column();
parentItem = remoteModel->itemFromIndex(parentIndex);
}
// 安全检查:如果因某些原因找不到父项,则直接返回,避免程序崩溃。
if (!parentItem){
qDebug() << "[DEBUG] CRITICAL ERROR: Could not find parentItem. Aborting function.";
qDebug() << "=======================================================";
return;
}
qDebug() << "[DEBUG] Parent item found. Name:" << parentItem->text();
qDebug() << "[DEBUG] Parent item has" << parentItem->rowCount() << "children BEFORE adding new ones.";
// ==================== 核心修复代码 ====================
// 在添加新项目之前,先清空该父项下的所有现有子项(即移除占位符)。
// 这确保了视图能够正确地接收到模型变化的信号。
if (parentItem->hasChildren()) {
qDebug() << "[DEBUG] Removing" << parentItem->rowCount() << "old child item(s)...";
parentItem->removeRows(0, parentItem->rowCount());
}
//步骤2:遍历从后台线程传递 过来的文件列表
for (const RemoteFile &file : files) {
//逻辑:遍历从信号中接收到的files列表,对于其中的每一个RemoteFile对象,执行循环体内的代表
QList<QStandardItem*> rowItems;//每次在准备新的一行数据前,先创建一个临时的,用于组装表格中一整行数据的容器
// --- 第 0 列: 文件名 ---
QStandardItem* nameItem = new QStandardItem(file.name);
// **关键点**: 如果这一项是一个目录,我们给它添加一个空的子项。
// 这个空的子项就像一个“占位符”,它会告诉 QTreeView 在这个目录旁边显示一个“>”展开箭头。
// 当用户点击这个箭头时,`expanded` 信号才会被触发。
if (file.isDirectory) {
nameItem->appendRow(new QStandardItem());
}
rowItems.append(nameItem);
//QStandardItem是QStandTtemModel中的基本数据单元,代表一个单元格。
//rowItems.append(new QStandardItem(file.name));//创建表格中的一个单元格将其添加到我们准备的临时托盘
rowItems.append(new QStandardItem(file.isDirectory ? "" : QString::number(file.size)));//负责添加"大小"这一列的单元格
rowItems.append(new QStandardItem(QString("%1 / %2").arg(file.ownerId).arg(file.groupId)));//所有者/组
//rowItems.append(new QStandardItem(file.isDirectory ? "目录" : "文件"));//这行代码添加"类型"的这一列单元格
rowItems.append(new QStandardItem(file.modificationTime.toLocalTime().toString("yyyy-MM-dd hh:mm")));
//file.modificationTime:存储文件的最后修改时间
//传递给 toString 的这个字符串 "yyyy-MM-dd hh:mm" 是一个格式化模板
rowItems.append(new QStandardItem(file.permissionsString));//权限,直接使用之前生成的权限字符串
parentItem->appendRow(rowItems);//将整行数据添加到模型,并最终触发界面更新的最关键一步。
//appendRow是QStandardItemModel的核心函数之一,用于在模型的末尾追加一个完整的新行。
}
//ui->messageLogTextEdit->append(QString("远程目录已列出,共 %1 项。").arg(files.count()));
//arg的作用:用一个具体的值来替换字符串的占位符%1
qDebug() << "[DEBUG] Loop finished. Parent item now has" << parentItem->rowCount() << "children AFTER adding new ones.";
qDebug() << "=======================================================";
}
//作用:当用户展开一个目录时,计算出这个目录的完整路径,然后发出请求去获取它的内容
void MainWindow::on_remoteTree_expanded(const QModelIndex &index)
{
QStandardItem *item = remoteModel->itemFromIndex(index);
if (!item || !item->hasChildren() || (item->child(0) && !item->child(0)->text().isEmpty())) {
// 如果没有子项,或者第一个子项不是空的占位符,说明已经加载过了,直接返回
return;
}
//item->removeRows(0, item->rowCount()); // 移除占位符
QStringList pathParts;
for (QModelIndex current = index; current.isValid(); current = current.parent()) {
pathParts.prepend(remoteModel->data(current, Qt::DisplayRole).toString());
}
QString path = "/" + pathParts.join('/');
ui->messageLogTextEdit->append(QString("请求列出目录 %1...").arg(path));
emit listDirectoryRequested(index, path);
}
//在ui显示中,让表格视图的根,指向树状视图中被点击的项
void MainWindow::on_remoteTree_clicked(const QModelIndex &index)
{
// 步骤 1: 无论如何,先立即将表格的根指向被点击的项。
// 这可能会导致表格暂时变为空白,但很快会被新加载的数据填充。
ui->remoteTable->setRootIndex(index);
// 步骤 2: 更新顶部的路径显示栏,提供UI反馈
QStringList pathParts;
for (QModelIndex current = index; current.isValid(); current = current.parent()) {
pathParts.prepend(remoteModel->data(current, Qt::DisplayRole).toString());
}
ui->remotePathComboBox->lineEdit()->setText("/" + pathParts.join('/'));
// 步骤 3: 检查被点击的项是否是一个“尚未加载内容的目录”
QStandardItem *item = remoteModel->itemFromIndex(index);
if (!item) return;
// 判断条件:
// 1. 它有子项 (item->hasChildren())
// 2. 并且它的第一个子项是我们的空占位符 (item->child(0)->text().isEmpty())
// 如果满足这两个条件,说明这是一个需要加载内容的目录。
if (item->hasChildren() && item->child(0) && item->child(0)->text().isEmpty()) {
// 直接调用我们已经写好的“展开”函数,来触发数据加载流程!
on_remoteTree_expanded(index);
}
}
//逻辑:如果双击的是一个目录,就在树状图中展开它(这会自动触发我们之前写的 expanded 逻辑去加载数据);
//如果是个文件,我们就打印一条日志(为未来的下载功能做准备)
void MainWindow::handleRemoteDoubleClick(const QModelIndex &index)
{
QStandardItem *item = remoteModel->itemFromIndex(index);
if (!item) return;
if (item->hasChildren()) {
// 如果是目录,逻辑不变:展开并进入
ui->remoteTree->expand(index);
ui->remoteTable->setRootIndex(index);
} else {
// 如果是文件,执行下载逻辑
// 步骤 1: 获取远程文件的完整路径 (不变)
QStringList pathParts;
for (QModelIndex current = index; current.isValid(); current = current.parent()) {
pathParts.prepend(remoteModel->data(current, Qt::DisplayRole).toString());
}
QString remotePath = "/" + pathParts.join('/');
// 步骤 2: (修复) 获取当前本地视图所在的路径,而不是写死桌面
QString currentLocalDir = ui->localPathComboBox->lineEdit()->text();
if (currentLocalDir.isEmpty()) {
ui->messageLogTextEdit->append("错误:未能确定本地保存路径,请先在本地站点选择一个目录。");
return;
}
QString localPath = QDir(currentLocalDir).filePath(item->text());
// 步骤 3: 在日志中给出提示并发射信号 (不变)
ui->messageLogTextEdit->append(QString("请求下载文件 %1 到 %2").arg(remotePath).arg(localPath));
emit downloadFileRequested(remotePath, localPath);
}
}
void MainWindow::onTransferProgress(qint64 bytesTransferred, qint64 totalBytes)
{
// 暂时只打印进度到调试输出
qDebug() << "Download progress:" << bytesTransferred << "/" << totalBytes;
}
void MainWindow::handleLocalDoubleClick(const QModelIndex &index)
{
// 从模型中获取被双击项的文件信息
QFileInfo fileInfo = localModel->fileInfo(index);
// 只有当双击的是一个文件(而不是目录)时,才执行上传逻辑
if (fileInfo.isFile()) {
// 获取当前远程路径 (我们需要知道上传到哪里)
// 注意:我们需要一个方法来获取当前远程路径,暂时先用一个占位符
// 我们将在下一步完善它
QString currentRemotePath = ui->remotePathComboBox->lineEdit()->text();
if (currentRemotePath.isEmpty()) {
ui->messageLogTextEdit->append("错误:未能确定远程路径,请先在远程站点导航。");
return;
}
QString localPath = fileInfo.filePath();
QString remotePath = QDir(currentRemotePath).filePath(fileInfo.fileName());
// 在日志中给出提示并发射上传请求信号
ui->messageLogTextEdit->append(QString("请求上传文件 %1 到 %2").arg(localPath).arg(remotePath));
emit uploadFileRequested(localPath, remotePath);
}
}
void MainWindow::onUploadFinished(const QString &localPath, const QString &remotePath, qint64 size, bool success, const QString &errorMessage)
{
addTransferLogEntry(localPath, remotePath, "-->", size, success, errorMessage);
if (success) {
ui->messageLogTextEdit->append(QString("文件 %1 上传成功!").arg(QFileInfo(localPath).fileName()));
// 新增:自动刷新当前远程目录
emit listDirectoryRequested(QModelIndex(), ui->remotePathComboBox->lineEdit()->text());
} else {
ui->messageLogTextEdit->append(QString("文件 %1 上传失败: %2").arg(QFileInfo(localPath).fileName()).arg(errorMessage));
}
}
void MainWindow::onDownloadFinished(const QString &remotePath, const QString &localPath, qint64 size, bool success, const QString &errorMessage)
{
// 使用收到的完整 localPath 和 remotePath 调用辅助函数
addTransferLogEntry(localPath, remotePath, "<--", size, success, errorMessage);
}
void MainWindow::addTransferLogEntry(const QString &localPath, const QString &remotePath, const QString &direction, qint64 size, bool success, const QString &message)
{
QList<QStandardItem*> rowItems;
if (success) {
rowItems.append(new QStandardItem(localPath));
rowItems.append(new QStandardItem(direction));
rowItems.append(new QStandardItem(remotePath));
rowItems.append(new QStandardItem(QString::number(size)));
rowItems.append(new QStandardItem("成功"));
successfulTransfersModel->appendRow(rowItems);
} else {
rowItems.append(new QStandardItem(localPath));
rowItems.append(new QStandardItem(direction));
rowItems.append(new QStandardItem(remotePath));
rowItems.append(new QStandardItem(QString::number(size)));
rowItems.append(new QStandardItem(message));
failedTransfersModel->appendRow(rowItems);
}
}
void MainWindow::onDeleteActionTriggered()
{
// 1. 获取当前在远程视图中选中的项
QModelIndex currentIndex = ui->remoteTree->selectionModel()->currentIndex();
if (!currentIndex.isValid()) {
// 如果没有选中任何项,则什么也不做
return;
}
// 2. 重建选中项的完整远程路径
QStringList pathParts;
for (QModelIndex current = currentIndex; current.isValid(); current = current.parent()) {
pathParts.prepend(remoteModel->data(current, Qt::DisplayRole).toString());
}
QString remotePath = "/" + pathParts.join('/');
QString fileName = remoteModel->itemFromIndex(currentIndex)->text();
// 3. 弹出确认对话框,防止误删
QMessageBox::StandardButton reply;
reply = QMessageBox::question(this, "确认删除", QString("您确定要永久删除远程文件或目录 '%1' 吗?").arg(fileName),
QMessageBox::Yes|QMessageBox::No);
// 4. 如果用户确认,则发射删除请求信号
if (reply == QMessageBox::Yes) {
ui->messageLogTextEdit->append(QString("请求删除: %1").arg(remotePath));
emit deleteFileRequested(remotePath);
}
}
void MainWindow::onRemoteFileDeleted(const QString &remotePath, bool success, const QString &errorMessage)
{
if (success) {
ui->messageLogTextEdit->append(QString("成功删除: %1").arg(remotePath));
// 新增:自动刷新当前远程目录
emit listDirectoryRequested(QModelIndex(), ui->remotePathComboBox->lineEdit()->text());
} else {
ui->messageLogTextEdit->append(QString("删除失败: %1, 原因: %2").arg(remotePath).arg(errorMessage));
}
}
void MainWindow::onRefreshActionTriggered()
{
QString currentRemotePath = ui->remotePathComboBox->lineEdit()->text();
// 确保我们已经连接并且在一个有效的目录中
if (currentRemotePath.isEmpty()) {
return;
}
ui->messageLogTextEdit->append(QString("正在刷新: %1...").arg(currentRemotePath));
// 发射我们早已创建好的信号来请求刷新
emit listDirectoryRequested(QModelIndex(), currentRemotePath);
}
//站点管理器
void MainWindow::onSiteManagerActionTriggered()
{
SiteManagerDialog dialog(this); // 创建对话框实例
// 新增:将对话框的 connectSiteRequested 信号连接到主窗口的新槽上
connect(&dialog, &SiteManagerDialog::connectSiteRequested, this, &MainWindow::onConnectFromSiteManager);
dialog.exec(); // 以模态方式显示对话框,意味着在关闭它之前无法操作主窗口
}
void MainWindow::onConnectFromSiteManager(const QVariantMap &siteData)
{
// 1. 从 siteData 中“解包”出连接信息
QString host = siteData.value("host").toString();
int port = siteData.value("port").toInt();
QString user = siteData.value("user").toString();
QString pass = siteData.value("pass").toString();
// 2. (可选)将这些信息更新到快速连接栏,方便用户查看
findChild<QLineEdit*>("hostLineEdit")->setText(host);
findChild<QLineEdit*>("portLineEdit")->setText(QString::number(port));
findChild<QLineEdit*>("userLineEdit")->setText(user);
findChild<QLineEdit*>("passLineEdit")->setText(pass);
// 3. 发射主连接信号,启动连接流程
emit connectRequested(host, port, user, pass);
}