diff --git a/README.md b/README.md index ca813df..4a53c05 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ # 部分效果预览 +## 内置一个ChatGPT聊天Demo + +![](doc/preview/chatpgt.png) + ## 各种Button按钮 ![](doc/preview/buttons.png) diff --git a/doc/preview/chatgpt.png b/doc/preview/chatgpt.png new file mode 100644 index 0000000..d93f8c6 Binary files /dev/null and b/doc/preview/chatgpt.png differ diff --git a/example/App.qml b/example/App.qml index 3d380c0..fa690bb 100644 --- a/example/App.qml +++ b/example/App.qml @@ -15,6 +15,7 @@ Window { "/":"qrc:/page/MainPage.qml", "/about":"qrc:/page/AboutPage.qml", "/login":"qrc:/page/LoginPage.qml", + "/chat":"qrc:/page/ChatPage.qml", } FluApp.initialRoute = "/" FluApp.run() diff --git a/example/ChatController.cpp b/example/ChatController.cpp new file mode 100644 index 0000000..2e728cd --- /dev/null +++ b/example/ChatController.cpp @@ -0,0 +1,41 @@ +#include "ChatController.h" + +ChatController::ChatController(QObject *parent) + : QObject{parent} +{ + isLoading(false); + networkManager = new QNetworkAccessManager(this); +} + +void ChatController::sendMessage(const QString& text){ + isLoading(true); + QUrl apiUrl("https://api.openai.com/v1/engines/text-davinci-003/completions"); + QNetworkRequest request(apiUrl); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", "Bearer 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"); + QJsonObject requestData; + requestData.insert("prompt", text); + requestData.insert("max_tokens", 1000); + requestData.insert("temperature", 0.5); + QJsonDocument requestDoc(requestData); + QByteArray requestDataBytes = requestDoc.toJson(); + QNetworkReply* reply = networkManager->post(request, requestDataBytes); + connect(reply, &QNetworkReply::finished,this, [=]() { + QString responseString = QString::fromUtf8(reply->readAll()); + qDebug() << responseString; + QJsonDocument doc = QJsonDocument::fromJson(responseString.toUtf8()); + QJsonObject jsonObj = doc.object(); + QString text = jsonObj.value("choices").toArray().at(0).toObject().value("text").toString(); + if(text.isEmpty()){ + text = "不好意思,我似乎听不懂您的意思"; + } + responseData(text); + reply->deleteLater(); + isLoading(false); + }); + connect(reply, QOverload::of(&QNetworkReply::errorOccurred), this, [=](QNetworkReply::NetworkError) { + qDebug() << "Network error occurred: " << reply->errorString(); + reply->deleteLater(); + isLoading(false); + }); +} diff --git a/example/ChatController.h b/example/ChatController.h new file mode 100644 index 0000000..acfa73c --- /dev/null +++ b/example/ChatController.h @@ -0,0 +1,27 @@ +#ifndef CHATCONTROLLER_H +#define CHATCONTROLLER_H + +#include +#include +#include +#include +#include +#include +#include +#include "stdafx.h" + +class ChatController : public QObject +{ + Q_OBJECT + Q_PROPERTY_AUTO(bool,isLoading) + Q_PROPERTY_AUTO(QString,responseData); +public: + explicit ChatController(QObject *parent = nullptr); + + Q_INVOKABLE void sendMessage(const QString& text); + +private: + QNetworkAccessManager* networkManager; +}; + +#endif // CHATCONTROLLER_H diff --git a/example/example.pro b/example/example.pro index d97d9c2..6766eb9 100644 --- a/example/example.pro +++ b/example/example.pro @@ -1,9 +1,10 @@ -QT += quick concurrent +QT += quick concurrent network CONFIG += c++11 DEFINES += QT_DEPRECATED_WARNINGS QT_NO_WARNING_OUTPUT SOURCES += \ + ChatController.cpp \ main.cpp RESOURCES += qml.qrc @@ -24,3 +25,6 @@ CONFIG(debug,debug|release) { } else { DESTDIR = $$absolute_path($${_PRO_FILE_PWD_}/../bin/release) } + +HEADERS += \ + ChatController.h diff --git a/example/main.cpp b/example/main.cpp index b6935d8..d020b00 100644 --- a/example/main.cpp +++ b/example/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include "ChatController.h" QMap properties(){ QMap map; @@ -20,6 +21,9 @@ int main(int argc, char *argv[]) // QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software); QGuiApplication app(argc, argv); QQmlApplicationEngine engine; + + qmlRegisterType("Controller",1,0,"ChatController"); + QMapIterator iterator(properties()); while (iterator.hasNext()) { iterator.next(); diff --git a/example/page/AboutPage.qml b/example/page/AboutPage.qml index 6ce269e..dd824d1 100644 --- a/example/page/AboutPage.qml +++ b/example/page/AboutPage.qml @@ -34,7 +34,7 @@ FluWindow { fontStyle: FluText.Title } FluText{ - text:"v1.0.9" + text:"v1.0.10" fontStyle: FluText.Body Layout.alignment: Qt.AlignBottom } diff --git a/example/page/ChatPage.qml b/example/page/ChatPage.qml new file mode 100644 index 0000000..5e3ead5 --- /dev/null +++ b/example/page/ChatPage.qml @@ -0,0 +1,230 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import FluentUI 1.0 +import Controller 1.0 + +FluWindow { + + width: 680 + height: 600 + minimumWidth: 500 + minimumHeight: 600 + + title:"ChatGPT" + + ChatController{ + id:controller + + onResponseDataChanged: { + appendMessage(false,responseData) + } + + } + + ListModel{ + id:model_message + ListElement{ + isMy:false + text:"欢迎使用ChatGPT" + } + ListElement{ + isMy:true + text:"好的,3Q" + } + } + + FluAppBar{ + id:appbar + title:"ChatGPT" + } + + Component{ + id:com_text + TextEdit { + text: modelData.text + wrapMode: Text.WrapAnywhere + readOnly: true + textFormat: Text.RichText + selectByMouse: true + selectByKeyboard: true + selectedTextColor: color + color:FluColors.Black + selectionColor: { + if(FluTheme.isDark){ + return FluTheme.primaryColor.lighter + }else{ + return FluTheme.primaryColor.dark + } + } + width: Math.min(list_message.width-200,600,implicitWidth) + } + } + + FluArea{ + id:layout_content + anchors{ + top: appbar.bottom + left: parent.left + right: parent.right + bottom: layout_bottom.top + margins: 10 + } + color: FluTheme.isDark ? Qt.rgba(39/255,39/255,39/255,1) : Qt.rgba(245/255,245/255,245/255,1) + + ListView{ + id:list_message + anchors.fill: parent + model:model_message + clip: true + ScrollBar.vertical: FluScrollBar {} + preferredHighlightBegin: 0 + preferredHighlightEnd: 0 + highlightMoveDuration: 0 + header:Item{ + width: list_message.width + height:20 + } + footer:Item{ + width: list_message.width + height:20 + } + delegate: Item{ + width: ListView.view.width + height: childrenRect.height + + FluRectangle{ + id:item_avatar + width: 30 + height: 30 + radius:[15,15,15,15] + anchors{ + right: isMy ? parent.right : undefined + rightMargin: isMy ? 20 : undefined + left: isMy ? undefined : parent.left + leftMargin: isMy ? undefined : 20 + top:parent.top + } + Image { + asynchronous: true + anchors.fill: parent + sourceSize: Qt.size(100,100) + source: isMy ? "qrc:/res/svg/avatar_2.svg" : "qrc:/res/image/logo_openai.png" + } + } + + Rectangle{ + id:item_layout_content + color: isMy ? "#FF95EC69" : "#FFFFFF" + width: item_msg_loader.width+10 + height: item_msg_loader.height+10 + radius: 3 + anchors{ + top: item_avatar.top + right: isMy ? item_avatar.left : undefined + rightMargin: isMy ? 10 : undefined + left: isMy ? undefined : item_avatar.right + leftMargin: isMy ? undefined : 10 + + } + + Loader{ + id:item_msg_loader + property var modelData: model + anchors.centerIn: parent + sourceComponent: com_text + } + } + + + Item{ + id:item_layout_bottom + width: parent.width + anchors.top: item_layout_content.bottom + height: 20 + } + } + } + } + + FluArea{ + id:layout_bottom + height: 90 + anchors{ + bottom: parent.bottom + bottomMargin: 10 + left: parent.left + right: parent.right + leftMargin: 10 + rightMargin: 10 + } + + + ScrollView{ + anchors{ + bottom: parent.bottom + left: parent.left + right: button_send.left + bottomMargin: 10 + leftMargin: 10 + rightMargin: 10 + } + height: Math.min(textbox.implicitHeight,64) + FluMultiLineTextBox{ + id:textbox + } + } + + FluFilledButton{ + id:button_send + text:controller.isLoading ? timer_loading.loadingText :"发送" + anchors{ + bottom: parent.bottom + right: parent.right + bottomMargin: 10 + rightMargin: 10 + } + width: 60 + disabled: controller.isLoading + onClicked:{ + var text = textbox.text + appendMessage(true,text) + controller.sendMessage(text) + textbox.clear() + } + + Timer{ + id:timer_loading + property int count : 0 + property string loadingText : "" + interval: 500 + running: controller.isLoading + repeat: true + onTriggered: { + switch(count%3){ + case 0: + loadingText = "." + break + case 1: + loadingText = ".." + break + case 2: + loadingText = "..." + break + default: + loadingText = "" + break + } + count++ + } + } + + } + } + + function appendMessage(isMy,text){ + model_message.append({isMy:isMy,text:text}) + list_message.positionViewAtEnd() + } + +} diff --git a/example/page/MainPage.qml b/example/page/MainPage.qml index dd3c7c5..d86aaf8 100644 --- a/example/page/MainPage.qml +++ b/example/page/MainPage.qml @@ -204,6 +204,35 @@ FluWindow { items:original_items footerItems:footer_items + actions:[ + Image { + width: 30 + height: 30 + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + sourceSize: Qt.size(60,60) + source: "qrc:/res/image/logo_openai.png" + Layout.rightMargin: 5 + MouseArea{ + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + FluApp.navigate("/chat") + } + } + }, + FluText{ + text:"夜间模式" + fontStyle: FluText.Body + }, + FluToggleSwitch{ + selected: FluTheme.isDark + clickFunc:function(){ + FluTheme.isDark = !FluTheme.isDark + } + } + ] + Component.onCompleted: { nav_view.setCurrentIndex(1) nav_view.push("qrc:/T_Buttons.qml") diff --git a/example/qml.qrc b/example/qml.qrc index 13c83eb..136840a 100644 --- a/example/qml.qrc +++ b/example/qml.qrc @@ -38,5 +38,7 @@ res/image/banner_1.jpg res/image/banner_2.jpg res/image/banner_3.jpg + res/image/logo_openai.png + page/ChatPage.qml diff --git a/example/res/image/logo_openai.png b/example/res/image/logo_openai.png new file mode 100644 index 0000000..58c06c5 Binary files /dev/null and b/example/res/image/logo_openai.png differ diff --git a/src/controls/FluNavigationView.qml b/src/controls/FluNavigationView.qml index 60c4680..7c50356 100644 --- a/src/controls/FluNavigationView.qml +++ b/src/controls/FluNavigationView.qml @@ -16,6 +16,8 @@ Item { property bool displaMinimalNav : false + property alias actions: layout_actions.data + onDisplayModeChanged: { if(displayMode === FluNavigationView.Minimal){ anim_navi.enabled = false @@ -172,22 +174,13 @@ Item { RowLayout{ + id:layout_actions anchors{ right: parent.right rightMargin: 14 verticalCenter: parent.verticalCenter } spacing: 5 - FluText{ - text:"夜间模式" - fontStyle: FluText.Body - } - FluToggleSwitch{ - selected: FluTheme.isDark - clickFunc:function(){ - FluTheme.isDark = !FluTheme.isDark - } - } } } @@ -377,11 +370,8 @@ Item { } } } - } - - function push(url){ nav_swipe.push(url) } diff --git a/src/controls/FluRectangle.qml b/src/controls/FluRectangle.qml index 4ddf836..76cb41a 100644 --- a/src/controls/FluRectangle.qml +++ b/src/controls/FluRectangle.qml @@ -6,8 +6,6 @@ Item{ id:root property var radius:[0,0,0,0] property color color : "#FFFFFF" - property color borderColor:"red" - property int borderWidth: 1 property bool shadow: true default property alias contentItem: container.data