From edcd5d1e18db91234e34037e609eb536b7e953fb Mon Sep 17 00:00:00 2001 From: ila Date: Wed, 17 Jun 2026 15:13:01 +0800 Subject: [PATCH] feat: auto-preview, timestamped/grouped output, folder memory; fix batch format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-preview the first image after a folder loads (both panels), so the canvas isn't empty (§17.8). - Group exports as ///.; one timestamp per export run, shared across the batch (§17.9). - Remember the last garment/print folders and reopen the picker there; persisted via config, wired through main_window (§17.10). - Shrink the 3 box templates further and lower them (chest placement). - Fix: batch export ignored the chosen format/quality/output dir because the queue never received ExportOptions. Export panel now emits export_options_changed → queue.set_export_options (+ initial sync) (§17.11). Docs: PRD 6.1/6.8/9, architecture 4.12, UI design 5.1, tasks.md 17.8–17.11. Co-Authored-By: Claude Opus 4.8 --- docs/02-prd.md | 10 +++- docs/05-project-architecture.md | 2 +- docs/07-ui-design.md | 1 + src/app/main_window.py | 18 +++++++ src/app/widgets/export_panel.py | 39 +++++++++++---- src/app/widgets/image_list_panel.py | 32 ++++++++++-- src/app/widgets/queue_panel.py | 25 +++++---- src/core/batch.py | 6 ++- src/services/file_service.py | 23 +++++++-- src/services/template_service.py | 14 +++--- tasks.md | 78 +++++++++++++++++++++++++++++ 11 files changed, 208 insertions(+), 40 deletions(-) diff --git a/docs/02-prd.md b/docs/02-prd.md index 015ff09..88a03bc 100644 --- a/docs/02-prd.md +++ b/docs/02-prd.md @@ -96,6 +96,7 @@ - 支持单独选中或取消选中某张衣服图片。 - 支持单独选中或取消选中某张印花图片。 - 支持选择当前预览使用的衣服图和印花图。 +- **记住上次选择的衣服文件夹和印花文件夹**:分别持久化,下次打开「打开文件夹」对话框时定位到上次的位置;目录已不存在时回退到系统默认位置。 - 加载失败时显示可理解的错误信息。 - 忽略不支持的文件格式,并在日志中记录。 @@ -162,7 +163,7 @@ - 用户点击模板后,印花位置、尺寸、旋转参数立即应用到当前预览。 - 模板参数应基于衣服底图尺寸计算,避免只适配单一图片分辨率。 - 模板的 `width_ratio` / `height_ratio` 定义衣服上的**目标框**,而非强制拉伸尺寸。已知印花原始尺寸时,印花按自身宽高比缩放后 contain 进目标框并居中,不被拉伸变形;未知印花尺寸时退化为铺满目标框。 -- 内置模板尺寸以贴近真实印花占比为准(例如正方形模板约为衣服宽度的 1/3),避免印花占据过大面积。 +- 内置模板尺寸以贴近真实印花占比为准(例如正方形模板约为衣服宽度的 1/6,居中偏下),避免印花占据过大面积。 - 选中的模板是**粘性**的:在预览区切换衣服或切换印花时,印花不应丢失,应按当前选中模板基于新的衣服/印花尺寸重新计算落位,而非回退到画布的写死默认值。已手动微调的项不受影响,仍使用其自身参数。 - **记住上次选择的模板**:程序退出前持久化当前选中的模板,下次启动自动恢复选中该模板。若该模板为自定义模板且已被删除,则回退到第一个内置模板。 - 提供 `位置` / `尺寸` / `旋转` **三项独立的重置**,分别只把对应参数还原为当前选中模板的值(位置→模板居中位置;尺寸→模板目标框内适配的大小;旋转→模板角度,通常为 0),三者互不影响。 @@ -192,6 +193,8 @@ - 支持 JPG、PNG 输出。 - 支持输出质量设置。 - 文件名应避免覆盖已有文件,除非用户确认。 +- 输出按「运行时间戳 → 印花」两级分组:每次导出运行先在输出目录下新建一个时间戳文件夹,其内再按印花分子文件夹,即 `输出目录/<时间戳>/<印花文件名>/<衣服文件名>.<扩展名>`(单张导出与批量导出一致)。 +- 一次导出运行(一次批量,或一次单张)共用同一个时间戳,避免重复生成时互相覆盖或混在一起。 ### 6.9 批量合成导出 @@ -292,6 +295,10 @@ AutoPrint/ templates.json logs/ output/ + <时间戳>/ # 每次导出运行一个时间戳文件夹(如 20260617_143022) + <印花文件名>/ # 其内按印花分组,子文件夹以印花文件名命名 + <衣服文件名>.png + ... resources/ ``` @@ -329,6 +336,7 @@ AutoPrint/ 说明: +- `last_garment_dir` / `last_print_dir`:上次打开的衣服 / 印花文件夹路径,用于让文件夹对话框定位到上次位置。 - `last_template`:上次选中的模板名称,用于启动恢复。 - `last_batch_mode`:上次选择的批量模式(取 `BatchMode` 枚举值,如 `full_combo` / `many_garments` / `many_prints` / `one_to_one`)。 - 偏好的读取、分发与「改一次存一次」由主窗口集中处理,UI 控件不直接读写配置文件。 diff --git a/docs/05-project-architecture.md b/docs/05-project-architecture.md index 96cc744..e165535 100644 --- a/docs/05-project-architecture.md +++ b/docs/05-project-architecture.md @@ -240,7 +240,7 @@ BatchResult - 读取和保存应用配置。 - 管理默认配置。 - 在配置损坏或缺失时提供安全默认值。 -- 持久化用户偏好:输出设置、上次选择的模板(`last_template`)、上次选择的批量模式(`last_batch_mode`)等。 +- 持久化用户偏好:输出设置、上次的衣服/印花文件夹(`last_garment_dir`/`last_print_dir`)、上次选择的模板(`last_template`)、上次选择的批量模式(`last_batch_mode`)等。 由主窗口集中使用:启动时加载一次并把初值分发给各面板,面板选择变化时「改一次存一次」回写。各 UI 控件不直接读写配置文件,避免分散解析。 diff --git a/docs/07-ui-design.md b/docs/07-ui-design.md index 3fe5e75..19f4e7f 100644 --- a/docs/07-ui-design.md +++ b/docs/07-ui-design.md @@ -100,6 +100,7 @@ 交互规则: - 选择文件夹后递归导入该文件夹及所有子文件夹中的支持格式图片。 +- 加载文件夹后默认选中并预览第一张(按当前排序);空文件夹则不选。 - 支持全选和取消全选。 - 支持单张勾选和取消勾选。 - 支持点击某张衣服图片作为当前预览底图。 diff --git a/src/app/main_window.py b/src/app/main_window.py index 4230a1f..3cc22c3 100644 --- a/src/app/main_window.py +++ b/src/app/main_window.py @@ -260,6 +260,10 @@ class MainWindow(QMainWindow): self.template_panel.template_applied.connect(self.queue_panel.set_transform) self.template_panel.template_applied.connect(self.export_panel.set_transform) + # Output settings (dir/format/quality) flow to the queue for batch export + self.export_panel.export_options_changed.connect(self.queue_panel.set_export_options) + self.queue_panel.set_export_options(self.export_panel.current_options()) # initial sync + def _load_print_preview(self, path): """Load a print into the canvas and sync its native size to the template panel.""" self.image_canvas.load_print(path) @@ -414,9 +418,15 @@ class MainWindow(QMainWindow): except ValueError: logger.warning("Unknown last_batch_mode %r, keeping default", raw_mode) + # Restore where the folder pickers open (last-used folders) + self.image_list_panel.set_garment_start_dir(self._config.get("last_garment_dir", "")) + self.image_list_panel.set_print_start_dir(self._config.get("last_print_dir", "")) + # Persist future changes (connected after restore to avoid echo writes) self.template_panel.template_changed.connect(self._on_template_persist) self.queue_panel.batch_mode_changed.connect(self._on_batch_mode_persist) + self.image_list_panel.garment_folder_opened.connect(self._on_garment_dir_persist) + self.image_list_panel.print_folder_opened.connect(self._on_print_dir_persist) def _on_template_persist(self, name): self._config["last_template"] = name @@ -426,6 +436,14 @@ class MainWindow(QMainWindow): self._config["last_batch_mode"] = getattr(mode, "value", str(mode)) save_config(self._config) + def _on_garment_dir_persist(self, path): + self._config["last_garment_dir"] = path + save_config(self._config) + + def _on_print_dir_persist(self, path): + self._config["last_print_dir"] = path + save_config(self._config) + # ------------------------------------------------------------------ # Style (ref: docs/07-ui-design.md §10) # ------------------------------------------------------------------ diff --git a/src/app/widgets/export_panel.py b/src/app/widgets/export_panel.py index 0879946..379888f 100644 --- a/src/app/widgets/export_panel.py +++ b/src/app/widgets/export_panel.py @@ -2,7 +2,7 @@ import logging from pathlib import Path from typing import Optional -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( QComboBox, QFileDialog, @@ -17,7 +17,11 @@ from PySide6.QtWidgets import ( from core.composer import compose from core.models import ExportOptions, TransformState -from services.file_service import get_output_dir, make_safe_output_path +from services.file_service import ( + get_output_dir, + make_safe_output_path, + timestamped_run_dir, +) logger = logging.getLogger(__name__) @@ -25,6 +29,8 @@ logger = logging.getLogger(__name__) class ExportPanel(QWidget): """Right-panel section: output settings and single-image export.""" + export_options_changed = Signal(object) # ExportOptions when dir/format/quality changes + def __init__(self, parent=None): super().__init__(parent) self._garment_path: Optional[str] = None @@ -66,6 +72,7 @@ class ExportPanel(QWidget): self._dir_input.setReadOnly(True) self._dir_input.setPlaceholderText("输出目录") self._dir_input.setText(str(get_output_dir())) + self._dir_input.textChanged.connect(self._emit_options) browse_btn = QPushButton("浏览…") browse_btn.setObjectName("exportBrowseBtn") @@ -92,6 +99,7 @@ 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 + self._quality_combo.currentIndexChanged.connect(self._emit_options) # 格式 and 质量 share one row (quality lights up only for JPG) row.addWidget(QLabel("格式")) @@ -133,11 +141,24 @@ class ExportPanel(QWidget): """Update the operation-scope hint label.""" self._hint_lbl.setText(text) + def current_options(self) -> ExportOptions: + """Build ExportOptions from the current output settings.""" + fmt = self._format_combo.currentData() + return ExportOptions( + output_dir=self._dir_input.text().strip(), + output_format=fmt, + quality=self._quality_combo.currentData() if fmt == "JPG" else 95, + ) + # ── slot handlers ──────────────────────────────────────────────────────── + def _emit_options(self, *_): + self.export_options_changed.emit(self.current_options()) + def _on_format_changed(self, _index: int): fmt = self._format_combo.currentData() self._quality_combo.setEnabled(fmt == "JPG") + self._emit_options() def _browse_output_dir(self): folder = QFileDialog.getExistingDirectory( @@ -158,9 +179,8 @@ class ExportPanel(QWidget): QMessageBox.information(self, "提示", "当前没有可导出的印花参数。") return - out_dir = self._dir_input.text().strip() or str(get_output_dir()) - fmt = self._format_combo.currentData() - quality = self._quality_combo.currentData() if fmt == "JPG" else 95 + options = self.current_options() + out_dir = options.output_dir or str(get_output_dir()) out_dir_path = Path(out_dir) try: @@ -173,13 +193,10 @@ class ExportPanel(QWidget): ) return - options = ExportOptions( - output_dir=out_dir, - output_format=fmt, - quality=quality, - ) + # Group this export under a timestamped run folder (compose creates it) + run_dir = timestamped_run_dir(out_dir) output_path = make_safe_output_path( - out_dir, self._garment_path, self._print_path, fmt + run_dir, self._garment_path, self._print_path, options.output_format ) result = compose( diff --git a/src/app/widgets/image_list_panel.py b/src/app/widgets/image_list_panel.py index 784fdf9..413c522 100644 --- a/src/app/widgets/image_list_panel.py +++ b/src/app/widgets/image_list_panel.py @@ -155,12 +155,14 @@ class _AssetPanel(QWidget): preview_changed = Signal(object) # ImageAsset | None assets_changed = Signal(list) # List[ImageAsset] + folder_opened = Signal(str) # chosen folder path (for persistence) def __init__(self, title: str, open_text: str, parent=None): super().__init__(parent) self._assets: List[ImageAsset] = [] self._root_folder: Optional[Path] = None self._current_preview_row: int = -1 + self._start_dir: str = "" # where the folder picker opens self._setup_ui(title, open_text) # ── UI construction ──────────────────────────────────────────────── @@ -238,10 +240,16 @@ class _AssetPanel(QWidget): # ── folder scanning ──────────────────────────────────────────────── + def set_start_dir(self, path: str): + """Set where the folder picker opens (restored last-used folder).""" + self._start_dir = path or "" + def _open_folder(self): - folder = QFileDialog.getExistingDirectory(self, "选择文件夹") + folder = QFileDialog.getExistingDirectory(self, "选择文件夹", self._start_dir) if not folder: return + self._start_dir = folder + self.folder_opened.emit(folder) self._root_folder = Path(folder) try: self._assets = scan_image_folder(self._root_folder) @@ -251,6 +259,9 @@ class _AssetPanel(QWidget): logger.info("Loaded %d asset(s) from %s", len(self._assets), folder) self._apply_sort() self.assets_changed.emit(list(self._assets)) + # Auto-preview the first image so the canvas isn't empty after loading + if self._assets: + self._select_preview_row(0) # ── sorting & list rebuild ───────────────────────────────────────── @@ -284,7 +295,12 @@ class _AssetPanel(QWidget): # ── interaction ──────────────────────────────────────────────────── def _on_item_clicked(self, item: QListWidgetItem): - row = self._list.row(item) + self._select_preview_row(self._list.row(item)) + + def _select_preview_row(self, row: int): + """Highlight the row as the current preview and emit preview_changed.""" + if not (0 <= row < self._list.count()): + return # Clear previous preview highlight if 0 <= self._current_preview_row < self._list.count(): @@ -295,7 +311,7 @@ class _AssetPanel(QWidget): w.set_preview_selected(False) self._current_preview_row = row - widget = self._list.itemWidget(item) + widget = self._list.itemWidget(self._list.item(row)) if widget: widget.set_preview_selected(True) self.preview_changed.emit(widget.asset) @@ -357,6 +373,8 @@ class ImageListPanel(QWidget): print_preview_changed = Signal(object) # ImageAsset | None garments_changed = Signal(list) # List[ImageAsset] prints_changed = Signal(list) # List[ImageAsset] + garment_folder_opened = Signal(str) # last-used garment folder + print_folder_opened = Signal(str) # last-used print folder def __init__(self, parent=None): super().__init__(parent) @@ -378,6 +396,8 @@ class ImageListPanel(QWidget): self._print_panel.preview_changed.connect(self.print_preview_changed) self._garment_panel.assets_changed.connect(self.garments_changed) self._print_panel.assets_changed.connect(self.prints_changed) + self._garment_panel.folder_opened.connect(self.garment_folder_opened) + self._print_panel.folder_opened.connect(self.print_folder_opened) splitter.addWidget(self._garment_panel) splitter.addWidget(self._print_panel) @@ -456,3 +476,9 @@ class ImageListPanel(QWidget): def get_prints(self) -> List[ImageAsset]: return self._print_panel.get_assets() + + def set_garment_start_dir(self, path: str): + self._garment_panel.set_start_dir(path) + + def set_print_start_dir(self, path: str): + self._print_panel.set_start_dir(path) diff --git a/src/app/widgets/queue_panel.py b/src/app/widgets/queue_panel.py index 02ec1cf..14d3861 100644 --- a/src/app/widgets/queue_panel.py +++ b/src/app/widgets/queue_panel.py @@ -23,7 +23,11 @@ from PySide6.QtWidgets import ( from core.composer import compose from core.models import BatchMode, ExportOptions, ImageAsset, TransformState -from services.file_service import get_output_dir, make_safe_output_path +from services.file_service import ( + get_output_dir, + make_safe_output_path, + timestamped_run_dir, +) logger = logging.getLogger(__name__) @@ -351,7 +355,7 @@ class QueuePanel(QWidget): row = rows[0].row() item = self._model.get_item(row) if item and item.status == "待导出": - self._export_item(row, item) + self._export_item(row, item, self._new_run_dir()) self._update_stats() def _toggle_batch(self): @@ -360,24 +364,30 @@ class QueuePanel(QWidget): else: self._start_batch() + def _new_run_dir(self): + """Timestamped folder shared by all images in one export run.""" + base = self._export_options.output_dir or str(get_output_dir()) + return timestamped_run_dir(base) + def _start_batch(self): self._running = True self._batch_btn.setText("停止") items = self._model._items + run_dir = self._new_run_dir() # one timestamp for the whole batch for i, item in enumerate(items): if not self._running: break if item.status != "待导出": continue - self._export_item(i, item) + self._export_item(i, item, run_dir) self._update_stats() QApplication.processEvents() self._running = False self._batch_btn.setText("开始批量导出") - def _export_item(self, row: int, item: QueueItem): + def _export_item(self, row: int, item: QueueItem, run_dir): transform = item.transform or self._template_transform if not transform: item.status = "失败" @@ -385,14 +395,9 @@ class QueuePanel(QWidget): self._model.update_row(row) return - out_dir = ( - self._export_options.output_dir - if self._export_options.output_dir - else str(get_output_dir()) - ) fmt = self._export_options.output_format or "PNG" output_path = make_safe_output_path( - out_dir, item.garment.path, item.print_asset.path, fmt + run_dir, item.garment.path, item.print_asset.path, fmt ) item.status = "导出中" diff --git a/src/core/batch.py b/src/core/batch.py index 606fea2..8c13002 100644 --- a/src/core/batch.py +++ b/src/core/batch.py @@ -11,7 +11,7 @@ from core.models import ( ImageAsset, TransformState, ) -from services.file_service import get_output_dir, make_safe_output_path +from services.file_service import get_output_dir, make_safe_output_path, timestamped_run_dir logger = logging.getLogger(__name__) @@ -38,7 +38,9 @@ def run_batch( results = [] export_options = options.export_options - output_dir = Path(export_options.output_dir) if export_options.output_dir else get_output_dir() + base_dir = Path(export_options.output_dir) if export_options.output_dir else get_output_dir() + # One timestamped run folder for the whole batch + output_dir = timestamped_run_dir(base_dir) logger.info( "Batch started: mode=%s, garments=%d, prints=%d, tasks=%d", diff --git a/src/services/file_service.py b/src/services/file_service.py index e2c1d47..c7d9a3e 100644 --- a/src/services/file_service.py +++ b/src/services/file_service.py @@ -1,5 +1,6 @@ import logging import sys +from datetime import datetime from pathlib import Path from typing import List @@ -102,20 +103,32 @@ def scan_image_folder(folder) -> List: # 安全输出文件名 # --------------------------------------------------------------------------- +def timestamped_run_dir(base_dir): + """Return base_dir/ for grouping one export run. + + Compute this once per export run (a batch, or a single export) and pass the + result as the output_dir to make_safe_output_path, so repeated runs land in + separate timestamped folders instead of mixing together. + """ + return Path(base_dir) / datetime.now().strftime("%Y%m%d_%H%M%S") + + def make_safe_output_path(output_dir, garment_path, print_path, output_format="PNG"): """Return an output Path that will not overwrite an existing file. - Filename pattern : _. + Outputs are grouped by print into a subfolder named after the print file: + //. If that path exists, appends _1, _2, ... until a free slot is found. + (The composer creates the subfolder when saving.) """ - output_dir = Path(output_dir) ext = ".png" if output_format.upper() == "PNG" else ".jpg" - stem = "{}_{}".format(Path(garment_path).stem, Path(print_path).stem) + target_dir = Path(output_dir) / Path(print_path).stem + stem = Path(garment_path).stem - candidate = output_dir / (stem + ext) + candidate = target_dir / (stem + ext) counter = 1 while candidate.exists(): - candidate = output_dir / ("{}_{}{}".format(stem, counter, ext)) + candidate = target_dir / ("{}_{}{}".format(stem, counter, ext)) counter += 1 return candidate diff --git a/src/services/template_service.py b/src/services/template_service.py index 23ee83e..ee420f4 100644 --- a/src/services/template_service.py +++ b/src/services/template_service.py @@ -15,25 +15,25 @@ _TEMPLATES_FILENAME = "templates.json" BUILTIN_TEMPLATES: List[Template] = [ Template( name="正方形模板", - x_ratio=0.34, y_ratio=0.22, - width_ratio=0.32, height_ratio=0.32, + x_ratio=0.40, y_ratio=0.40, + width_ratio=0.25, height_ratio=0.25, rotation=0.0, type="builtin", ), Template( name="纵向长方形模板", - x_ratio=0.35, y_ratio=0.16, - width_ratio=0.30, height_ratio=0.46, + x_ratio=0.435, y_ratio=0.385, + width_ratio=0.20, height_ratio=0.30, rotation=0.0, type="builtin", ), Template( name="横向长方形模板", - x_ratio=0.27, y_ratio=0.26, - width_ratio=0.46, height_ratio=0.30, + x_ratio=0.385, y_ratio=0.435, + width_ratio=0.30, height_ratio=0.20, rotation=0.0, type="builtin", ), Template( name="左胸小号模板", - x_ratio=0.50, y_ratio=0.22, + x_ratio=0.55, y_ratio=0.30, width_ratio=0.16, height_ratio=0.16, rotation=0.0, type="builtin", ), diff --git a/tasks.md b/tasks.md index dbde080..e95a8be 100644 --- a/tasks.md +++ b/tasks.md @@ -686,6 +686,84 @@ - [x] 系统窗口标题仍显示 `APP_NAME` 与 `APP_VERSION` - [x] 工作区因移除标题栏而获得更多纵向空间 +### 17.8 加载文件夹后默认预览第一张 + +前置阅读: + +- `docs/07-ui-design.md`(5.1 衣服图片面板) + +任务: + +- [x] 衣服、印花面板加载文件夹后,自动选中并预览第一张(按当前排序) +- [x] 空文件夹不选;把选中逻辑抽成按行选择以复用 +- [x] 与现有「模板粘性」衔接:自动选印花后按当前模板落位 + +验收: + +- [x] 加载文件夹后画布不再为空、无需手动点选 +- [x] 两个文件夹加载顺序无关,最终都能得到合成预览 + +### 17.9 输出按「时间戳 → 印花」分组到子文件夹 + +前置阅读: + +- `docs/02-prd.md`(6.8 单张合成导出、9 数据与文件结构) + +任务: + +- [x] `make_safe_output_path` 输出到 `<输出目录>/<印花文件名>/<衣服文件名>.<扩展名>` +- [x] 新增 `timestamped_run_dir(base)`,每次导出运行在输出目录下新建时间戳文件夹 +- [x] 一次导出运行(批量/单张)共用同一个时间戳;批量在开始时计算一次 +- [x] 单张导出、队列导出、`core.batch.run_batch` 均接入时间戳层 +- [x] 子文件夹由 `composer.compose` 保存时自动创建;重名仍追加 `_1`/`_2` + +验收: + +- [x] 路径形如 `输出目录/<时间戳>/<印花文件名>/<衣服文件名>.<扩展名>` +- [x] 重复运行落在不同时间戳文件夹,互不覆盖 +- [x] 一次批量运行内所有图片共用同一个时间戳文件夹 + +### 17.10 记住上次的衣服/印花文件夹 + +前置阅读: + +- `docs/02-prd.md`(6.1 图片加载、9 数据与文件结构) +- `docs/05-project-architecture.md`(4.12 config_service) + +任务: + +- [x] `_AssetPanel` 打开对话框时定位到上次目录(`set_start_dir`),并在选中后发 `folder_opened` 信号 +- [x] `ImageListPanel` 暴露 `set_garment_start_dir` / `set_print_start_dir` 与 `garment_folder_opened` / `print_folder_opened` +- [x] `main_window` 启动恢复 `last_garment_dir` / `last_print_dir`,并在打开文件夹时「改一次存一次」回写 +- [x] UI 不直接读写配置,统一经主窗口 + +验收: + +- [x] 重启后打开「打开文件夹」对话框定位到上次的衣服/印花目录 +- [x] 目录已不存在时安全回退,不报错 + +### 17.11 修复:批量导出忽略所选输出格式/质量/目录 + +前置阅读: + +- `docs/07-ui-design.md`(7.5 输出设置区域、8 底部合成队列) + +问题: + +- `queue_panel.set_export_options()` 从未被调用,导出面板也无「选项变化」信号,导致队列始终使用默认 `ExportOptions`(PNG、默认目录),批量导出无视 UI 所选格式/质量/目录。 + +任务: + +- [x] 导出面板新增 `export_options_changed` 信号与公开 `current_options()` +- [x] 格式/质量/输出目录任一变化即发出当前 `ExportOptions` +- [x] `main_window` 接到 `queue_panel.set_export_options`,并在启动时同步一次 +- [x] 单张导出 `_do_export` 改用同一 `current_options()` 来源 + +验收: + +- [x] 选 JPG 后批量导出的文件为 `.jpg` 且按 JPEG 保存 +- [x] 批量导出跟随所选输出目录与质量 + ## 18. 后续暂缓任务 以下任务第一阶段暂不做,后续需要时再新增设计文档: