diff --git a/devel/1008.md b/devel/1008.md new file mode 100644 index 0000000000..90427bb161 --- /dev/null +++ b/devel/1008.md @@ -0,0 +1,107 @@ +# [1008] 统一模板打开器重构 + +## 1 相关文档 +- [dddd.md](dddd.md) - 任务文档模板 + +## 2 任务相关的代码文件 +- `src/Plugins/Qt/QTMTemplateOpener.hpp` +- `src/Plugins/Qt/QTMTemplateOpener.cpp` +- `src/Plugins/Qt/QTMHomePage.cpp` +- `src/Plugins/Qt/QTMHomePage.hpp` +- `src/Plugins/Qt/QTMTemplatePage.cpp` +- `src/Plugins/Qt/QTMTemplatePage.hpp` +- `src/Plugins/Qt/qt_template_utils.hpp` +- `src/Plugins/Qt/qt_template_utils.cpp` + +## 3 如何测试 + +### 非确定性测试(文档验证) + +启动应用并验证以下功能: +1. HomePage 点击模板卡片能否正常打开模板 +2. TemplatePage 预览后点击打开能否正常工作 +3. 本地模板和远程模板的打开流程是否正确 +4. 下载进度对话框是否正常显示 +5. 取消下载后是否无错误提示 + + +## 4 如何提交 + +提交前执行以下最少步骤: + +```bash +# 代码格式化 +./bin/format + +# 编译验证 +xmake build +``` + +## 5 What + +将 `QTMHomePage` 和 `QTMTemplatePage` 中分散的模板打开逻辑抽取为独立的 `QTMTemplateOpener` 类,实现统一的模板打开流程。 + +1. 新增 `QTMTemplateOpener` 类(`QTMTemplateOpener.hpp/cpp`) + - 封装本地模板打开逻辑:复制到 Documents 并加载 + - 封装远程模板下载逻辑:显示进度对话框、同步下载、错误处理 + - 提供信号:`completed`、`failed`、`downloadProgress` + +2. 重构 `QTMHomePage` + - 删除 `createDocumentFromTemplate` 方法 + - 使用 `QTMTemplateOpener` 打开模板 + +3. 重构 `QTMTemplatePage` + - 删除重复的模板打开代码 + - 使用 `QTMTemplateOpener` 打开模板 + +4. 清理冗余 include + - 从 `QTMHomePage.cpp` 和 `QTMTemplatePage.cpp` 中移除不再需要的 `qt_template_utils.hpp` + +## 6 Why + +`QTMHomePage` 和 `QTMTemplatePage` 中各自维护了一套几乎相同的模板打开逻辑,存在以下问题: + +- **代码重复**:下载进度对话框、错误处理、本地/远程判断逻辑在两个类中重复实现 +- **维护困难**:修改模板打开流程需要在两个地方同步修改 +- **职责不单一**:页面类既负责 UI 展示又负责业务逻辑 + +通过抽取 `QTMTemplateOpener` 类,实现单一职责原则,提高代码复用性和可维护性。 + +## 7 How + +### 7.1 设计思路 + +采用 QObject 子类封装模板打开逻辑,通过信号通知调用方结果: + +``` +QTMTemplateOpener (QObject) +├── openTemplate(templateId) // 主入口 +├── isAvailableLocally(templateId) // 本地检查 +├── openLocalTemplate_() // 本地打开(私有) +├── startDownload_() // 远程下载(私有) +└── 信号: completed / failed / downloadProgress +``` + +### 7.2 同步下载处理 + +由于模板打开需要阻塞等待下载完成(同时保持 UI 响应),采用 `QProgressDialog` + `downloadTemplateSync` 的同步风格: + +1. 显示模态进度对话框 +2. 调用 `TemplateManager::downloadTemplateSync` 阻塞下载 +3. 通过信号更新进度对话框 +4. 下载完成后关闭对话框并打开模板 + +### 7.3 错误处理策略 + +- 本地模板不存在 → 显示错误 Toast,发射 `failed` 信号 +- 下载失败 → 显示错误 Toast(非用户取消时),发射 `failed` 信号 +- 用户取消下载 → 静默失败,发射空的 `failed` 信号 + +### 7.4 使用方式 + +```cpp +QTMTemplateOpener opener(this); +opener.openTemplate("template-id"); +``` + +调用方可以选择连接信号处理结果,也可以直接调用(同步风格,结果在返回前已确定)。 diff --git a/src/Plugins/Qt/QTMHomePage.cpp b/src/Plugins/Qt/QTMHomePage.cpp index 38b7a23caa..d34abd48ff 100644 --- a/src/Plugins/Qt/QTMHomePage.cpp +++ b/src/Plugins/Qt/QTMHomePage.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #include #include #include @@ -35,9 +34,9 @@ #include #include +#include "QTMTemplateOpener.hpp" #include "qt_dpi_utils.hpp" #include "qt_floating_toast.hpp" -#include "qt_template_utils.hpp" #include "qt_utilities.hpp" #include "s7_tm.hpp" #include "sys_utils.hpp" @@ -761,7 +760,8 @@ QTMHomePage::createDocumentWithStyle (const QString& styleId) { for (const auto& style : styles_) { if (style.id == styleId) { - createDocumentFromTemplate (style.templateId); + QTMTemplateOpener opener (this); + opener.openTemplate (style.templateId); return; } } @@ -769,75 +769,6 @@ QTMHomePage::createDocumentWithStyle (const QString& styleId) { qWarning () << "Invalid style ID:" << styleId; } -void -QTMHomePage::createDocumentFromTemplate (const QString& templateId) { - TemplateManager* mgr= TemplateManager::instance (); - if (!mgr) return; - - if (mgr->isTemplateAvailableLocally (templateId)) { - auto meta= mgr->templateById (templateId); - if (!meta) { - QtFloatingToast::showToast (this, - qt_translate ("Template metadata not found"), - 3000, QtFloatingToast::Error); - return; - } - QString localPath= mgr->localTemplatePath (templateId); - if (localPath.isEmpty ()) { - QtFloatingToast::showToast ( - this, qt_translate ("Local template file is missing"), 3000, - QtFloatingToast::Error); - return; - } - qt_copy_template_and_load (this, localPath, meta->name); - return; - } - - QProgressDialog dialog (qt_translate ("Downloading template..."), - qt_translate ("Cancel"), 0, 100, this); - dialog.setWindowModality (Qt::WindowModal); - dialog.setAutoClose (true); - - bool cancelledByUser= false; - connect (&dialog, &QProgressDialog::canceled, [&] () { - cancelledByUser= true; - mgr->cancelDownload (templateId); - }); - - connect (mgr, &TemplateManager::downloadProgress, &dialog, - [&dialog] (const QString&, qint64 received, qint64 total) { - if (total <= 0) return; - dialog.setMaximum (static_cast (total)); - dialog.setValue (static_cast (received)); - }); - - dialog.show (); - - QString errorMsg; - QString localPath= mgr->downloadTemplateSync (templateId, 30000, &errorMsg); - - dialog.hide (); - - if (localPath.isEmpty ()) { - if (!cancelledByUser) { - QtFloatingToast::showToast ( - this, - errorMsg.isEmpty () ? qt_translate ("Download failed") : errorMsg, - 3000, QtFloatingToast::Error); - } - return; - } - - auto meta= mgr->templateById (templateId); - if (!meta) { - QtFloatingToast::showToast (this, - qt_translate ("Template metadata not found"), - 3000, QtFloatingToast::Error); - return; - } - qt_copy_template_and_load (this, localPath, meta->name); -} - void QTMHomePage::refreshTemplateThumbnails () { TemplateManager* mgr= TemplateManager::instance (); diff --git a/src/Plugins/Qt/QTMHomePage.hpp b/src/Plugins/Qt/QTMHomePage.hpp index 72bc7c6a9f..9a8b313f50 100644 --- a/src/Plugins/Qt/QTMHomePage.hpp +++ b/src/Plugins/Qt/QTMHomePage.hpp @@ -119,7 +119,6 @@ class QTMHomePage : public QWidget { void removeRecentDoc (const QString& path); void clearAllRecentDocs (); void createDocumentWithStyle (const QString& styleId); - void createDocumentFromTemplate (const QString& templateId); void refreshTemplateThumbnails (); // 样式卡片相关 diff --git a/src/Plugins/Qt/QTMTemplateOpener.cpp b/src/Plugins/Qt/QTMTemplateOpener.cpp new file mode 100644 index 0000000000..89a70f02bf --- /dev/null +++ b/src/Plugins/Qt/QTMTemplateOpener.cpp @@ -0,0 +1,180 @@ + +/****************************************************************************** + * MODULE : QTMTemplateOpener.cpp + * DESCRIPTION: Unified template opener implementation + * COPYRIGHT : (C) 2026 Yuki Lu + ******************************************************************************/ + +#include "QTMTemplateOpener.hpp" +#include "qt_floating_toast.hpp" +#include "qt_template_utils.hpp" +#include "qt_utilities.hpp" +#include "template_manager.hpp" + +#include + +QTMTemplateOpener::QTMTemplateOpener (QWidget* parent) + : QObject (parent), parent_ (parent), + templateManager_ (TemplateManager::instance ()) {} + +QTMTemplateOpener::~QTMTemplateOpener () { cleanupProgressDialog_ (); } + +bool +QTMTemplateOpener::isAvailableLocally (const QString& templateId) const { + return templateManager_ && + templateManager_->isTemplateAvailableLocally (templateId); +} + +bool +QTMTemplateOpener::openTemplate (const QString& templateId) { + resetState_ (); + currentTemplateId_= templateId; + + if (isAvailableLocally (templateId)) { + return openLocalTemplate_ (templateId); + } + + return startDownload_ (templateId); +} + +bool +QTMTemplateOpener::openLocalTemplate_ (const QString& templateId) { + if (!templateManager_) { + showError_ (qt_translate ("Template manager not available")); + emit failed (templateId, qt_translate ("Template manager not available")); + return false; + } + + auto meta= templateManager_->templateById (templateId); + if (!meta) { + showError_ (qt_translate ("Template metadata not found")); + emit failed (templateId, qt_translate ("Template metadata not found")); + return false; + } + + QString localPath= templateManager_->localTemplatePath (templateId); + if (localPath.isEmpty ()) { + showError_ (qt_translate ("Local template file is missing")); + emit failed (templateId, qt_translate ("Local template file is missing")); + return false; + } + + return loadFromLocalPath_ (templateId, localPath, meta->name); +} + +bool +QTMTemplateOpener::startDownload_ (const QString& templateId) { + if (!templateManager_) { + showError_ (qt_translate ("Template manager not available")); + emit failed (templateId, qt_translate ("Template manager not available")); + return false; + } + + cleanupProgressDialog_ (); + + progressDialog_= + new QProgressDialog (qt_translate ("Downloading template..."), + qt_translate ("Cancel"), 0, 100, parent_); + progressDialog_->setWindowModality (Qt::WindowModal); + + connect (progressDialog_, &QProgressDialog::canceled, [this, templateId] () { + downloadCancelledByUser_= true; + if (templateManager_) { + templateManager_->cancelDownload (templateId); + } + }); + + connect (templateManager_, &TemplateManager::downloadProgress, this, + &QTMTemplateOpener::onDownloadProgress); + + progressDialog_->show (); + + QString errorMsg; + QString localPath= + templateManager_->downloadTemplateSync (templateId, 30000, &errorMsg); + + cleanupProgressDialog_ (); + + if (localPath.isEmpty ()) { + if (!downloadCancelledByUser_) { + QString msg= + errorMsg.isEmpty () ? qt_translate ("Download failed") : errorMsg; + showError_ (msg); + emit failed (templateId, msg); + } + else { + emit failed (templateId, QString ()); + } + return false; + } + + auto meta= templateManager_->templateById (templateId); + if (!meta) { + showError_ (qt_translate ("Template metadata not found")); + emit failed (templateId, qt_translate ("Template metadata not found")); + return false; + } + + return loadFromLocalPath_ (templateId, localPath, meta->name); +} + +bool +QTMTemplateOpener::loadFromLocalPath_ (const QString& templateId, + const QString& localPath, + const QString& templateName) { + QString docPath= qt_copy_template_to_documents (localPath, templateName); + if (docPath.isEmpty ()) { + showError_ (qt_translate ("Failed to copy template to Documents")); + emit failed (templateId, + qt_translate ("Failed to copy template to Documents")); + return false; + } + + qt_load_document_path (docPath); + emit completed (templateId, docPath); + return true; +} + +void +QTMTemplateOpener::onDownloadProgress (const QString& templateId, + qint64 bytesReceived, + qint64 bytesTotal) { + if (templateId != currentTemplateId_) return; + if (!progressDialog_) return; + + if (bytesTotal < 0) { + progressDialog_->setRange (0, 0); + } + else { + progressDialog_->setMaximum (static_cast (bytesTotal)); + progressDialog_->setValue (static_cast (bytesReceived)); + } + + emit downloadProgress (templateId, bytesReceived, bytesTotal); +} + +void +QTMTemplateOpener::cleanupProgressDialog_ () { + if (progressDialog_) { + progressDialog_->hide (); + progressDialog_->deleteLater (); + progressDialog_= nullptr; + } + + if (templateManager_) { + disconnect (templateManager_, &TemplateManager::downloadProgress, this, + &QTMTemplateOpener::onDownloadProgress); + } +} + +void +QTMTemplateOpener::showError_ (const QString& message) { + QtFloatingToast::showToast (parent_, message, 3000, QtFloatingToast::Error); +} + +void +QTMTemplateOpener::resetState_ () { + currentTemplateId_.clear (); + downloadCancelledByUser_= false; + cleanupProgressDialog_ (); +} diff --git a/src/Plugins/Qt/QTMTemplateOpener.hpp b/src/Plugins/Qt/QTMTemplateOpener.hpp new file mode 100644 index 0000000000..14fdddd951 --- /dev/null +++ b/src/Plugins/Qt/QTMTemplateOpener.hpp @@ -0,0 +1,110 @@ + +/****************************************************************************** + * MODULE : QTMTemplateOpener.hpp + * DESCRIPTION: Unified template opener for HomePage and TemplatePage + * COPYRIGHT : (C) 2026 Yuki Lu + ******************************************************************************/ + +#ifndef QTMTEMPLATEOPENER_HPP +#define QTMTEMPLATEOPENER_HPP + +#include +#include + +class QProgressDialog; +class QWidget; +class TemplateManager; + +/** + * @brief 统一的模板打开器 + * + * 封装打开模板的核心逻辑: + * - 本地模板:复制到 Documents 并立即打开 + * - 远程模板:下载(带进度对话框)→ 复制 → 打开 + * + * 使用示例(HomePage 一键打开): + * @code + * QTMTemplateOpener opener(this); + * opener.openTemplate("elegantbook"); + * @endcode + * + * 使用示例(TemplatePage 预览后打开): + * @code + * QTMTemplateOpener opener(this); + * opener.openTemplate("nsfc-ysf-c"); + * @endcode + * + * @note openTemplate() 为同步风格:本地模板立即完成; + * 远程模板会阻塞(通过 QProgressDialog 保持事件循环响应), + * 直到下载完成。completed() / failed() 信号会在 openTemplate() + * 返回前发出。 + */ +class QTMTemplateOpener : public QObject { + Q_OBJECT + +public: + explicit QTMTemplateOpener (QWidget* parent= nullptr); + ~QTMTemplateOpener (); + + QTMTemplateOpener (const QTMTemplateOpener&) = delete; + QTMTemplateOpener& operator= (const QTMTemplateOpener&)= delete; + + /** + * @brief 打开模板(本地或远程) + * + * 若模板在本地可用,则直接打开; + * 否则显示进度对话框并先下载。 + * + * @param templateId 模板 ID + * @return 成功返回 true,失败返回 false + */ + bool openTemplate (const QString& templateId); + + /** + * @brief 检查模板是否在本地可用 + */ + bool isAvailableLocally (const QString& templateId) const; + +signals: + /** + * @brief 下载进度更新 + */ + void downloadProgress (const QString& templateId, qint64 bytesReceived, + qint64 bytesTotal); + + /** + * @brief 模板打开成功 + * @param templateId 模板 ID + * @param documentPath Documents 中的文档路径 + */ + void completed (const QString& templateId, const QString& documentPath); + + /** + * @brief 打开模板失败 + * @param templateId 模板 ID + * @param error 可读错误信息(用户取消时为空) + */ + void failed (const QString& templateId, const QString& error); + +private slots: + void onDownloadProgress (const QString& templateId, qint64 bytesReceived, + qint64 bytesTotal); + +private: + bool openLocalTemplate_ (const QString& templateId); + bool startDownload_ (const QString& templateId); + bool loadFromLocalPath_ (const QString& templateId, const QString& localPath, + const QString& templateName); + void cleanupProgressDialog_ (); + void showError_ (const QString& message); + void resetState_ (); + + QWidget* parent_; + TemplateManager* templateManager_; + QPointer progressDialog_; + + QString currentTemplateId_; + bool downloadCancelledByUser_= false; +}; + +#endif // QTMTEMPLATEOPENER_HPP diff --git a/src/Plugins/Qt/QTMTemplatePage.cpp b/src/Plugins/Qt/QTMTemplatePage.cpp index f0bcf0e9b9..7c76ba5711 100644 --- a/src/Plugins/Qt/QTMTemplatePage.cpp +++ b/src/Plugins/Qt/QTMTemplatePage.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -26,10 +25,10 @@ #include #include +#include "QTMTemplateOpener.hpp" #include "qt_dpi_utils.hpp" #include "qt_floating_toast.hpp" #include "qt_pdf_preview_widget.hpp" -#include "qt_template_utils.hpp" #include "qt_utilities.hpp" #include "template_manager.hpp" #include "thumbnail_loader.hpp" @@ -562,7 +561,8 @@ QTMTemplatePage::showTemplatePreview (const QString& templateId) { useBtn->setDefault (true); connect (useBtn, &QPushButton::clicked, [this, dialog, templateId] () { dialog->accept (); - downloadAndUseTemplate (templateId); + QTMTemplateOpener opener (this); + opener.openTemplate (templateId); }); btnLayout->addWidget (useBtn); @@ -571,79 +571,6 @@ QTMTemplatePage::showTemplatePreview (const QString& templateId) { dialog->exec (); } -void -QTMTemplatePage::downloadAndUseTemplate (const QString& templateId) { - if (!templateManager_) return; - - if (templateManager_->isTemplateAvailableLocally (templateId)) { - auto meta= templateManager_->templateById (templateId); - if (!meta) { - QtFloatingToast::showToast (this, - qt_translate ("Template metadata not found"), - 3000, QtFloatingToast::Error); - return; - } - QString localPath= templateManager_->localTemplatePath (templateId); - if (localPath.isEmpty ()) { - QtFloatingToast::showToast ( - this, qt_translate ("Local template file is missing"), 3000, - QtFloatingToast::Error); - return; - } - qt_copy_template_and_load (this, localPath, meta->name); - return; - } - - QProgressDialog dialog (qt_translate ("Downloading template..."), - qt_translate ("Cancel"), 0, 100, this); - dialog.setWindowModality (Qt::WindowModal); - dialog.setAutoClose (true); - - bool cancelledByUser= false; - connect (&dialog, &QProgressDialog::canceled, [&] () { - cancelledByUser= true; - templateManager_->cancelDownload (templateId); - }); - - connect (templateManager_, &TemplateManager::downloadProgress, &dialog, - [&dialog] (const QString&, qint64 received, qint64 total) { - if (total < 0) { - dialog.setRange (0, 0); - } - else { - dialog.setMaximum (static_cast (total)); - dialog.setValue (static_cast (received)); - } - }); - - dialog.show (); - - QString errorMsg; - QString localPath= - templateManager_->downloadTemplateSync (templateId, 30000, &errorMsg); - - dialog.hide (); - - if (localPath.isEmpty ()) { - if (!cancelledByUser) { - QtFloatingToast::showToast ( - this, - errorMsg.isEmpty () ? qt_translate ("Download failed") : errorMsg, - 3000, QtFloatingToast::Error); - } - return; - } - - auto meta= templateManager_->templateById (templateId); - if (!meta) { - QtFloatingToast::showToast (this, - qt_translate ("Template metadata not found"), - 3000, QtFloatingToast::Error); - return; - } - qt_copy_template_and_load (this, localPath, meta->name); -} - void QTMTemplatePage::onTemplatesLoaded () { // Initialize category bar if not already done diff --git a/src/Plugins/Qt/QTMTemplatePage.hpp b/src/Plugins/Qt/QTMTemplatePage.hpp index 6ecdfbf623..2163cf13f8 100644 --- a/src/Plugins/Qt/QTMTemplatePage.hpp +++ b/src/Plugins/Qt/QTMTemplatePage.hpp @@ -55,7 +55,6 @@ private slots: void refreshTemplateGrid (const QString& category); int calculateColumnCount () const; void showTemplatePreview (const QString& templateId); - void downloadAndUseTemplate (const QString& templateId); // UI components QLabel* titleLabel_;