diff --git a/docs/02-prd.md b/docs/02-prd.md index 6daa9e7..015ff09 100644 --- a/docs/02-prd.md +++ b/docs/02-prd.md @@ -165,7 +165,9 @@ - 内置模板尺寸以贴近真实印花占比为准(例如正方形模板约为衣服宽度的 1/3),避免印花占据过大面积。 - 选中的模板是**粘性**的:在预览区切换衣服或切换印花时,印花不应丢失,应按当前选中模板基于新的衣服/印花尺寸重新计算落位,而非回退到画布的写死默认值。已手动微调的项不受影响,仍使用其自身参数。 - **记住上次选择的模板**:程序退出前持久化当前选中的模板,下次启动自动恢复选中该模板。若该模板为自定义模板且已被删除,则回退到第一个内置模板。 -- 提供 `位置` / `尺寸` / `旋转` **三项独立的重置**,分别只把对应参数还原为当前选中模板的值(位置→模板居中位置;尺寸→模板目标框内适配的大小;旋转→模板角度,通常为 0),三者互不影响;不再使用单一的合并重置。未选模板或未加载衣服/印花时,重置不可用。 +- 提供 `位置` / `尺寸` / `旋转` **三项独立的重置**,分别只把对应参数还原为当前选中模板的值(位置→模板居中位置;尺寸→模板目标框内适配的大小;旋转→模板角度,通常为 0),三者互不影响。 +- 另在参数栏底部提供 `位置尺寸旋转重置为当前模板`,一次性把位置、尺寸、旋转整体还原为模板,作为三项分项重置的「全部」入口。该按钮归属「调整参数」,放在参数栏(紧挨它操作的参数),不放在模板管理区域。 +- 未选模板或未加载衣服/印花时,以上所有重置不可用。 ### 6.7 自定义模板 diff --git a/docs/07-ui-design.md b/docs/07-ui-design.md index 6fb18b6..4d9d298 100644 --- a/docs/07-ui-design.md +++ b/docs/07-ui-design.md @@ -195,7 +195,9 @@ Shift 锁比例 - 尺寸重置 → 还原为模板目标框内适配的大小(宽 / 高)。 - 旋转重置 → 还原为模板角度(通常为 0)。 -未选模板或未加载衣服/印花时三个重置按钮置灰。视觉层级见第 10 节「重置类按钮」。 +参数栏底部另有一个全宽的 `位置尺寸旋转重置为当前模板` 按钮,一次性把位置、尺寸、旋转整体还原为当前选中模板,作为三个分项重置的「全部」入口。它紧挨它所操作的参数,不放在模板区域。 + +未选模板或未加载衣服/印花时,三个分项重置与 `位置尺寸旋转重置为当前模板` 一并置灰。视觉层级见第 10 节「重置类按钮」。 ### 7.1 模板区域 @@ -204,7 +206,9 @@ Shift 锁比例 - 模板下拉框。 - `保存` 按钮。 - `另存为` 按钮。 -- `重置为模板` 按钮(把印花参数一次性整体还原为所选模板)。 +- `删除` 按钮。 + +模板区域只管模板本身(选择 / 保存 / 另存为 / 删除)。把印花参数整体还原为模板的 `位置尺寸旋转重置为当前模板` 属于「调整参数」而非「管理模板」,因此放在参数栏底部(见 7 节开头与 7.4)。 示例模板: @@ -217,19 +221,22 @@ Shift 锁比例 - 选择模板后更新当前印花位置、尺寸和旋转。 - 保存用于覆盖当前自定义模板。 - 另存为用于创建新模板。 -- `保存` 仅对自定义模板有意义:选中内置模板时**隐藏**该按钮(而非禁用置灰),避免出现长期灰显且无法说明原因的控件。 +- `保存` 与 `删除` 仅对自定义模板有意义:选中内置模板时**隐藏**这两个按钮(而非禁用置灰),避免出现长期灰显且无法说明原因的控件。 +- `删除` 为破坏性操作,点击后必须二次确认(弹「确定删除模板「X」?此操作不可撤销。」),确认后才删除;删除后下拉框回选第一个内置模板。 +- `删除` 用红色文字提示危险,但保持安静(不使用红色实心块);删除可逆性低,区别于「重置」类的可逆操作。 ### 7.2 位置区域 控件: -- `X 坐标` 数字输入框。 -- `Y 坐标` 数字输入框。 +- `X` 数字输入框。 +- `Y` 数字输入框。 - 分区标题右侧的 `重置` 按钮(还原 X / Y 为模板值)。 要求: - 单位为衣服底图原始像素。 +- X、Y 是一对坐标,放在**同一行**(标签简化为 `X` / `Y`),节省纵向空间。 - 用户输入后实时或确认后更新预览。 - 预览区拖动后同步更新输入框。 @@ -237,14 +244,15 @@ Shift 锁比例 控件: -- `宽度` 数字输入框。 -- `高度` 数字输入框。 +- `宽` 数字输入框。 +- `高` 数字输入框。 - `锁定宽高比例` 复选框。 - 分区标题右侧的 `重置` 按钮(仅还原宽 / 高为模板值,位置 X / Y 不变)。 要求: - 单位为像素。 +- 宽、高是一对,放在**同一行**(标签简化为 `宽` / `高`),`锁定宽高比例` 复选框在其下方。 - 默认锁定宽高比例。 - 预览区缩放后同步更新宽度和高度。 - 输入框修改后同步更新预览。 @@ -264,7 +272,8 @@ Shift 锁比例 - 支持负角度。 - 顶部旋转控制点拖动后同步角度输入框。 - 按钮旋转后同步预览和角度输入框。 -- 旋转分区标题的 `重置` 已覆盖「拉直」需求,不再单独提供 `归零` 按钮;也不再提供合并的 `重置位置 / 尺寸 / 角度` 按钮,统一由三个分区各自的独立重置取代。 +- 旋转分区标题的 `重置` 已覆盖「拉直」需求,不再单独提供 `归零` 按钮。 +- 旋转分区下方(参数栏底部)提供 `位置尺寸旋转重置为当前模板`,把位置 / 尺寸 / 旋转一并还原为所选模板;它和三个分项重置构成「全部 + 分项」两级还原。 ### 7.5 输出设置区域 @@ -281,6 +290,7 @@ Shift 锁比例 - 格式至少支持 PNG 和 JPG。 - PNG 默认保留透明相关能力;JPG 需要处理背景。 - 质量设置用于 JPG 或需要压缩的输出格式。 +- `格式` 与 `质量` 是一对相关设置,放在**同一行**;`质量` 仅在选中 JPG 时可用(PNG 时置灰),并排能体现「先选格式 → 质量才可用」的关系。 ### 7.6 提示信息区域 @@ -456,16 +466,21 @@ Microsoft YaHei 按钮层级(一套主色,按角色克制使用): - **主按钮**:主色填充、白字。用于每个区域「推动流程前进」的唯一动作:左侧 `打开文件夹`、右侧 `导出当前单张`、底部 `开始批量导出`。三者分处不同区域,各自是本区域主操作,不互相竞争。 -- **次按钮**:白底、主色描边与文字。用于有意义但非主线的动作,如 `应用为模板`、导出区的 `重置为模板`。 -- **辅助按钮**:浅灰底、细边框。用于就地微调类动作,如 `浏览…`、排序、`左旋 / 右旋`。 -- **重置类按钮**:最安静的一档(幽灵样式),用于 `位置 / 尺寸 / 旋转` 三个分区的独立重置。它们会重复出现且属于「纠正/撤销」类动作,不应占用常驻强调色;强调色只在悬停时短暂出现以表达意图。重置可逆,因此**不使用红色/警告色**。 +- **次按钮**:白底、主色描边与文字。用于有意义但非主线的动作,如模板区域的 `另存为`。 +- **辅助按钮**:浅灰底、细边框。用于就地微调类动作,如 `浏览…`、排序、`左旋 / 右旋`、模板区域的 `保存`。 +- **重置类按钮**:最安静的一档,用于「还原为模板」的动作(三个分区的 `↺ 重置` 与底部的 `位置尺寸旋转重置为当前模板`)。它们属于「纠正/撤销」类动作,不占用常驻强调色;强调色只在悬停时短暂出现以表达意图。重置可逆,因此**不使用红色/警告色**。 ```text -重置类按钮(分区标题内的 ↺ 重置): +分区标题内的 ↺ 重置(幽灵样式): 静止:背景 透明 / #f7f7f7 图标+文字 #8a8a8a 无边框 悬停:背景 #eaf3fc 图标+文字 #0078d4 按下:背景 #d6e8f9 -禁用:文字 #c0c0c0 不响应悬停(未选模板或未加载图片时) +禁用:文字 #c0c0c0 不响应悬停 + +底部 位置尺寸旋转重置为当前模板(全宽,安静但有存在感): +静止:背景 #f0f0f0 文字 #555555 边框 #d6d6d6 +悬停:背景 #eaf3fc 文字 #0078d4 边框 #0078d4 +禁用:文字 #c0c0c0 背景 #f7f7f7 ``` 避免所有按钮使用同一种灰色样式而丧失层级;同一动作在不同位置应使用一致的文案与层级。 diff --git a/src/app/main_window.py b/src/app/main_window.py index 5dfb4c0..904825c 100644 --- a/src/app/main_window.py +++ b/src/app/main_window.py @@ -277,6 +277,8 @@ class MainWindow(QMainWindow): self.transform_panel.reset_position.connect(lambda: self._reset_aspect("pos")) self.transform_panel.reset_size.connect(lambda: self._reset_aspect("size")) self.transform_panel.reset_rotation.connect(lambda: self._reset_aspect("rot")) + # Whole reset: restore position + size + rotation to the selected template + self.transform_panel.reset_all.connect(self._reset_all_to_template) # ── Template panel ───────────────────────────────────────────────── # Template selection updates canvas, param panel, and queue template @@ -403,6 +405,18 @@ class MainWindow(QMainWindow): new.rotation = tpl.rotation self._apply_preview_transform(new) + def _reset_all_to_template(self): + """Restore position + size + rotation to the selected template at once.""" + tpl = self.template_panel.current_template_state() + cur = self.image_canvas.get_transform() + if tpl is None or cur is None: + return + new = TransformState( + x=tpl.x, y=tpl.y, width=tpl.width, height=tpl.height, + rotation=tpl.rotation, keep_aspect_ratio=cur.keep_aspect_ratio, + ) + self._apply_preview_transform(new) + def _apply_preview_transform(self, state): """Push a transform to the canvas + panels as if it were a user edit.""" self.image_canvas.set_transform(state) diff --git a/src/app/widgets/export_panel.py b/src/app/widgets/export_panel.py index 7207149..0879946 100644 --- a/src/app/widgets/export_panel.py +++ b/src/app/widgets/export_panel.py @@ -6,7 +6,6 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QComboBox, QFileDialog, - QFormLayout, QHBoxLayout, QLabel, QLineEdit, @@ -77,17 +76,15 @@ class ExportPanel(QWidget): row.addWidget(browse_btn) return row - def _make_format_form(self) -> QFormLayout: - form = QFormLayout() - form.setSpacing(6) - form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter) + def _make_format_form(self) -> QHBoxLayout: + row = QHBoxLayout() + row.setSpacing(6) self._format_combo = QComboBox() self._format_combo.setObjectName("exportCombo") self._format_combo.addItem("PNG", "PNG") self._format_combo.addItem("JPG", "JPG") self._format_combo.currentIndexChanged.connect(self._on_format_changed) - form.addRow("格式", self._format_combo) self._quality_combo = QComboBox() self._quality_combo.setObjectName("exportCombo") @@ -95,9 +92,13 @@ class ExportPanel(QWidget): self._quality_combo.addItem(label, val) self._quality_combo.setCurrentIndex(1) # default: 高 (90) self._quality_combo.setEnabled(False) # enabled only for JPG - form.addRow("质量", self._quality_combo) - return form + # 格式 and 质量 share one row (quality lights up only for JPG) + row.addWidget(QLabel("格式")) + row.addWidget(self._format_combo, 1) + row.addWidget(QLabel("质量")) + row.addWidget(self._quality_combo, 1) + return row def _make_hint_label(self) -> QLabel: self._hint_lbl = QLabel("加载衣服和印花图片后即可导出。") diff --git a/src/app/widgets/template_panel.py b/src/app/widgets/template_panel.py index 732bde4..dc5f7ee 100644 --- a/src/app/widgets/template_panel.py +++ b/src/app/widgets/template_panel.py @@ -14,7 +14,7 @@ from PySide6.QtWidgets import ( ) from core.models import Template, TransformState -from services.template_service import add_template, get_all_templates +from services.template_service import add_template, delete_template, get_all_templates logger = logging.getLogger(__name__) @@ -75,16 +75,16 @@ class TemplatePanel(QWidget): self._saveas_btn.setToolTip("将当前参数保存为新模板") self._saveas_btn.clicked.connect(self._save_as_template) + self._delete_btn = QPushButton("删除") + self._delete_btn.setObjectName("templateDeleteBtn") + self._delete_btn.setToolTip("删除当前自定义模板") + self._delete_btn.clicked.connect(self._delete_template) + btn_row.addWidget(self._save_btn) btn_row.addWidget(self._saveas_btn) + btn_row.addWidget(self._delete_btn) body_col.addLayout(btn_row) - self._reset_btn = QPushButton("重置为模板") - self._reset_btn.setObjectName("templateBtn") - self._reset_btn.setToolTip("将印花参数还原为所选模板的默认值") - self._reset_btn.clicked.connect(self._reset_to_template) - body_col.addWidget(self._reset_btn) - col.addWidget(body) # ── template list management ───────────────────────────────────────────── @@ -113,9 +113,10 @@ class TemplatePanel(QWidget): def _update_button_states(self): t = self._current_template() is_builtin = (t is None) or (t.type == "builtin") - # "保存" overwrites a custom template — meaningless for built-ins, so - # hide it entirely rather than leaving a permanently-greyed button. + # "保存"/"删除" only apply to custom templates — hide them for built-ins + # rather than leaving permanently-greyed buttons. self._save_btn.setVisible(not is_builtin) + self._delete_btn.setVisible(not is_builtin) def _current_template(self) -> Optional[Template]: return self._combo.currentData() @@ -182,17 +183,6 @@ class TemplatePanel(QWidget): logger.debug("Template applied: %s", t.name) self.template_applied.emit(state) - def _reset_to_template(self): - """Re-apply the currently selected template.""" - t = self._current_template() - if not t: - return - if self._garment_w <= 0 or self._garment_h <= 0: - QMessageBox.information(self, "提示", "请先加载衣服图片。") - return - state = self._state_for_template(t) - self.template_applied.emit(state) - def _save_template(self): """Overwrite the selected custom template with the current transform.""" t = self._current_template() @@ -233,6 +223,29 @@ class TemplatePanel(QWidget): self._load_templates(select_name=name) self.template_changed.emit(name) + def _delete_template(self): + """Delete the selected custom template after confirmation.""" + t = self._current_template() + if not t or t.type == "builtin": + return + reply = QMessageBox.question( + self, "删除模板", + "确定删除模板「{}」?此操作不可撤销。".format(t.name), + QMessageBox.Yes | QMessageBox.No, QMessageBox.No, + ) + if reply != QMessageBox.Yes: + return + try: + delete_template(t.name) + except ValueError as exc: + QMessageBox.warning(self, "删除失败", str(exc)) + return + logger.info("Template deleted: %s", t.name) + # Reload; selection falls back to the first (built-in) template + self._load_templates() + cur = self._current_template() + self.template_changed.emit(cur.name if cur else "") + # ── helpers ────────────────────────────────────────────────────────────── def _state_to_template(self, name: str) -> Template: @@ -279,4 +292,14 @@ class TemplatePanel(QWidget): #templateBtn:hover { background: #e0e0e0; } #templateBtn:pressed { background: #d0d0d0; } #templateBtn:disabled { color: #aaaaaa; background: #f7f7f7; } + #templateDeleteBtn { + font-size: 12px; + padding: 4px 8px; + border: 1px solid #d6d6d6; + border-radius: 3px; + background: #f0f0f0; + color: #c42b1c; + } + #templateDeleteBtn:hover { background: #fde7e9; border-color: #c42b1c; } + #templateDeleteBtn:pressed { background: #f9d4d7; } """) diff --git a/src/app/widgets/transform_panel.py b/src/app/widgets/transform_panel.py index 7474850..bf76325 100644 --- a/src/app/widgets/transform_panel.py +++ b/src/app/widgets/transform_panel.py @@ -27,6 +27,7 @@ class TransformPanel(QWidget): reset_position = Signal() reset_size = Signal() reset_rotation = Signal() + reset_all = Signal() # "位置尺寸旋转重置为当前模板" — reset all three at once def __init__(self, parent=None): super().__init__(parent) @@ -46,6 +47,7 @@ class TransformPanel(QWidget): col.addWidget(self._make_section("位置", self._make_pos_form(), "pos")) col.addWidget(self._make_section("尺寸", self._make_size_form(), "size")) col.addWidget(self._make_section("旋转", self._make_rot_form(), "rot")) + col.addWidget(self._make_footer()) def _make_section(self, title: str, body: QWidget, reset_tag: str) -> QWidget: section = QWidget() @@ -82,18 +84,20 @@ class TransformPanel(QWidget): def _make_pos_form(self) -> QWidget: w = QWidget() w.setObjectName("transformForm") - form = QFormLayout(w) - form.setContentsMargins(10, 6, 10, 6) - form.setSpacing(6) - form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter) + row = QHBoxLayout(w) + row.setContentsMargins(10, 6, 10, 6) + row.setSpacing(6) self._x_spin = self._make_spin(-99999, 99999, "px") self._y_spin = self._make_spin(-99999, 99999, "px") self._x_spin.valueChanged.connect(self._on_xy_changed) self._y_spin.valueChanged.connect(self._on_xy_changed) - form.addRow("X 坐标", self._x_spin) - form.addRow("Y 坐标", self._y_spin) + # X and Y are a coordinate pair → keep them on one row + row.addWidget(self._make_field_label("X")) + row.addWidget(self._x_spin, 1) + row.addWidget(self._make_field_label("Y")) + row.addWidget(self._y_spin, 1) return w def _make_size_form(self) -> QWidget: @@ -103,18 +107,20 @@ class TransformPanel(QWidget): col.setContentsMargins(10, 6, 10, 6) col.setSpacing(6) - form = QFormLayout() - form.setSpacing(6) - form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter) - self._w_spin = self._make_spin(_MIN_SIZE, 99999, "px") self._h_spin = self._make_spin(_MIN_SIZE, 99999, "px") self._w_spin.valueChanged.connect(self._on_width_changed) self._h_spin.valueChanged.connect(self._on_height_changed) - form.addRow("宽度", self._w_spin) - form.addRow("高度", self._h_spin) - col.addLayout(form) + # Width and height are a pair → keep them on one row + size_row = QHBoxLayout() + size_row.setContentsMargins(0, 0, 0, 0) + size_row.setSpacing(6) + size_row.addWidget(self._make_field_label("宽")) + size_row.addWidget(self._w_spin, 1) + size_row.addWidget(self._make_field_label("高")) + size_row.addWidget(self._h_spin, 1) + col.addLayout(size_row) self._lock_cb = QCheckBox("锁定宽高比例") self._lock_cb.setChecked(True) @@ -157,6 +163,21 @@ class TransformPanel(QWidget): col.addLayout(btn_row) return w + def _make_footer(self) -> QWidget: + footer = QWidget() + footer.setObjectName("transformFooter") + col = QVBoxLayout(footer) + col.setContentsMargins(10, 8, 10, 10) + col.setSpacing(0) + + self._reset_all_btn = QPushButton("位置尺寸旋转重置为当前模板") + self._reset_all_btn.setObjectName("transformResetAllBtn") + self._reset_all_btn.setToolTip("将位置、尺寸、旋转一并还原为当前选中模板") + self._reset_all_btn.setEnabled(False) + self._reset_all_btn.clicked.connect(lambda: self.reset_all.emit()) + col.addWidget(self._reset_all_btn) + return footer + def _emit_reset(self, tag: str): if tag == "pos": self.reset_position.emit() @@ -166,9 +187,16 @@ class TransformPanel(QWidget): self.reset_rotation.emit() def set_resets_enabled(self, enabled: bool): - """Enable/disable the three section reset buttons together.""" + """Enable/disable the section resets and the全部重置 button together.""" for btn in self._reset_btns.values(): btn.setEnabled(enabled) + self._reset_all_btn.setEnabled(enabled) + + @staticmethod + def _make_field_label(text: str) -> QLabel: + lbl = QLabel(text) + lbl.setObjectName("fieldLabel") + return lbl @staticmethod def _make_spin(minimum: float, maximum: float, suffix: str) -> QDoubleSpinBox: @@ -209,6 +237,11 @@ class TransformPanel(QWidget): #transformForm { background-color: #ffffff; } + #fieldLabel { + font-family: "Microsoft YaHei", "Segoe UI", sans-serif; + font-size: 12px; + color: #555555; + } #transformSpin { font-size: 12px; border: 1px solid #d6d6d6; @@ -227,6 +260,25 @@ class TransformPanel(QWidget): } #rotBtn:hover { background: #e0e0e0; } #rotBtn:pressed { background: #d0d0d0; } + #transformFooter { + background-color: #ffffff; + border-bottom: 1px solid #e0e0e0; + } + #transformResetAllBtn { + font-size: 12px; + color: #555555; + background: #f0f0f0; + border: 1px solid #d6d6d6; + border-radius: 4px; + padding: 6px 0; + } + #transformResetAllBtn:hover { + background: #eaf3fc; + color: #0078d4; + border-color: #0078d4; + } + #transformResetAllBtn:pressed { background: #d6e8f9; } + #transformResetAllBtn:disabled { color: #c0c0c0; background: #f7f7f7; } """) # ── public API ──────────────────────────────────────────────────────────── diff --git a/tasks.md b/tasks.md index acd5763..3d25501 100644 --- a/tasks.md +++ b/tasks.md @@ -608,6 +608,66 @@ - [ ] 同一个动作在界面上只出现一次,命名一致 - [ ] 导出面板仅保留输出设置与 `导出当前单张` +### 17.4 「重置为模板」归位并更名为「位置尺寸旋转重置为当前模板」 + +前置阅读: + +- `docs/07-ui-design.md`(7 右侧参数栏、10 视觉规范) + +任务: + +- [ ] 把模板区域的 `重置为模板` 按钮移出,移到参数栏底部(旋转分区下方) +- [ ] 更名为 `位置尺寸旋转重置为当前模板`,与三个分项 `↺ 重置` 构成「全部 + 分项」两级 +- [ ] 还原逻辑改为把位置/尺寸/旋转整体设为模板值,并与分项重置同走 `_apply_preview_transform` +- [ ] 与三个分项重置一起受「未加载图片时置灰」控制 +- [ ] 采用安静、全宽、悬停出强调色的样式(不使用红色) + +验收: + +- [ ] 模板区域只剩 `保存` / `另存为`,不含整体重置 +- [ ] `位置尺寸旋转重置为当前模板` 在旋转分区下方,未加载图片时置灰 +- [ ] 点击后位置、尺寸、旋转一并还原为当前选中模板 + +### 17.5 模板删除按钮 + +前置阅读: + +- `docs/07-ui-design.md`(7.1 模板区域) + +任务: + +- [ ] 在模板区域增加 `删除` 按钮(复用 `template_service.delete_template`) +- [ ] `删除` 仅对自定义模板可用,选中内置模板时隐藏(与 `保存` 一致) +- [ ] 删除前弹二次确认对话框;确认后才删除 +- [ ] 删除后刷新下拉框并回选第一个内置模板,同步持久化选择 +- [ ] `删除` 用红色文字提示危险但保持安静样式 + +验收: + +- [ ] 可在界面上删除自定义模板,无需手改 `templates.json` +- [ ] 内置模板不显示 `删除` +- [ ] 删除有二次确认,取消则不删 + +### 17.6 参数/导出面板紧凑化与命名 + +前置阅读: + +- `docs/07-ui-design.md`(7.2 位置区域、7.3 尺寸区域、7.5 输出设置区域) + +任务: + +- [ ] 「位置」的 X、Y 输入框放在同一行(标签简化为 `X` / `Y`) +- [ ] 「尺寸」的宽、高输入框放在同一行(标签简化为 `宽` / `高`),`锁定宽高比例` 在其下方 +- [ ] 导出面板「格式」「质量」放在同一行 +- [ ] 底部整体重置按钮更名为 `位置尺寸旋转重置为当前模板` +- [ ] 仅改布局/文案,不动同步与导出逻辑 + +验收: + +- [ ] X/Y 一行、宽/高一行、格式/质量一行,界面更紧凑 +- [ ] 拖动/缩放预览后输入框仍正常同步 +- [ ] 质量仍仅在 JPG 时可用 + ## 18. 后续暂缓任务 以下任务第一阶段暂不做,后续需要时再新增设计文档: