diff --git a/Database/Task.h b/Database/Task.h index d6dafaa..1951f55 100644 --- a/Database/Task.h +++ b/Database/Task.h @@ -13,7 +13,7 @@ public: bool finished = false; std::chrono::system_clock::time_point createTime; std::string content; - std::string comment; + std::string remark; Wt::Dbo::ptr parent; Tasks children; @@ -21,7 +21,7 @@ public: template void persist(Action &a) { Wt::Dbo::field(a, content, "content"); - Wt::Dbo::field(a, comment, "comment"); + Wt::Dbo::field(a, remark, "remark"); Wt::Dbo::field(a, finished, "finished"); Wt::Dbo::field(a, createTime, "create_time"); diff --git a/Server/Application.cpp b/Server/Application.cpp index 481d5f4..0b37d86 100644 --- a/Server/Application.cpp +++ b/Server/Application.cpp @@ -104,7 +104,7 @@ Application::Application(const std::string &path) : ApplicationSettings(path), m auto task = std::make_unique(); task->createTime = system_clock::time_point(seconds(root.at("createTime").as_int64())); task->content = content; - task->comment = std::string(root.at("comment").as_string()); + task->remark = std::string(root.at("comment").as_string()); auto t = database->add(std::move(task)); Wt::Dbo::ptr parent = database->find("where id=?").bind(root.at("parentId").as_int64()); if (parent) { diff --git a/UnitTest/DatabaseTest.cpp b/UnitTest/DatabaseTest.cpp index 6576082..eb69c55 100644 --- a/UnitTest/DatabaseTest.cpp +++ b/UnitTest/DatabaseTest.cpp @@ -6,20 +6,20 @@ using namespace std::chrono; -BOOST_AUTO_TEST_CASE(DatabaseTest) { +BOOST_AUTO_TEST_CASE(DatabaseTest) { auto session = Database::session(); Wt::Dbo ::Transaction transaction(*session); auto task = std::make_unique(); - task->comment = "my_comment"; + task->remark = "my_comment"; task->content = "my_content"; auto p = session->add(std::move(task)); { task = std::make_unique(); - task->comment = "my_comment1"; + task->remark = "my_comment1"; task->content = "my_content1"; auto c = session->add(std::move(task)); p.modify()->children.insert(c); @@ -27,14 +27,14 @@ BOOST_AUTO_TEST_CASE(DatabaseTest) { { task = std::make_unique(); - task->comment = "my_comment2"; + task->remark = "my_comment2"; task->content = "my_content2"; auto c = session->add(std::move(task)); p.modify()->children.insert(c); { task = std::make_unique(); - task->comment = "my_comment3"; + task->remark = "my_comment3"; task->content = "my_content3"; auto d = session->add(std::move(task)); c.modify()->children.insert(d); @@ -42,7 +42,7 @@ BOOST_AUTO_TEST_CASE(DatabaseTest) { { task = std::make_unique(); - task->comment = "my_comment4"; + task->remark = "my_comment4"; task->content = "my_content4"; auto d = session->add(std::move(task)); c.modify()->children.insert(d); @@ -51,6 +51,7 @@ BOOST_AUTO_TEST_CASE(DatabaseTest) { Wt::Dbo::ptr tt = session->find("where id = 3"); LOG(info) << tt->parent->content; + BOOST_CHECK_EQUAL(tt.id(), 3); LOG(info) << tt->children.size(); { diff --git a/WebApplication/Application.cpp b/WebApplication/Application.cpp index 26e717f..fdbb1ec 100644 --- a/WebApplication/Application.cpp +++ b/WebApplication/Application.cpp @@ -7,6 +7,7 @@ #include "NavigationBar.h" #include "RedirectPage.h" #include "Restful.h" +#include "TaskPage.h" #include "VisitorRecordsPage.h" #include "WebRTCClientPage.h" #include "model/AuthModel.h" @@ -50,6 +51,7 @@ Application::Application(const Wt::WEnvironment &env, bool embedded) m_root = root()->addNew(); m_root->addStyleClass("bulma-container bulma-is-flex-grow-1 bulma-is-flex"); } else { + m_extern = true; std::unique_ptr topPtr = std::make_unique(); m_root = topPtr.get(); const std::string *div = env.getParameter("div"); @@ -208,6 +210,9 @@ void Application::handlePathChange(const std::string &path) { } else if (path.starts_with("/wt/webrtc")) { m_root->clear(); auto p = m_root->addNew(); + } else if (path.starts_with("/wt/task")) { + m_root->clear(); + m_root->addNew(); } else { m_root->clear(); m_root->addNew(); @@ -215,6 +220,10 @@ void Application::handlePathChange(const std::string &path) { } } +bool Application::inDocusaurus() const { + return m_extern; +} + Server::Server(uint16_t port, const std::string &applicationRoot, const std::string &documentRoot) { try { std::vector args; diff --git a/WebApplication/Application.h b/WebApplication/Application.h index fc81ca3..74f9acf 100644 --- a/WebApplication/Application.h +++ b/WebApplication/Application.h @@ -1,11 +1,11 @@ #ifndef __WEBAPPLICATION_H__ #define __WEBAPPLICATION_H__ +#include "Database/User.h" #include "Singleton.h" #include #include #include -#include "Database/User.h" namespace Wt { class WServer; @@ -30,6 +30,7 @@ class Application : public Wt::WApplication { public: Application(const Wt::WEnvironment &env, bool embedded); ~Application(); + bool inDocusaurus() const; protected: void authEvent(); @@ -37,6 +38,7 @@ protected: private: std::unique_ptr m_session; + bool m_extern = false; Wt::JSignal<> m_startup; Wt::WContainerWidget *m_root = nullptr; diff --git a/WebApplication/CMakeLists.txt b/WebApplication/CMakeLists.txt index a9fbc84..9c961a6 100644 --- a/WebApplication/CMakeLists.txt +++ b/WebApplication/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(WebApplication RegistrationPage.h RegistrationPage.cpp NavigationBar.h NavigationBar.cpp RedirectPage.h RedirectPage.cpp + TaskPage.h TaskPage.cpp VisitorRecordsPage.h VisitorRecordsPage.cpp WebRTCClientPage.h WebRTCClientPage.cpp Restful.h Restful.cpp diff --git a/WebApplication/HomePage.cpp b/WebApplication/HomePage.cpp index 1c79c87..6970e4d 100644 --- a/WebApplication/HomePage.cpp +++ b/WebApplication/HomePage.cpp @@ -15,6 +15,10 @@ HomePage::HomePage() { li->setHtmlTagName("li"); li->addNew(Wt::WLink(Wt::LinkType::InternalPath, "/wt/login"), "登录页面"); + li = ul->addNew(); + li->setHtmlTagName("li"); + li->addNew(Wt::WLink(Wt::LinkType::InternalPath, "/wt/task"), "任务清单"); + li = ul->addNew(); li->setHtmlTagName("li"); li->addNew(Wt::WLink(Wt::LinkType::InternalPath, "/wt/visitor/analysis"), "访客数据"); diff --git a/WebApplication/TaskPage.cpp b/WebApplication/TaskPage.cpp new file mode 100644 index 0000000..d7b919b --- /dev/null +++ b/WebApplication/TaskPage.cpp @@ -0,0 +1,199 @@ +#include "TaskPage.h" +#include "Application.h" +#include "BoostLog.h" +#include "Database/Session.h" +#include +#include +#include +#include +#include +#include +#include +#include + +TaskPage::TaskPage() : Wt::WTemplate(tr("Wt.Task.Home")) { + using namespace Wt; + WApplication *app = WApplication::instance(); + app->messageResourceBundle().use(app->appRoot() + "webrtc"); + addStyleClass("bulma-is-flex-grow-1 bulma-is-flex bulma-is-flex-direction-column"); + + auto addButton = bindNew("add-new", "创建"); + addButton->clicked().connect(this, [this]() { showAddTaskDialog(-1); }); + update(); +} + +std::unique_ptr TaskPage::createTask(const Wt::Dbo::ptr &task) { + auto ret = std::make_unique(tr("Wt.Task.Item")); + auto app = dynamic_cast(Wt::WApplication::instance()); + if (app->inDocusaurus()) { + ret->addStyleClass("li-none-style"); + } + ret->bindString("content", task->content); + ret->bindString("remark", task->remark); + ret->setHtmlTagName("li"); + + auto checkbox = ret->bindNew("finished-checkbox"); + checkbox->setChecked(task->finished); + checkbox->clicked().connect(ret.get(), [this, id = task.id(), checkbox]() { + { + auto session = Database::session(); + Wt::Dbo::Transaction transaction(*session); + Wt::Dbo::ptr task = session->find().where("id = ?").bind(id); + if (task) { + task.modify()->finished = checkbox->isChecked(); + } + } + update(); + }); + + auto addButton = ret->bindNew("add-child", tr("Wt.Icon.Add"), Wt::TextFormat::XHTML); + addButton->clicked().connect(ret.get(), [id = task.id(), this]() { showAddTaskDialog(id); }); + + auto removeButton = ret->bindNew("remove-button", tr("Wt.Icon.Remove"), Wt::TextFormat::XHTML); + removeButton->clicked().connect(ret.get(), [this, id = task.id()]() { + Wt::StandardButton result = Wt::WMessageBox::show("删除任务", "是否删除任务?子任务也将一起删除", + Wt::StandardButton::Ok | Wt::StandardButton::Cancel); + if (result == Wt::StandardButton::Ok) { + { + auto session = Database::session(); + Wt::Dbo::Transaction transaction(*session); + Wt::Dbo::ptr task = session->find().where("id = ?").bind(id); + if (task) { + removeTask(task); + } + } + update(); + } + }); + + auto collapseButton = ret->bindNew("collapse-button", tr("Wt.Icon.Expand"), Wt::TextFormat::XHTML); + collapseButton->setCheckable(true); + if (task->children.empty()) { + ret->bindEmpty("children"); + collapseButton->addStyleClass("bulma-is-invisible"); + } else { + auto list = ret->bindNew("children"); + list->setList(true); + for (auto child : task->children) { + list->addWidget(createTask(child)); + } + collapseButton->clicked().connect(ret.get(), [collapseButton, list]() { + collapseButton->setText(collapseButton->isChecked() ? tr("Wt.Icon.Fold") : tr("Wt.Icon.Expand")); + if (collapseButton->isChecked()) { + list->addStyleClass("bulma-is-hidden"); + } else { + list->removeStyleClass("bulma-is-hidden"); + } + }); + } + return ret; +} + +class AddTaskModel : public Wt::WFormModel { +public: + static constexpr Field ContentField = "task-content"; + static constexpr Field RemarkField = "task-remark"; + AddTaskModel() : Wt::WFormModel() { + addField(ContentField); + addField(RemarkField); + } + Task task() { + Task t; + t.content = Wt::asString(value(ContentField)).toUTF8(); + t.remark = Wt::asString(value(RemarkField)).toUTF8(); + return t; + } +}; + +class AddTaskView : public Wt::WTemplateFormView { +public: + AddTaskView(const Wt::WString &text) : Wt::WTemplateFormView(text), m_model{std::make_shared()} { + setFormWidget(AddTaskModel::ContentField, std::make_unique()); + + auto remark = std::make_unique(); + remark->setColumns(40); + remark->setRows(5); + setFormWidget(AddTaskModel::RemarkField, std::move(remark)); + + auto button = bindWidget("submit-button", std::make_unique("确定")); + button->clicked().connect(this, &AddTaskView::process); + + updateView(m_model.get()); + } + + Wt::Signal &accepted() { + return m_accepted; + } + + std::unique_ptr task() const { + return std::make_unique(m_model->task()); + } + +protected: + void process() { + updateModel(m_model.get()); + if (m_model->validate()) { + m_accepted.emit(m_model->task()); + // updateView(m_model.get()); + } else { + LOG(error) << "validate failed."; + updateView(m_model.get()); + } + } + +private: + Wt::Signal m_accepted; + std::shared_ptr m_model; +}; + +void TaskPage::showAddTaskDialog(int64_t parentId) { + auto d = std::make_unique("添加任务"); + d->setMovable(false); + auto content = d->contents()->addNew(tr("Wt.Task.Add")); + auto cancelButton = content->bindNew("cancel-button", "取消"); + cancelButton->clicked().connect(content, [dialog = d.get()]() { dialog->reject(); }); + content->accepted().connect(this, [dialog = d.get()](const Task &task) { dialog->accept(); }); + auto code = d->exec(); + if (code == Wt::DialogCode::Accepted) { + auto task = content->task(); + auto session = Database::session(); + if (session) { + Wt::Dbo::Transaction transaction(*session); + Wt::Dbo::ptr parent; + if (parentId >= 0) { + parent = session->find(std::format("where id = {}", parentId)); + } + if (parent) { + task->parent = parent; + } + session->add(std::move(task)); + } + update(); + } +} + +void TaskPage::update() { + auto session = Database::session(); + Wt::Dbo::Transaction transaction(*session); + Tasks tasks = session->find("WHERE parent_id IS NULL"); + if (tasks.empty()) { + bindEmpty("tasks"); + return; + } else { + auto list = bindNew("tasks"); + list->setList(true); + + for (auto &task : tasks) { + list->addWidget(createTask(task)); + } + } +} + +void TaskPage::removeTask(Wt::Dbo::ptr task) { + if (!task->children.empty()) { + for (auto child : task->children) { + removeTask(child); + } + } + task.remove(); +} \ No newline at end of file diff --git a/WebApplication/TaskPage.h b/WebApplication/TaskPage.h new file mode 100644 index 0000000..e52c4fd --- /dev/null +++ b/WebApplication/TaskPage.h @@ -0,0 +1,20 @@ +#ifndef __TASKPAGE_H__ +#define __TASKPAGE_H__ + +#include +#include + +class Task; + +class TaskPage : public Wt::WTemplate { +public: + TaskPage(); + +protected: + void update(); + void showAddTaskDialog(int64_t parentId = -1); + std::unique_ptr createTask(const Wt::Dbo::ptr &task); + void removeTask(Wt::Dbo::ptr task); +}; + +#endif // __TASKPAGE_H__ \ No newline at end of file diff --git a/resources/app.css b/resources/app.css index e252402..8c6fa5e 100644 --- a/resources/app.css +++ b/resources/app.css @@ -3,6 +3,16 @@ body { height: 100%; } +ul li ul li { + margin-left: 2.5rem +} + +.li-none-style { + list-style: none; + margin: 0; + padding: 0; +} + .Wt-itemview .Wt-headerdiv { overflow: hidden; -webkit-user-select: none; diff --git a/resources/webrtc.xml b/resources/webrtc.xml index 618faa0..a46c920 100644 --- a/resources/webrtc.xml +++ b/resources/webrtc.xml @@ -26,4 +26,75 @@ + +

任务清单

+
+ ${add-new class="bulma-button bulma-is-success"} +
+ ${tasks} +
+ +
+ ${finished-checkbox} +
+
${content}
+
${remark}
+
+
+ ${remove-button class="bulma-button bulma-is-danger"} + ${add-child class="bulma-button bulma-is-success"} + ${collapse-button class="bulma-button"} +
+
+ ${children} +
+ +
+ +
+ ${task-content class="bulma-input" type="text"} + + + +
+ ${task-content-info class="Wt-info bulma-help"} +
+
+ +
+ ${task-remark class="bulma-textarea"} +
+ ${task-remark-info class="Wt-info bulma-help"} +
+
+ ${submit-button class="bulma-button"} + ${cancel-button class="bulma-button"} +
+
+ + + + + + + + + + + + + + + + + + + + + 内容 + 备注 \ No newline at end of file