diff --git a/AicsKnowledgeBase/NoteListWindow.qml b/AicsKnowledgeBase/NoteListWindow.qml new file mode 100644 index 0000000..5560aee --- /dev/null +++ b/AicsKnowledgeBase/NoteListWindow.qml @@ -0,0 +1,5 @@ +import QtQuick + +Item { + +} diff --git a/AicsKnowledgeBase/qml/LoginWindow.qml b/AicsKnowledgeBase/qml/LoginWindow.qml index 07735c4..9ff5ccd 100644 --- a/AicsKnowledgeBase/qml/LoginWindow.qml +++ b/AicsKnowledgeBase/qml/LoginWindow.qml @@ -93,8 +93,8 @@ AppFluWindow { text: "登录" onClicked: { var param = { - "username": account.text, - "password": password.text + "username": "admin", + "password": "123456" } btn_login.disabled = true btn_login.text = "登录中" @@ -108,8 +108,8 @@ AppFluWindow { }, function (p1, p2) { console.log(p1) console.log(p2) - FluApp.navigate("/") - window.close() + //FluApp.navigate("/") + //window.close() btn_login.disabled = false btn_login.text = "登录" }) diff --git a/AicsKnowledgeBase/qml/MainWindow.qml b/AicsKnowledgeBase/qml/MainWindow.qml index 49db787..7e1eb3c 100644 --- a/AicsKnowledgeBase/qml/MainWindow.qml +++ b/AicsKnowledgeBase/qml/MainWindow.qml @@ -96,20 +96,6 @@ FluWindow { title_bar.maximizeButton.visible = true // window.backgroundVisible = false window.show() - - Request.post( - "knowledge/file", "{\n" - + " \"name\": \"特东局却热\",\n" + " \"brief\": \"fugiat occaecat\",\n" - + " \"size\": 88,\n" + " \"sha256\": \"ea incididunt pariatur\",\n" - + " \"tags\": [\n" + " 43,\n" - + " 45\n" + " ],\n" + " \"parentId\": \"4\"\n" + "}", - function (p1, p2) { - console.log(p1) - console.log(p2) - }, function (p1, p2) { - console.log(p1) - console.log(p2) - }) } } } diff --git a/AicsKnowledgeBase/qml/component/FileList.qml b/AicsKnowledgeBase/qml/component/FileList.qml index d1fdb2a..e992e3e 100644 --- a/AicsKnowledgeBase/qml/component/FileList.qml +++ b/AicsKnowledgeBase/qml/component/FileList.qml @@ -1,8 +1,10 @@ import QtQuick 2.15 import QtQuick.Layouts import FluentUI +import QtQuick.Dialogs import "qrc:///AicsKnowledgeBase/qml/global" import SignalFileOperation 1.0 +import AicsKB.FileTransferManager Item { anchors.fill: parent @@ -34,11 +36,74 @@ Item { Component { id: fileListItemHeader - Item { - id: fileListItemHeaderItem - width: ListView.view.width - height: 48 + ColumnLayout { + function add(folderName, uuid) { + console.log(header.items) + header.items = header.items.concat([{ + "title": folderName, + "uuid": uuid + }]) + } + FluButton { + Layout.alignment: Qt.AlignRight + text: "上传" + onClicked: function () { + console.log("click") + fileDialog.open() + } + FileDialog { + id: fileDialog + onAccepted: function () { + let name = FileTransferManager.getFileName(selectedFile) + const size = FileTransferManager.getFileSize( + selectedFile) + const md5 = FileTransferManager.getFileMd5(selectedFile) + if (size <= 0 || md5 === '') + return + var body = { + "name": name, + "brief": "brief", + "size": size, + "md5": md5, + "tags": [], + "parentId": header.items.length + !== 0 ? header.items[header.items.length - 1].uuid : null + } + console.log("begin") + console.log(JSON.stringify(body)) + Request.post("knowledge/file", JSON.stringify(body), + function (res, data) { + console.log(res) + console.log(data) + FileTransferManager.upload( + selectedFile, data.id, + data.ticket, name) + }, function (res, data) { + console.log(res) + }) + } + } + } RowLayout { + id: fileListItemHeaderItem + width: ListView.view.width + height: 48 + + // back to folder button + FluIconButton { + Layout.alignment: Qt.AlignVCenter + id: backButton + width: 24 + height: 24 + iconSource: FluentIcons.ForwardCall + onClicked: { + if (header.count() > 0) { + header.items = header.items.slice( + 0, header.count() - 1) + } + fileListItemHeaderItem.update() + } + } FluBreadcrumbBar { id: header width: parent.width @@ -54,80 +119,77 @@ Item { fileListItemHeaderItem.update() } } - // back to folder button - FluIconButton { - Layout.alignment: Qt.AlignVCenter - id: backButton - width: 24 - height: 24 - iconSource: FluentIcons.ChromeBack - onClicked: { - if (header.count() > 0) { - header.items = header.items.slice( - 0, header.count() - 1) - } - fileListItemHeaderItem.update() - } + + function getType(suffix) { + if (suffix === "ppt" || suffix === "pptx") + return "PPT" + else if (suffix === "doc" || suffix === "docx") + return "WORD" + else if (suffix === "pdf") + return "PDF" + else if (suffix === "txt") + return "TXT" + else if (suffix === "xls" || suffix === "xlsx") + return "EXCEL" + else if (suffix === "zip" || suffix === "rar") + return "ZIP" + else if (suffix === "png" || suffix === "jpg" + || suffix === "jpeg" || suffix === "gif") + return "IMAGE" + else if (suffix === "mp3" || suffix === "wav") + return "AUDIO" + else if (suffix === "mp4" || suffix === "avi" + || suffix === "rmvb" || suffix === "rm" + || suffix === "wmv" || suffix === "mkv") + return "VIDEO" + else + return "OTHER" } - } - function add(folderName, uuid) { - console.log(header.items) - header.items = header.items.concat([{ - "title": folderName, - "uuid": uuid - }]) - } - - function getType(suffix) { - if (suffix === "ppt" || suffix === "pptx") - return "PPT" - else if (suffix === "doc" || suffix === "docx") - return "WORD" - } - - function update() { - var uuid = header.items.length - === 0 ? "null" : header.items[header.items.length - 1].uuid - Request.get(uuid, function (response) { - var data = JSON.parse(response) - console.log(data.knowledgeFileAttribute) - fileListModel.clear() - var files = data.children - for (var i = 0; i < files.length; i++) { - var file = files[i] - console.log(file.name) - var modelItem = { - "title": file.name, - "uuid": file.id, - "date": file.createTime - } - if (file.knowledgeFileAttribute === null) { - modelItem.type = "folder" - modelItem.isDir = true - modelItem.size = 0 - } else { - modelItem.isDir = false - modelItem.type = getType( - file.knowledgeFileAttribute.suffix) - modelItem.size = file.knowledgeFileAttribute.size - modelItem.brief = file.knowledgeFileAttribute.brief - modelItem.pageView = file.knowledgeFileAttribute.pageView - modelItem.stars = 0 - // merge file.knowledgeFileAttribute.tags array to a string - var tagString = "" - for (var j = 0; j < file.knowledgeFileAttribute.tags.length; j++) { - if (j != 0) - tagString = tagString + "," - tagString = tagString + file.knowledgeFileAttribute.tags[j].name + function update() { + var uuid = header.items.length + === 0 ? "null" : header.items[header.items.length - 1].uuid + Request.get("/knowledge/" + uuid, function (response) { + var data = JSON.parse(response) + console.log(data.knowledgeFileAttribute) + fileListModel.clear() + var files = data.children + for (var i = 0; i < files.length; i++) { + var file = files[i] + console.log(file.name) + var modelItem = { + "title": file.name, + "uuid": file.id, + "date"// cut time after 'T' + : file.createTime.substring(0, 10) } - modelItem.tags = tagString + if (file.knowledgeFileAttribute === null) { + modelItem.type = "folder" + modelItem.isDir = true + modelItem.size = 0 + } else { + modelItem.isDir = false + modelItem.type = getType( + file.knowledgeFileAttribute.suffix) + modelItem.size = file.knowledgeFileAttribute.size + modelItem.brief = file.knowledgeFileAttribute.brief + modelItem.pageView = file.knowledgeFileAttribute.pageView + modelItem.stars = 0 + // merge file.knowledgeFileAttribute.tags array to a string + var tagString = "" + for (var j = 0; j < file.knowledgeFileAttribute.tags.length; j++) { + if (j != 0) + tagString = tagString + "," + tagString = tagString + file.knowledgeFileAttribute.tags[j].name + } + modelItem.tags = tagString + } + fileListModel.append(modelItem) } - fileListModel.append(modelItem) - } - console.log(fileListModel) - listView.currentIndex = -1 - }) + console.log(fileListModel) + listView.currentIndex = -1 + }) + } } } } @@ -152,8 +214,9 @@ Item { size: isDir ? 0 : model.size stars: isDir ? 0 : model.stars // split string to array - tags: isDir ? [] : model.tags.length === 0 ? [] : model.tags.split( - ",") + tags: isDir ? [] : model.tags === null + || model.tags === undefined + || model.tags === "" ? [] : model.tags.split(",") onTagClicked: { emit: search(tag) console.log(tag) @@ -162,6 +225,7 @@ Item { function doubleClicked() { listView.currentIndex = index if (model.isDir) { + console.log(listView.headerItem) listView.headerItem.add(model.title, model.uuid) } else { emit: SignalFileOperation.open(model.uuid) diff --git a/AicsKnowledgeBase/qml/component/FileListItem.qml b/AicsKnowledgeBase/qml/component/FileListItem.qml index 8b8d5e4..7516c72 100644 --- a/AicsKnowledgeBase/qml/component/FileListItem.qml +++ b/AicsKnowledgeBase/qml/component/FileListItem.qml @@ -39,11 +39,13 @@ FluArea { Image { id: icon source: type ? "qrc:/AicsKnowledgeBase/res/" + type + ".png" : "" - Layout.preferredHeight: 18 - Layout.preferredWidth: 18 + Layout.preferredHeight: 32 + Layout.preferredWidth: 32 } FluText { id: title + font.bold: true + font.pointSize: 15 text: fileItem.title } } @@ -52,17 +54,23 @@ FluArea { id: brief visible: !fileItem.isDir text: fileItem.brief + Layout.fillWidth: true + wrapMode: Text.WrapAnywhere + elide: Text.ElideRight + maximumLineCount: 2 } RowLayout { id: infoRow visible: !fileItem.isDir FluText { id: date + color: "#5F5F5F" text: fileItem.date } FluText { id: size // cast Byte size to right text size + color: "#5F5F5F" text: fileItem.size > 1024 * 1024 ? (fileItem.size / 1024 / 1024).toFixed( 2) + "MB" : (fileItem.size / 1024).toFixed( @@ -70,10 +78,12 @@ FluArea { } FluText { id: pageView + color: "#5F5F5F" text: fileItem.pageView + "浏览" } FluText { id: stars + color: "#5F5F5F" text: fileItem.stars + "收藏" } } diff --git a/AicsKnowledgeBase/qml/component/NoteList.qml b/AicsKnowledgeBase/qml/component/NoteList.qml index c5010c1..539c917 100644 --- a/AicsKnowledgeBase/qml/component/NoteList.qml +++ b/AicsKnowledgeBase/qml/component/NoteList.qml @@ -6,6 +6,46 @@ import "qrc:///AicsKnowledgeBase/qml/global" Item { anchors.fill: parent signal open(string note) + signal createNote + width: parent.width - 8 + property ListModel noteListModel: ListModel { + ListElement { + uuid: "1" + title: "超级无敌报错" + brief: "file:///D:/academic/2023-qtBig/AicsKnowledgeBase_client/AicsKnowledgeBase/qml/component/NoteList.qml:41:21: Unable to assign [undefined] to QString" + author: "admin" + pageView: 123 + stars: 27 + date: "2022-02-02" + } + ListElement { + uuid: "2" + title: "Qt布局" + brief: "锚定(anchors)在确定父子组件之间,同级组件之间的相对位置时非常常用,若使用锚定方式确定子组件与父组件之间的位置关系,使用 top,bottom,left,right, topMargin,bottomMargin,leftMargin,rightMargin进行上下左右对齐,以及对齐后的留白距离。若要使子组件在父组件的水平,垂直居中,使用:" + author: "超级无敌长的账户名" + pageView: 123 + stars: 27 + date: "2022-02-02" + } + ListElement { + uuid: "2" + title: "Qt布局" + brief: "锚定(anchors)在确定父子组件之间,同级组件之间的相对位置时非常常用,若使用锚定方式确定子组件与父组件之间的位置关系,使用 top,bottom,left,right, topMargin,bottomMargin,leftMargin,rightMargin进行上下左右对齐,以及对齐后的留白距离。若要使子组件在父组件的水平,垂直居中,使用:" + author: "超级无敌长的账户名" + pageView: 123 + stars: 27 + date: "2022-02-02" + } + ListElement { + uuid: "2" + title: "Qt布局" + brief: "锚定(anchors)在确定父子组件之间,同级组件之间的相对位置时非常常用,若使用锚定方式确定子组件与父组件之间的位置关系,使用 top,bottom,left,right, topMargin,bottomMargin,leftMargin,rightMargin进行上下左右对齐,以及对齐后的留白距离。若要使子组件在父组件的水平,垂直居中,使用:" + author: "超级无敌长的账户名" + pageView: 123 + stars: 27 + date: "2022-02-02" + } + } ListView { id: listView anchors.fill: parent @@ -18,11 +58,24 @@ Item { id: noteListItemHeader Item { id: noteListItemHeaderItem - // width: ListView.view.width - // height: 48 - // FluText { - // text: "笔记" - // } + width: ListView.view.width + height: 48 + RowLayout { + width: parent.width + FluText { + text: "笔记列表" + font.pixelSize: 18 + font.bold: true + } + FluButton { + id: createNoteButton + text: "新建笔记" + Layout.alignment: Qt.AlignRight + onClicked: { + createNote() + } + } + } } } Component { @@ -54,25 +107,4 @@ Item { function setListModel(listModel) { listView.model = listModel } - ListModel { - id: noteListModel - ListElement { - uuid: "1" - title: "超级无敌报错" - brief: "file:///D:/academic/2023-qtBig/AicsKnowledgeBase_client/AicsKnowledgeBase/qml/component/NoteList.qml:41:21: Unable to assign [undefined] to QString" - author: "admin" - pageView: 123 - stars: 27 - date: "2022-02-02" - } - ListElement { - uuid: "2" - title: "Qt布局" - brief: "锚定(anchors)在确定父子组件之间,同级组件之间的相对位置时非常常用,若使用锚定方式确定子组件与父组件之间的位置关系,使用 top,bottom,left,right, topMargin,bottomMargin,leftMargin,rightMargin进行上下左右对齐,以及对齐后的留白距离。若要使子组件在父组件的水平,垂直居中,使用:" - author: "超级无敌长的账户名" - pageView: 123 - stars: 27 - date: "2022-02-02" - } - } } diff --git a/AicsKnowledgeBase/qml/component/TransferListPopup.qml b/AicsKnowledgeBase/qml/component/TransferListPopup.qml index dece83f..bd3c66e 100644 --- a/AicsKnowledgeBase/qml/component/TransferListPopup.qml +++ b/AicsKnowledgeBase/qml/component/TransferListPopup.qml @@ -2,7 +2,9 @@ import QtQuick import QtQuick.Controls import QtQuick.Window import QtQuick.Layouts +import Qt5Compat.GraphicalEffects import FluentUI +import AicsKB.FileTransferManager Popup { id: transfer_popup @@ -21,21 +23,99 @@ Popup { anchors.topMargin: 10 ColumnLayout { Layout.fillWidth: true - Layout.bottomMargin: -10 + spacing: 0 Repeater { model: FileTransferListModel - delegate: ColumnLayout { - Layout.bottomMargin: 10 - spacing: 10 - width: parent.width - Text { - text: name + delegate: Item { + Layout.fillWidth: true + height: 50 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 5 + spacing: 2 + Text { + text: name + } + + FluProgressBar { + Layout.fillWidth: true + progress: completedSize / totalSize + indeterminate: false + } + + Text { + color: FluColors.Grey130 + text: formatSize(speed) + "/s - " + formatSize( + completedSize) + "/" + formatSize( + totalSize) + + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @param {Number} size 文件大小 + * @param {Number} [pointLength=1] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。 + * 如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + */ + function formatSize(size, pointLength, units) { + var unit + units = units || ['B', 'KB', 'MB', 'GB', 'TB'] + while ((unit = units.shift()) && size > 1024) { + size = size / 1024 + } + return (unit === 'B' ? size : size.toFixed( + pointLength === undefined ? 1 : pointLength)) + ' ' + unit + } + } } - FluProgressBar { - Layout.fillWidth: true - progress: completedSize / totalSize - indeterminate: false + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: file_buttons.visible = true + onExited: file_buttons.visible = false + + LinearGradient { + anchors.fill: file_buttons + visible: file_buttons.visible + + start: Qt.point(0, 0) + end: Qt.point(20, 0) + gradient: Gradient { + GradientStop { + position: 0.0 + color: "transparent" + } + GradientStop { + position: 1.0 + color: "#f8f8f8" + } + } + } + RowLayout { + id: file_buttons + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + layoutDirection: Qt.RightToLeft + visible: false + + FluIconButton { + iconSource: FluentIcons.Cancel + } + + FluIconButton { + Layout.leftMargin: 20 + iconSource: paused ? FluentIcons.Play : FluentIcons.Pause + onClicked: { + if (!paused) { + console.log("pause") + FileTransferManager.pause(fileId) + } + } + } + } } } } diff --git a/AicsKnowledgeBase/qml/global/Request.qml b/AicsKnowledgeBase/qml/global/Request.qml index e8539d9..29fae57 100644 --- a/AicsKnowledgeBase/qml/global/Request.qml +++ b/AicsKnowledgeBase/qml/global/Request.qml @@ -5,7 +5,7 @@ import QtQuick QtObject { id: request - property string baseUrl: "http://127.0.0.1:4523/m1/2914957-0-5604d062/" + property string baseUrl: "https://api.hammer-hfut.tk:233/aics/main/" //property string baseUrl: "http://192.168.156.74:8080/" // GET diff --git a/AicsKnowledgeBase/qml/page/ContentPage.qml b/AicsKnowledgeBase/qml/page/ContentPage.qml index aec6663..fccba38 100644 --- a/AicsKnowledgeBase/qml/page/ContentPage.qml +++ b/AicsKnowledgeBase/qml/page/ContentPage.qml @@ -8,8 +8,34 @@ import FluentUI import AicsKB.FileTransferManager import SignalFileOperation 1.0 import "qrc:///AicsKnowledgeBase/qml/page" +import "qrc:///AicsKnowledgeBase/qml/component" FluArea { + Popup { + id: popup + modal: true //模态, 为 true后弹出窗口会叠加一个独特的背景调光效果 + focus: true //焦点, 当弹出窗口实际接收到焦点时,activeFocus将为真 + padding: 0 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + property var raiseItem: null + background: Rectangle { + color: Qt.rgba(0, 0, 0, 0) //背景为无色 + } + FluArea { + width: 350 + height: 500 + backgroundColor: "#f9f9f9" + NoteList { + id: noteList + onOpen: function handle(noteId) { + console.log("open note: " + noteId) + } + onCreateNote: function handle() { + console.log("create note") + } + } + } + } id: content_area paddings: 0 backgroundColor: "#f9f9f9" @@ -76,6 +102,28 @@ FluArea { bold: true } } + ColumnLayout { + id: layout_note + FluIconButton { + id: button_note + iconSize: 15 + iconSource: FluentIcons.QuickNote + text: "笔记" + onClicked: { + popup.open() + } + } + FluText { + text: "" + font.pointSize: 8 + Layout.alignment: button_note.Center + Layout.topMargin: -5 + } + anchors { + verticalCenter: text_title.verticalCenter + right: parent.right + } + } ColumnLayout { id: layout_share FluIconButton { @@ -93,7 +141,7 @@ FluArea { } anchors { verticalCenter: text_title.verticalCenter - right: parent.right + right: layout_note.left } } ColumnLayout { @@ -145,7 +193,9 @@ FluArea { } onClicked: { emit: content_area.download(content_area.knowledgeFileId) - FileTransferManager.download(content_area.knowledgeFileId) + // FileTransferManager.download(content_area.knowledgeFileId) + FileTransferManager.download( + "4973c59e-3ba1-41f2-a57e-3b53c2b5e2a4") } } } @@ -155,6 +205,7 @@ FluArea { implicitHeight: 100 ColumnLayout { RowLayout { + width: parent.width FluText { padding: 10 text: content_page.publishTime.toDateString() @@ -216,7 +267,6 @@ FluArea { Layout.fillWidth: true implicitHeight: 400 } - } Component { id: other_view @@ -231,28 +281,28 @@ FluArea { } } -// Item { -// Layout.fillWidth: true -// implicitHeight: 50 -// FluText { -// id: text_note -// text: "笔记" -// padding: 10 -// font { -// pointSize: 15 -// bold: true -// } -// } -// FluTextButton { -// id: button_publish -// text: "发布笔记" -// hoverColor: "blue" -// normalColor: "black" -// anchors { -// verticalCenter: text_note.verticalCenter -// right: parent.right -// } -// } -// } + // Item { + // Layout.fillWidth: true + // implicitHeight: 50 + // FluText { + // id: text_note + // text: "笔记" + // padding: 10 + // font { + // pointSize: 15 + // bold: true + // } + // } + // FluTextButton { + // id: button_publish + // text: "发布笔记" + // hoverColor: "blue" + // normalColor: "black" + // anchors { + // verticalCenter: text_note.verticalCenter + // right: parent.right + // } + // } + // } } } diff --git a/AicsKnowledgeBase/qml/page/FilePage.qml b/AicsKnowledgeBase/qml/page/FilePage.qml index 1d86c48..57675d7 100644 --- a/AicsKnowledgeBase/qml/page/FilePage.qml +++ b/AicsKnowledgeBase/qml/page/FilePage.qml @@ -5,8 +5,8 @@ import QtQuick.Controls import QtQuick.Controls.Basic import QtQuick.Dialogs import FluentUI -import AicsKB.FileTransferManager import "qrc:///AicsKnowledgeBase/qml/component" +import "qrc:///AicsKnowledgeBase/qml/global" FluArea { property string url: '' @@ -22,22 +22,8 @@ FluArea { text: "" } - FluButton { - text: "上传" - onClicked: function () { - console.log("click") - fileDialog.open() - } - - FileDialog { - id: fileDialog - onAccepted: function () { - FileTransferManager.upload(selectedFile) - } - } - } - FileList { autoRequest: true + width: parent.width } } diff --git a/AicsKnowledgeBase/res/AUDIO.png b/AicsKnowledgeBase/res/AUDIO.png new file mode 100644 index 0000000..1cc2039 Binary files /dev/null and b/AicsKnowledgeBase/res/AUDIO.png differ diff --git a/AicsKnowledgeBase/res/OTHER.png b/AicsKnowledgeBase/res/OTHER.png new file mode 100644 index 0000000..54a10d1 Binary files /dev/null and b/AicsKnowledgeBase/res/OTHER.png differ diff --git a/AicsKnowledgeBase/res/note.png b/AicsKnowledgeBase/res/note.png new file mode 100644 index 0000000..348ce77 Binary files /dev/null and b/AicsKnowledgeBase/res/note.png differ diff --git a/AicsKnowledgeBase/src/FileTransferListModel.cpp b/AicsKnowledgeBase/src/FileTransferListModel.cpp index 663eb4b..6cb46eb 100644 --- a/AicsKnowledgeBase/src/FileTransferListModel.cpp +++ b/AicsKnowledgeBase/src/FileTransferListModel.cpp @@ -3,18 +3,13 @@ FileTransferListModel::FileTransferListModel(QObject *parent) : QAbstractListModel(parent) { + m_roleName.insert(kDownloadRole, "download"); m_roleName.insert(kIdRole, "fileId"); m_roleName.insert(kNameRole, "name"); m_roleName.insert(kCompletedSizeRole, "completedSize"); m_roleName.insert(kTotalSizeRole, "totalSize"); - - m_data = { - {"v", "vivo", 1200, 2000}, - {"m", "meizu", 1300, 1500}, - {"a", "apple", 1400, 1400}, - }; - - + m_roleName.insert(kSpeedRole, "speed"); + m_roleName.insert(kPausedRole, "paused"); } @@ -34,6 +29,8 @@ QVariant FileTransferListModel::data(const QModelIndex &index, int role) const return QVariant(); switch (role) { + case kDownloadRole: + return m_data.value(index.row()).download; case kIdRole: return m_data.value(index.row()).id; case kNameRole: @@ -42,6 +39,10 @@ QVariant FileTransferListModel::data(const QModelIndex &index, int role) const return m_data.value(index.row()).completedSize; case kTotalSizeRole: return m_data.value(index.row()).totalSize; + case kSpeedRole: + return m_data.value(index.row()).speed; + case kPausedRole: + return m_data.value(index.row()).paused; default: break; } diff --git a/AicsKnowledgeBase/src/FileTransferListModel.h b/AicsKnowledgeBase/src/FileTransferListModel.h index dfb14fd..593ba04 100644 --- a/AicsKnowledgeBase/src/FileTransferListModel.h +++ b/AicsKnowledgeBase/src/FileTransferListModel.h @@ -7,10 +7,13 @@ struct FileItem { + bool download = true; QString id; QString name; int64_t completedSize; int64_t totalSize; + int64_t speed; + bool paused = false; }; @@ -36,10 +39,13 @@ public: enum FileItemRole { - kIdRole = Qt::UserRole + 1, + kDownloadRole = Qt::UserRole + 1, + kIdRole, kNameRole, kCompletedSizeRole, - kTotalSizeRole + kTotalSizeRole, + kSpeedRole, + kPausedRole }; private: explicit FileTransferListModel(QObject *parent = nullptr); diff --git a/AicsKnowledgeBase/src/FileTransferManager.cpp b/AicsKnowledgeBase/src/FileTransferManager.cpp index a2ba674..2f68638 100644 --- a/AicsKnowledgeBase/src/FileTransferManager.cpp +++ b/AicsKnowledgeBase/src/FileTransferManager.cpp @@ -9,7 +9,91 @@ #include #include -static const std::string baseUrl = "http://127.0.0.1:4523/m1/2914957-0-default/"; +static const std::string baseUrl = "https://api.hammer-hfut.tk:233/aics/file/"; +//static const std::string baseUrl = "http://127.0.0.1:4523/m1/2914957-0-6e5f2db1/"; +//static const std::string baseUrl = "https://quic.nginx.org/"; + +struct FileDownloading : FileItem +{ + CURL *curlHandle = nullptr; + std::ofstream file; +}; +std::unordered_map downloadingMap; +std::mutex downloadingMapMutex; + +struct FileUploading : FileItem +{ + CURL *curlHandle = nullptr; + std::ifstream file; +}; + +std::unordered_map uploadingMap; +std::mutex uploadingMapMutex; + +size_t writeDataFunc(const void *ptr, size_t size, size_t nmemb, void *obj) +{ + auto buf = (FileDownloading *) obj; + auto writeSize = size * nmemb; + buf->file.write(reinterpret_cast(ptr), writeSize); + return writeSize; +} + +size_t readDataFunc(void *ptr, size_t size, size_t nmemb, void *obj) +{ + auto buf = (FileUploading *) obj; + auto readSize = size * nmemb; + buf->file.read(reinterpret_cast(ptr), readSize); + return readSize; +} + +static int xferinfo(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) +{ + auto fileTrans = (FileDownloading *) p; + curl_off_t speed = 0; + + curl_easy_getinfo(fileTrans->curlHandle, CURLINFO_SPEED_DOWNLOAD_T, &speed); + + fileTrans->completedSize += dlnow; + + fileTrans->speed = speed; + qDebug() << std::format("Downloading: {} / {}, Speed: {}", fileTrans->completedSize, fileTrans->totalSize, + speed).c_str(); + + auto item = static_cast(*fileTrans); + QTimer::singleShot(0, qApp, [item]() { + FileTransferListModel::instance().setItem(item); + }); + + if (fileTrans->paused) { + qDebug() << "Pause"; + return 1; + } + fileTrans->completedSize -= dlnow; + return 0; +} + +static int uploadInfo(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) +{ + auto fileTrans = (FileUploading *) p; + curl_off_t speed = 0; + curl_easy_getinfo(fileTrans->curlHandle, CURLINFO_SPEED_UPLOAD_T, &speed); + fileTrans->completedSize += ulnow; + fileTrans->speed = speed; + qDebug() << std::format("Uploading: {} / {}, Speed: {}", fileTrans->completedSize, fileTrans->totalSize, + speed).c_str(); + + auto item = static_cast(*fileTrans); + QTimer::singleShot(0, qApp, [item]() { + FileTransferListModel::instance().setItem(item); + }); + + if (fileTrans->paused) { + qDebug() << "Pause"; + return 1; + } + fileTrans->completedSize -= ulnow; + return 0; +} static size_t writeDataCallback(void *contents, size_t size, size_t nmemb, void *userp) { @@ -28,24 +112,26 @@ static size_t writeDataCallback(void *contents, size_t size, size_t nmemb, void return 0; } -CURLcode httpGet(const std::string &url, std::string &response, int timeout = 3000) +CURLcode +httpGet(const std::string &url, std::string &response, long httpVersion = CURL_HTTP_VERSION_3ONLY, int timeout = 3000) { CURLcode res; // 获取easy handle CURL *easy_handle = curl_easy_init(); - if (nullptr == easy_handle) { - curl_global_cleanup(); - return CURLE_FAILED_INIT; - } + if (!easy_handle) return CURLE_FAILED_INIT; + // 设置easy handle属性 - //curl_easy_setopt(easy_handle, CURLOPT_HTTP_VERSION, (long) CURL_HTTP_VERSION_3ONLY); + curl_easy_setopt(easy_handle, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(easy_handle, CURLOPT_HTTP_VERSION, httpVersion); curl_easy_setopt(easy_handle, CURLOPT_URL, (baseUrl + url).c_str()); curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, writeDataCallback); curl_easy_setopt(easy_handle, CURLOPT_TIMEOUT, timeout); curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, (void *) &response); curl_easy_setopt(easy_handle, CURLOPT_FOLLOWLOCATION, 1L); //curl_easy_setopt(easy_handle, CURLOPT_DEFAULT_PROTOCOL, "https"); + //curl_easy_setopt(easy_handle, CURLOPT_SSL_VERIFYPEER, 0L); + //curl_easy_setopt(easy_handle, CURLOPT_SSL_VERIFYHOST, 0L);//不验证证书和HOST // 执行数据请求 res = curl_easy_perform(easy_handle); curl_easy_cleanup(easy_handle); @@ -55,55 +141,92 @@ CURLcode httpGet(const std::string &url, std::string &response, int timeout = 30 FileTransferManager::FileTransferManager(QObject *parent) : QObject(parent) { - + std::string resData; + if (CURLE_OK == httpGet("hello", resData)) + qDebug() << "HTTP3 OK\n" << resData.c_str(); + else + qDebug() << "HTTP3 FAILED"; } - -void FileTransferManager::upload(QUrl fileUrl) +void +FileTransferManager::upload(const QUrl &fileUrl, const QString &fileId, const QString &ticket, const QString &fileName) { qDebug() << fileUrl.fileName(); + QtConcurrent::run([fileUrl, fileId, ticket, fileName] { - CURL *curl; - CURLcode res; - curl = curl_easy_init(); - if (curl) { - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_URL, "http://127.0.0.1:4523/m1/2914957-0-default/"); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_DEFAULT_PROTOCOL, "https"); - struct curl_slist *headers = NULL; - headers = curl_slist_append(headers, "User-Agent: Apifox/1.0.0 (https://apifox.com)"); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_mime *mime; - curl_mimepart *part; - mime = curl_mime_init(curl); - part = curl_mime_addpart(mime); - curl_mime_name(part, "ticket"); - curl_mime_data(part, "", CURL_ZERO_TERMINATED); - part = curl_mime_addpart(mime); - curl_mime_name(part, "rangeStart"); - curl_mime_data(part, "", CURL_ZERO_TERMINATED); - part = curl_mime_addpart(mime); - curl_mime_name(part, "rangeEnd"); - curl_mime_data(part, "", CURL_ZERO_TERMINATED); - part = curl_mime_addpart(mime); - curl_mime_name(part, "data"); - curl_mime_filedata(part, ""); - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); - res = curl_easy_perform(curl); - curl_mime_free(mime); - } - curl_easy_cleanup(curl); + + auto fileSize = QFileInfo(fileUrl.toLocalFile()).size(); + + FileItem item{false, fileId, fileName, 0, fileSize}; + QTimer::singleShot(0, qApp, [item]() { + FileTransferListModel::instance().insertItem(item); + }); + + uploadingMapMutex.lock(); + auto [iter, _] = uploadingMap.emplace(item.id, (FileUploading) item); + uploadingMapMutex.unlock(); + + FileUploading &fileUploading = iter->second; + fileUploading.file = std::ifstream(fileUrl.toLocalFile().toLocal8Bit().toStdString(), std::ios::binary); + + if (!fileUploading.file.is_open()) { + qDebug() << "Open File Failed"; + return false; + } + + CURLcode res; + fileUploading.curlHandle = curl_easy_init(); + if (!fileUploading.curlHandle) + return false; + + curl_httppost *formpost = nullptr; + curl_httppost *lastptr = nullptr; + curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "ticket", + CURLFORM_COPYCONTENTS, ticket.toStdString().c_str(), + CURLFORM_END); + + curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "rangeStart", + CURLFORM_COPYCONTENTS, std::to_string(0).c_str(), + CURLFORM_END); + curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "rangeEnd", + CURLFORM_COPYCONTENTS, std::to_string(fileSize).c_str(), + CURLFORM_END); + + curl_formadd(&formpost, &lastptr, + CURLFORM_COPYNAME, "data", + CURLFORM_STREAM, &fileUploading, + CURLFORM_CONTENTLEN, fileSize, + CURLFORM_END); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_URL, (baseUrl + fileId.toStdString()).c_str()); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3ONLY); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_HTTPPOST, formpost); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_READFUNCTION, readDataFunc); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_NOPROGRESS, false); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_XFERINFOFUNCTION, uploadInfo); + curl_easy_setopt(fileUploading.curlHandle, CURLOPT_XFERINFODATA, &fileUploading); + + qDebug() << "Begin Upload"; + + res = curl_easy_perform(fileUploading.curlHandle); + if (res == CURLE_OK) + qDebug() << "Upload Success"; + else + qDebug() << "Upload Failed"; + curl_easy_cleanup(fileUploading.curlHandle); + curl_formfree(formpost); + fileUploading.file.close(); + + uploadingMapMutex.lock(); + uploadingMap.erase(fileId); + uploadingMapMutex.unlock(); + + }); } -struct FileTransfering : FileItem -{ - CURL *curlHandle = nullptr; - std::ofstream file; -}; - curl_off_t getHttpFileSize(const std::string &url) { curl_off_t fileLength = 0; @@ -112,6 +235,7 @@ curl_off_t getHttpFileSize(const std::string &url) curl_easy_setopt(handle, CURLOPT_URL, url.c_str()); curl_easy_setopt(handle, CURLOPT_HEADER, 0); //只需要header头 curl_easy_setopt(handle, CURLOPT_NOBODY, 1); //不需要body + curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); if (curl_easy_perform(handle) == CURLE_OK) curl_easy_getinfo(handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &fileLength); @@ -123,73 +247,143 @@ curl_off_t getHttpFileSize(const std::string &url) return fileLength; } -size_t receiveDataFunc(const void *ptr, size_t size, size_t nmemb, void *obj) -{ - auto buf = (FileTransfering *) obj; - auto writeSize = size * nmemb; - buf->file.write(reinterpret_cast(ptr), writeSize); - buf->completedSize += writeSize; - qDebug() << std::format("Downloading: {} / {}", buf->completedSize, buf->totalSize).c_str(); - - QTimer::singleShot(0, qApp, [buf]() { - FileTransferListModel::instance().setItem(static_cast(*buf)); - }); - - return writeSize; -} - bool httpDownload(const std::string &url, const FileItem &item) { auto fileSize = getHttpFileSize(url);//获得文件大小。 - if (fileSize != -1) { - qDebug() << "FileSize: " << fileSize; - - FileTransfering obj = (FileTransfering) item; - - obj.curlHandle = curl_easy_init(); - if (!obj.curlHandle) - return false; - obj.totalSize = fileSize;//原始文件大小 - obj.file = std::ofstream("D:\\Downloads\\" + obj.name.toStdString() + ".zip", std::ios::binary); - - if (!obj.file.is_open()) { - qDebug() << "Open File Failed"; - curl_easy_cleanup(obj.curlHandle); - return false; - } else { - curl_easy_setopt(obj.curlHandle, CURLOPT_VERBOSE, 0L); - curl_easy_setopt(obj.curlHandle, CURLOPT_HEADER, 0); - curl_easy_setopt(obj.curlHandle, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(obj.curlHandle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(obj.curlHandle, CURLOPT_URL, url.c_str()); - curl_easy_setopt(obj.curlHandle, CURLOPT_WRITEDATA, (void *) &obj); - curl_easy_setopt(obj.curlHandle, CURLOPT_WRITEFUNCTION, receiveDataFunc); - - //下载 - auto res = curl_easy_perform(obj.curlHandle); - curl_easy_cleanup(obj.curlHandle); - obj.file.close(); - return res == CURLE_OK && obj.completedSize == obj.totalSize; - } - } else + if (fileSize == -1) return false; + + qDebug() << "FileSize: " << fileSize; + + downloadingMapMutex.lock(); + auto [iter, _] = downloadingMap.emplace(item.id, (FileDownloading) item); + downloadingMapMutex.unlock(); + + FileDownloading &obj = iter->second; + + obj.curlHandle = curl_easy_init(); + if (!obj.curlHandle) + return false; + obj.totalSize = fileSize;//原始文件大小 + obj.file = std::ofstream("D:\\Downloads\\" + obj.name.toStdString() + ".zip", std::ios::binary); + + if (!obj.file.is_open()) { + qDebug() << "Open File Failed"; + curl_easy_cleanup(obj.curlHandle); + return false; + } + + curl_easy_setopt(obj.curlHandle, CURLOPT_VERBOSE, 0L); + curl_easy_setopt(obj.curlHandle, CURLOPT_HEADER, 0); + curl_easy_setopt(obj.curlHandle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(obj.curlHandle, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(obj.curlHandle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(obj.curlHandle, CURLOPT_WRITEDATA, (void *) &obj); + curl_easy_setopt(obj.curlHandle, CURLOPT_WRITEFUNCTION, writeDataFunc); + curl_easy_setopt(obj.curlHandle, CURLOPT_NOPROGRESS, false);//设为false 下面才能设置进度响应函数 + curl_easy_setopt(obj.curlHandle, CURLOPT_XFERINFOFUNCTION, xferinfo); + curl_easy_setopt(obj.curlHandle, CURLOPT_XFERINFODATA, &obj); + + //下载 + auto res = curl_easy_perform(obj.curlHandle); + curl_easy_cleanup(obj.curlHandle); + obj.file.close(); + + downloadingMapMutex.lock(); + downloadingMap.erase(obj.id); + downloadingMapMutex.unlock(); + + return res == CURLE_OK; } void FileTransferManager::download(QString fileId) { static int _fileId = 0; - fileId = QString::number(_fileId++); + //fileId = QString::number(_fileId++); QtConcurrent::run([fileId, this] { qDebug() << "Start Get"; std::string resData; - if (CURLE_OK != httpGet(fileId.toStdString() + "/status", resData)) + if (CURLE_OK != httpGet("File/" + fileId.toStdString() + "/status", resData)) return; qDebug() << resData.c_str(); auto resJson = QJsonDocument::fromJson(resData.c_str()); //if(!resJson["isCompleted"].toBool()) + return; + + + +/* int size = resJson["size"].toInt(); + FileItem item{fileId, resJson["md5"].toString(), 0, size}; + QTimer::singleShot(0, qApp, [this, item]() { + FileTransferListModel::instance().insertItem(item); + });*/ + + auto fileUrl = "https://curl.se/download/curl-8.1.2.zip"; + + +/* curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Range: bytes=0-"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);*/ + + + int64_t size = getHttpFileSize(fileUrl); + FileItem item{true, fileId, fileId, 0, size}; + + auto res = httpDownload(fileUrl, item); + + + qDebug() << "End Get" << res; + }); + + +} + +void FileTransferManager::pause(const QString &fileId) +{ + downloadingMapMutex.lock(); + downloadingMap[fileId].paused = true; + downloadingMapMutex.unlock(); +} + +QString FileTransferManager::getFileMd5(const QUrl &fileUrl) +{ + QFile sourceFile = fileUrl.toLocalFile(); + + if (sourceFile.open(QIODevice::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Md5); + QByteArray buffer; + while ((buffer = sourceFile.read(10240)).size() > 0) { + hash.addData(buffer); + } + + sourceFile.close(); + return {hash.result().toHex()}; + } + return {}; +} + +int64_t FileTransferManager::getFileSize(const QUrl &fileUrl) +{ + return QFile(fileUrl.toLocalFile()).size(); +} + +void FileTransferManager::resume(const QString &fileId) +{ + + QtConcurrent::run([fileId, this] { + qDebug() << "Start Get"; + + std::string resData; + if (CURLE_OK != httpGet("File/" + fileId.toStdString() + "/status", resData, CURL_HTTP_VERSION_1_1)) + return; + + qDebug() << resData.c_str(); + return; + auto resJson = QJsonDocument::fromJson(resData.c_str()); + //if(!resJson["isCompleted"].toBool()) // return; @@ -202,8 +396,14 @@ void FileTransferManager::download(QString fileId) auto fileUrl = "https://curl.se/download/curl-8.1.2.zip"; + +/* curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Range: bytes=0-"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);*/ + + int64_t size = getHttpFileSize(fileUrl); - FileItem item{fileId, resJson["md5"].toString(), 0, size}; + FileItem item{true, fileId, fileId, 0, size}; QTimer::singleShot(0, qApp, [this, item]() { FileTransferListModel::instance().insertItem(item); }); @@ -211,20 +411,13 @@ void FileTransferManager::download(QString fileId) auto res = httpDownload(fileUrl, item); - while (item.completedSize < item.totalSize && false) { - item.completedSize += 1; - QTimer::singleShot(0, qApp, [this, item]() { - FileTransferListModel::instance().setItem(item); - }); - - using namespace std::chrono_literals; - std::this_thread::sleep_for(100ms); - } - - qDebug() << "End Get" << res; }); - +} + +QString FileTransferManager::getFileName(const QUrl &fileUrl) +{ + return fileUrl.fileName(); } diff --git a/AicsKnowledgeBase/src/FileTransferManager.h b/AicsKnowledgeBase/src/FileTransferManager.h index e7096f9..256aec0 100644 --- a/AicsKnowledgeBase/src/FileTransferManager.h +++ b/AicsKnowledgeBase/src/FileTransferManager.h @@ -8,17 +8,27 @@ #include #include +#include #include "FileTransferListModel.h" + + class FileTransferManager : public QObject { Q_OBJECT public: explicit FileTransferManager(QObject *parent = nullptr); - Q_INVOKABLE void upload(QUrl fileUrl); + Q_INVOKABLE QString getFileName(const QUrl& fileUrl); + Q_INVOKABLE int64_t getFileSize(const QUrl& fileUrl); + Q_INVOKABLE QString getFileMd5(const QUrl& fileUrl); + Q_INVOKABLE void upload(const QUrl& fileUrl, const QString& fileId, const QString& ticket, const QString &fileName); Q_INVOKABLE void download(QString fileId); + Q_INVOKABLE void pause(const QString& fileId); + Q_INVOKABLE void resume(const QString& fileId); + +private: };