Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions devel/1008.md
Original file line number Diff line number Diff line change
@@ -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");
```

调用方可以选择连接信号处理结果,也可以直接调用(同步风格,结果在返回前已确定)。
75 changes: 3 additions & 72 deletions src/Plugins/Qt/QTMHomePage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,16 @@
#include <QMouseEvent>
#include <QPainter>
#include <QPointer>
#include <QProgressDialog>
#include <QPushButton>
#include <QResizeEvent>
#include <QStringList>
#include <QStyleOption>
#include <QTimer>
#include <QVBoxLayout>

#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"
Expand Down Expand Up @@ -761,83 +760,15 @@ 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;
}
}

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<int> (total));
dialog.setValue (static_cast<int> (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 ();
Expand Down
1 change: 0 additions & 1 deletion src/Plugins/Qt/QTMHomePage.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 ();

// 样式卡片相关
Expand Down
Loading
Loading