feat: apply gui semantic risk colors
This commit is contained in:
+93
-3
@@ -10,7 +10,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
|
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt
|
||||||
from PySide6.QtGui import QPixmap
|
from PySide6.QtGui import QColor, QPixmap
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QApplication,
|
QApplication,
|
||||||
@@ -83,6 +83,13 @@ QTabBar::tab:hover:!selected {
|
|||||||
background: #eaf2ff;
|
background: #eaf2ff;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
COLOR_SUCCESS = "#1a7f37"
|
||||||
|
COLOR_DANGER = "#cf222e"
|
||||||
|
COLOR_INFO = "#0969da"
|
||||||
|
COLOR_PENDING = "#9a6700"
|
||||||
|
COLOR_MUTED = "#6e7781"
|
||||||
|
COLOR_WARNING = "#bc4c00"
|
||||||
|
COLOR_DANGER_BG = "#ffebe9"
|
||||||
|
|
||||||
|
|
||||||
if QT_IMPORT_ERROR is None:
|
if QT_IMPORT_ERROR is None:
|
||||||
@@ -101,6 +108,35 @@ if QT_IMPORT_ERROR is None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _qcolor(color):
|
||||||
|
return QColor(color)
|
||||||
|
|
||||||
|
|
||||||
|
def _status_base_color(status):
|
||||||
|
if status == "running":
|
||||||
|
return COLOR_INFO
|
||||||
|
if status == "failed":
|
||||||
|
return COLOR_DANGER
|
||||||
|
if status in {"skipped", "cancelled"}:
|
||||||
|
return COLOR_MUTED
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _danger_metric_text(text, active):
|
||||||
|
if not active:
|
||||||
|
return text
|
||||||
|
return f'<span style="color:{COLOR_DANGER}; font-weight:600;">{text}</span>'
|
||||||
|
|
||||||
|
|
||||||
|
def _danger_outline_button_style(object_name):
|
||||||
|
return (
|
||||||
|
f"QPushButton#{object_name} {{ "
|
||||||
|
f"color: {COLOR_DANGER}; border: 1px solid {COLOR_DANGER}; "
|
||||||
|
"font-weight: 600; padding: 3px 10px; border-radius: 4px; "
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _database_path(db_path=None, config=None) -> str:
|
def _database_path(db_path=None, config=None) -> str:
|
||||||
return db_path or appconfig.db_path(config)
|
return db_path or appconfig.db_path(config)
|
||||||
|
|
||||||
@@ -298,6 +334,8 @@ if QT_IMPORT_ERROR is None:
|
|||||||
task = self.tasks[index.row()]
|
task = self.tasks[index.row()]
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
return self._display_value(task, index.column())
|
return self._display_value(task, index.column())
|
||||||
|
if role == Qt.ForegroundRole and index.column() == 3:
|
||||||
|
return self._stage_color(task)
|
||||||
if role == Qt.ToolTipRole and self.is_unmatched(task):
|
if role == Qt.ToolTipRole and self.is_unmatched(task):
|
||||||
return "别名未匹配账号,采集时将略过"
|
return "别名未匹配账号,采集时将略过"
|
||||||
return None
|
return None
|
||||||
@@ -331,6 +369,16 @@ if QT_IMPORT_ERROR is None:
|
|||||||
return self.STATUS_TEXT[task.status]
|
return self.STATUS_TEXT[task.status]
|
||||||
return self.STAGE_TEXT.get(task.stage, task.stage)
|
return self.STAGE_TEXT.get(task.stage, task.stage)
|
||||||
|
|
||||||
|
def _stage_color(self, task):
|
||||||
|
if self.is_unmatched(task):
|
||||||
|
return _qcolor(COLOR_MUTED)
|
||||||
|
base_color = _status_base_color(getattr(task, "status", None))
|
||||||
|
if base_color is not None:
|
||||||
|
return _qcolor(base_color)
|
||||||
|
if getattr(task, "stage", None) in {"collected", "generated", "applied"}:
|
||||||
|
return _qcolor(COLOR_SUCCESS)
|
||||||
|
return _qcolor(COLOR_PENDING)
|
||||||
|
|
||||||
def _display_value(self, task, column):
|
def _display_value(self, task, column):
|
||||||
values = [
|
values = [
|
||||||
self._account_name(task),
|
self._account_name(task),
|
||||||
@@ -397,6 +445,8 @@ if QT_IMPORT_ERROR is None:
|
|||||||
task = self.tasks[index.row()]
|
task = self.tasks[index.row()]
|
||||||
if role in (Qt.DisplayRole, Qt.EditRole):
|
if role in (Qt.DisplayRole, Qt.EditRole):
|
||||||
return self._display_value(task, index.column())
|
return self._display_value(task, index.column())
|
||||||
|
if role == Qt.ForegroundRole and index.column() == 4:
|
||||||
|
return self._status_color(task)
|
||||||
if role == Qt.ToolTipRole:
|
if role == Qt.ToolTipRole:
|
||||||
if index.column() == 3 and self._can_edit_title(task):
|
if index.column() == 3 and self._can_edit_title(task):
|
||||||
return "双击可微调新标题,只修改本地待更新内容"
|
return "双击可微调新标题,只修改本地待更新内容"
|
||||||
@@ -466,6 +516,14 @@ if QT_IMPORT_ERROR is None:
|
|||||||
return self.STATUS_TEXT[task.status]
|
return self.STATUS_TEXT[task.status]
|
||||||
return self.STAGE_TEXT.get(task.stage, task.stage)
|
return self.STAGE_TEXT.get(task.stage, task.stage)
|
||||||
|
|
||||||
|
def _status_color(self, task):
|
||||||
|
base_color = _status_base_color(getattr(task, "status", None))
|
||||||
|
if base_color is not None:
|
||||||
|
return _qcolor(base_color)
|
||||||
|
if getattr(task, "stage", None) in {"generated", "applied"}:
|
||||||
|
return _qcolor(COLOR_SUCCESS)
|
||||||
|
return _qcolor(COLOR_PENDING)
|
||||||
|
|
||||||
def _display_value(self, task, column):
|
def _display_value(self, task, column):
|
||||||
values = [
|
values = [
|
||||||
self._account_name(task),
|
self._account_name(task),
|
||||||
@@ -525,6 +583,11 @@ if QT_IMPORT_ERROR is None:
|
|||||||
task = self.tasks[index.row()]
|
task = self.tasks[index.row()]
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
return self._display_value(task, index.column())
|
return self._display_value(task, index.column())
|
||||||
|
if role == Qt.ForegroundRole:
|
||||||
|
if index.column() == 4:
|
||||||
|
return self._stage_color(task)
|
||||||
|
if index.column() == 5:
|
||||||
|
return self._result_color(task)
|
||||||
if role == Qt.ToolTipRole and task.last_error:
|
if role == Qt.ToolTipRole and task.last_error:
|
||||||
return task.last_error
|
return task.last_error
|
||||||
return None
|
return None
|
||||||
@@ -568,6 +631,23 @@ if QT_IMPORT_ERROR is None:
|
|||||||
return "待更新"
|
return "待更新"
|
||||||
return self.STATUS_TEXT.get(task.status, task.status)
|
return self.STATUS_TEXT.get(task.status, task.status)
|
||||||
|
|
||||||
|
def _stage_color(self, task):
|
||||||
|
if getattr(task, "stage", None) == "applied":
|
||||||
|
return _qcolor(COLOR_SUCCESS)
|
||||||
|
return _qcolor(COLOR_PENDING)
|
||||||
|
|
||||||
|
def _result_color(self, task):
|
||||||
|
base_color = _status_base_color(getattr(task, "status", None))
|
||||||
|
if base_color is not None:
|
||||||
|
return _qcolor(base_color)
|
||||||
|
if getattr(task, "status", None) == "pending":
|
||||||
|
return _qcolor(COLOR_PENDING)
|
||||||
|
if getattr(task, "status", None) == "success" and getattr(task, "stage", None) == "generated":
|
||||||
|
return _qcolor(COLOR_PENDING)
|
||||||
|
if getattr(task, "stage", None) == "applied" or getattr(task, "status", None) == "success":
|
||||||
|
return _qcolor(COLOR_SUCCESS)
|
||||||
|
return _qcolor(COLOR_PENDING)
|
||||||
|
|
||||||
class GenerateTab(QWidget):
|
class GenerateTab(QWidget):
|
||||||
"""Tab 2: prompt area plus generation task filters/list."""
|
"""Tab 2: prompt area plus generation task filters/list."""
|
||||||
|
|
||||||
@@ -1316,7 +1396,10 @@ if QT_IMPORT_ERROR is None:
|
|||||||
self.start_update_button.setObjectName("startUpdateButton")
|
self.start_update_button.setObjectName("startUpdateButton")
|
||||||
self.start_update_button.setMinimumWidth(118)
|
self.start_update_button.setMinimumWidth(118)
|
||||||
self.start_update_button.setStyleSheet(
|
self.start_update_button.setStyleSheet(
|
||||||
"QPushButton#startUpdateButton { font-weight: 600; padding: 6px 16px; }"
|
"QPushButton#startUpdateButton { "
|
||||||
|
"font-weight: 600; padding: 6px 16px; "
|
||||||
|
f"color: {COLOR_WARNING}; border: 1px solid {COLOR_WARNING}; "
|
||||||
|
"border-radius: 4px; }"
|
||||||
)
|
)
|
||||||
self.stop_update_button = QPushButton("停止")
|
self.stop_update_button = QPushButton("停止")
|
||||||
self.reset_update_button = QPushButton("重置更新状态")
|
self.reset_update_button = QPushButton("重置更新状态")
|
||||||
@@ -2044,9 +2127,11 @@ if QT_IMPORT_ERROR is None:
|
|||||||
toolbar.addStretch(1)
|
toolbar.addStretch(1)
|
||||||
|
|
||||||
self.summary_label = QLabel("未导入任务")
|
self.summary_label = QLabel("未导入任务")
|
||||||
|
self.summary_label.setTextFormat(Qt.RichText)
|
||||||
self.match_detail_label = QLabel("")
|
self.match_detail_label = QLabel("")
|
||||||
self.show_all_button = QPushButton("全部")
|
self.show_all_button = QPushButton("全部")
|
||||||
self.show_unmatched_button = QPushButton("未匹配(0)")
|
self.show_unmatched_button = QPushButton("未匹配(0)")
|
||||||
|
self.show_unmatched_button.setObjectName("showUnmatchedButton")
|
||||||
|
|
||||||
summary_layout = QHBoxLayout()
|
summary_layout = QHBoxLayout()
|
||||||
summary_layout.addWidget(self.summary_label)
|
summary_layout.addWidget(self.summary_label)
|
||||||
@@ -2620,12 +2705,17 @@ if QT_IMPORT_ERROR is None:
|
|||||||
total = stats.get("total", len(task_rows))
|
total = stats.get("total", len(task_rows))
|
||||||
valid = stats.get("valid", len(task_rows))
|
valid = stats.get("valid", len(task_rows))
|
||||||
invalid = stats.get("invalid", 0)
|
invalid = stats.get("invalid", 0)
|
||||||
|
invalid_text = _danger_metric_text(f"无效{invalid}", invalid > 0)
|
||||||
|
unmatched_text = _danger_metric_text(f"未匹配{unmatched}", unmatched > 0)
|
||||||
self.summary_label.setText(
|
self.summary_label.setText(
|
||||||
f"{files} 文件 · {total} 行 · 有效{valid}/无效{invalid} · 匹配{matched} · 未匹配{unmatched}"
|
f"{files} 文件 · {total} 行 · 有效{valid}/{invalid_text} · 匹配{matched} · {unmatched_text}"
|
||||||
)
|
)
|
||||||
self.match_detail_label.setText(self._match_detail(task_rows, account_rows))
|
self.match_detail_label.setText(self._match_detail(task_rows, account_rows))
|
||||||
self.show_unmatched_button.setText(f"未匹配({unmatched})")
|
self.show_unmatched_button.setText(f"未匹配({unmatched})")
|
||||||
self.show_unmatched_button.setEnabled(unmatched > 0)
|
self.show_unmatched_button.setEnabled(unmatched > 0)
|
||||||
|
self.show_unmatched_button.setStyleSheet(
|
||||||
|
_danger_outline_button_style("showUnmatchedButton") if unmatched > 0 else ""
|
||||||
|
)
|
||||||
if unmatched == 0 and self.model.filter_mode == "unmatched":
|
if unmatched == 0 and self.model.filter_mode == "unmatched":
|
||||||
self.model.set_filter_mode("all")
|
self.model.set_filter_mode("all")
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -94,8 +94,8 @@
|
|||||||
| T-508 | ③ 更新shopee生产化操作区 | T-401, T-404a, T-506 | 「开始更新」作为主操作视觉强化;「重置更新状态」从底部按钮移到任务表右键菜单/高级入口,与批量更新物理分离,运行中禁用;更新安全拦截弹窗写明具体开关并提供「前往设置」深链到⑤,不改 Shopee/CDP 更新流程 | DONE |
|
| T-508 | ③ 更新shopee生产化操作区 | T-401, T-404a, T-506 | 「开始更新」作为主操作视觉强化;「重置更新状态」从底部按钮移到任务表右键菜单/高级入口,与批量更新物理分离,运行中禁用;更新安全拦截弹窗写明具体开关并提供「前往设置」深链到⑤,不改 Shopee/CDP 更新流程 | DONE |
|
||||||
| T-509 | ② 新标题人工微调 | T-303, T-401 | ②任务表「新标题」列可编辑;仅允许已生成、未提交线上、非运行中任务;编辑写回 `tasks.new_title`,保留 `stage=generated`,清空 `last_error`,状态回到可更新;不触碰 Shopee/CDP/Excel,③ 使用修改后的标题 | DONE |
|
| T-509 | ② 新标题人工微调 | T-303, T-401 | ②任务表「新标题」列可编辑;仅允许已生成、未提交线上、非运行中任务;编辑写回 `tasks.new_title`,保留 `stage=generated`,清空 `last_error`,状态回到可更新;不触碰 Shopee/CDP/Excel,③ 使用修改后的标题 | DONE |
|
||||||
| T-510 | ③ 检查本轮更新文案统一 | T-506, T-508 | 将用户可见「预览本轮更新」统一改为「检查本轮更新」,按钮、确认框、状态栏、完成弹窗、运行日志和文档一致;保留内部 `dry_run` 字段与执行语义,不改 Shopee/CDP 更新流程 | DONE |
|
| T-510 | ③ 检查本轮更新文案统一 | T-506, T-508 | 将用户可见「预览本轮更新」统一改为「检查本轮更新」,按钮、确认框、状态栏、完成弹窗、运行日志和文档一致;保留内部 `dry_run` 字段与执行语义,不改 Shopee/CDP 更新流程 | DONE |
|
||||||
| T-511 | 语义色板 + ①②③任务状态列上色 | T-202, T-303, T-401 | 依据 `docs/ui-color-design.md`。在 `app/gui.py` 顶部定义统一语义色板常量(success `#1a7f37`/danger `#cf222e`/info `#0969da`/pending `#9a6700`/muted `#6e7781`/warning `#bc4c00`),全局引用不散落硬编码。①②③ 三个 TableModel 在 `Qt.ForegroundRole` 按内部 `stage/status` 返回 `QColor`,不要按中文显示文案硬匹配:完成态=success、失败=danger、略过/取消=muted、待处理=pending、进行中=info;不给普通行刷底色(失败行浅底 `#ffebe9` 可选、仅此一种)。保持文字+颜色双编码,不只靠颜色。跑 `test_gui.py` 无回归 | TODO |
|
| T-511 | 语义色板 + ①②③任务状态列上色 | T-202, T-303, T-401 | 依据 `docs/ui-color-design.md`。在 `app/gui.py` 顶部定义统一语义色板常量(success `#1a7f37`/danger `#cf222e`/info `#0969da`/pending `#9a6700`/muted `#6e7781`/warning `#bc4c00`),全局引用不散落硬编码。①②③ 三个 TableModel 在 `Qt.ForegroundRole` 按内部 `stage/status` 返回 `QColor`,不要按中文显示文案硬匹配:完成态=success、失败=danger、略过/取消=muted、待处理=pending、进行中=info;不给普通行刷底色(失败行浅底 `#ffebe9` 可选、仅此一种)。保持文字+颜色双编码,不只靠颜色。跑 `test_gui.py` 无回归 | DONE |
|
||||||
| T-512 | ③高风险按钮上色 + ①导入校验数字标红 | T-511, T-508, T-202b | ③「开始更新」(写线上)改用 warning `#bc4c00` 系强调(描边或填充;第一版不强制图标,避免 Windows/Qt 字体渲染不稳),与②安全流程 info 蓝主操作区分;「停止」保持中性。①导入汇总栏:未匹配数 >0 时用 danger 且保持现有按钮可点击筛出,=0 保持中性;无效行数 >0 时只标红提示,不承诺在任务表筛出,因为脏行未入库,后续若要点击查看需另做「导入错误明细」。颜色取自 T-511 色板常量 | TODO |
|
| T-512 | ③高风险按钮上色 + ①导入校验数字标红 | T-511, T-508, T-202b | ③「开始更新」(写线上)改用 warning `#bc4c00` 系强调(描边或填充;第一版不强制图标,避免 Windows/Qt 字体渲染不稳),与②安全流程 info 蓝主操作区分;「停止」保持中性。①导入汇总栏:未匹配数 >0 时用 danger 且保持现有按钮可点击筛出,=0 保持中性;无效行数 >0 时只标红提示,不承诺在任务表筛出,因为脏行未入库,后续若要点击查看需另做「导入错误明细」。颜色取自 T-511 色板常量 | DONE |
|
||||||
| T-513 | 登录点 / ③Tab危险标识 / 破坏性按钮上色 | T-511, T-105, T-206 | ④登录状态 `●` 已登录=success、未登录/未检测=danger 或 muted(保留文字);③ 更新shopee Tab 若做危险标识,不依赖 QSS 硬选第 3 个 Tab,优先用 `QTabWidget.setTabIcon(2, ...)` 设置克制 warning 小圆点图标,或第一版先跳过 Tab 标识;不整条刷红。破坏性按钮「删除批次」「删除账号」文字/描边用 danger,与普通灰按钮区分(二次确认弹窗仍是主防线)。颜色取自 T-511 色板常量 | TODO |
|
| T-513 | 登录点 / ③Tab危险标识 / 破坏性按钮上色 | T-511, T-105, T-206 | ④登录状态 `●` 已登录=success、未登录/未检测=danger 或 muted(保留文字);③ 更新shopee Tab 若做危险标识,不依赖 QSS 硬选第 3 个 Tab,优先用 `QTabWidget.setTabIcon(2, ...)` 设置克制 warning 小圆点图标,或第一版先跳过 Tab 标识;不整条刷红。破坏性按钮「删除批次」「删除账号」文字/描边用 danger,与普通灰按钮区分(二次确认弹窗仍是主防线)。颜色取自 T-511 色板常量 | TODO |
|
||||||
| T-514 | ①②③ 首次空状态引导卡片 | T-205 | 依据 `docs/ux-review.md` P1。①②③ 在「无账号/未登录」时用显式空状态引导卡片替代灰按钮,例如「第一步:前往『④账号管理』配置并登录账号 →」并带跳转按钮;账号就绪后卡片消失,老用户无感。仅 UI 引导,不改采集/生成/更新流程与既有预检拦截逻辑 | TODO |
|
| T-514 | ①②③ 首次空状态引导卡片 | T-205 | 依据 `docs/ux-review.md` P1。①②③ 在「无账号/未登录」时用显式空状态引导卡片替代灰按钮,例如「第一步:前往『④账号管理』配置并登录账号 →」并带跳转按钮;账号就绪后卡片消失,老用户无感。仅 UI 引导,不改采集/生成/更新流程与既有预检拦截逻辑 | TODO |
|
||||||
| T-515 | 批次阶段进度总览 | T-206, T-401 | 依据 `docs/ux-review.md` P1。加轻量批次进度总览:按选中批次聚合各阶段计数(导入/已采集/已生成/已更新/失败/略过),落点可选①顶部进度条或独立概览区。数据由现有 `db.list_tasks(batch_id=...)` 按 `stage/status` 聚合,不新增表;已软删除批次不计入 | TODO |
|
| T-515 | 批次阶段进度总览 | T-206, T-401 | 依据 `docs/ux-review.md` P1。加轻量批次进度总览:按选中批次聚合各阶段计数(导入/已采集/已生成/已更新/失败/略过),落点可选①顶部进度条或独立概览区。数据由现有 `db.list_tasks(batch_id=...)` 按 `stage/status` 聚合,不新增表;已软删除批次不计入 | TODO |
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+14
@@ -932,3 +932,17 @@
|
|||||||
- CDP:`app/editor.py` 只给 `apply_task()` 增加可选 `on_step` 回调,用于上报 `open_product/change_title/replace_cover/click_update`,不改选择器、上传方式、确认框点击或提交逻辑。
|
- CDP:`app/editor.py` 只给 `apply_task()` 增加可选 `on_step` 回调,用于上报 `open_product/change_title/replace_cover/click_update`,不改选择器、上传方式、确认框点击或提交逻辑。
|
||||||
- 安全:DB 业务日志和本地 `logs/cmshopee.log` 均通过现有脱敏工具处理,不记录 Cookie、密码、API Key、token;异常路径本地日志保留脱敏 traceback。
|
- 安全:DB 业务日志和本地 `logs/cmshopee.log` 均通过现有脱敏工具处理,不记录 Cookie、密码、API Key、token;异常路径本地日志保留脱敏 traceback。
|
||||||
- 测试:新增/更新 `tests/test_gui.py` 覆盖 import/write_back/apply/chrome_launch/login_check/ai_model_test 运行日志和本地日志脱敏;`python -m py_compile app\gui.py app\editor.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(63 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(151 tests);`git diff --check` 无空白错误,仅 LF/CRLF 提示。
|
- 测试:新增/更新 `tests/test_gui.py` 覆盖 import/write_back/apply/chrome_launch/login_check/ai_model_test 运行日志和本地日志脱敏;`python -m py_compile app\gui.py app\editor.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(63 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(151 tests);`git diff --check` 无空白错误,仅 LF/CRLF 提示。
|
||||||
|
## 【2026-07-01】T-511 完成 · 语义色板与任务状态列上色
|
||||||
|
|
||||||
|
- 范围:按 `docs/ui-color-design.md` 落地第一步状态列上色,只改 GUI 展示层,不改 CDP、Shopee 提交流程、DB schema 或业务状态流转。
|
||||||
|
- 代码:`app/gui.py` 顶部新增统一语义色板常量(success/danger/info/pending/muted/warning/danger-bg),`TaskTableModel`、`GenerateTaskTableModel`、`ApplyTaskTableModel` 在状态相关列响应 `Qt.ForegroundRole`,按内部 `stage/status` 返回 `QColor`;完成态用 success、失败用 danger、略过/取消用 muted、待处理用 pending、进行中用 info,不按中文显示文案硬匹配,不给普通行刷底色。
|
||||||
|
- 测试:`tests/test_gui.py` 新增状态列前景色断言,覆盖①待采集/未匹配、②待生成/已生成/失败、③待更新/失败/已更新。
|
||||||
|
- 验证:`python -m py_compile app\gui.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(63 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(151 tests)。测试输出仍有 PySide6 字体目录提示,不影响结果。
|
||||||
|
- 下一步:T-512「③高风险按钮上色 + ①导入校验数字标红」。
|
||||||
|
## 【2026-07-01】T-512 完成 · 高风险按钮与导入校验数字标红
|
||||||
|
|
||||||
|
- 范围:按 `docs/ui-color-design.md` 落地第二步颜色优化,只改 GUI 样式层,不改 Tab 名、不改 CDP、Shopee 提交流程、DB schema 或业务状态流转。
|
||||||
|
- 代码:③「开始更新」按钮使用 warning `#bc4c00` 文字和描边,保留「停止」中性;①导入汇总使用 rich text 将无效行数 >0、未匹配数 >0 标为 danger `#cf222e`;未匹配按钮 >0 时同样使用 danger 描边/文字并保持可点击筛出,=0 时清空样式并禁用;无效行仅标红提示,不新增筛表入口。
|
||||||
|
- 测试:`tests/test_gui.py` 新增开始更新按钮 warning 样式、未匹配按钮 danger 样式、无效行 rich text danger 标记断言。
|
||||||
|
- 验证:`python -m py_compile app\gui.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p "test_gui.py"` 通过(63 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(151 tests)。测试输出仍有 PySide6 字体目录提示,不影响结果。
|
||||||
|
- 下一步:T-513「登录点 / ③Tab危险标识 / 破坏性按钮上色」。
|
||||||
|
|||||||
+24
-1
@@ -77,6 +77,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
def assert_foreground(self, model, row, column, color):
|
||||||
|
value = model.data(model.index(row, column), gui.Qt.ForegroundRole)
|
||||||
|
self.assertIsNotNone(value)
|
||||||
|
self.assertEqual(color, value.name())
|
||||||
|
|
||||||
def test_main_window_has_five_tabs_in_workflow_order(self):
|
def test_main_window_has_five_tabs_in_workflow_order(self):
|
||||||
with self.make_temp_dir() as temp_dir:
|
with self.make_temp_dir() as temp_dir:
|
||||||
window = MainWindow(config=self.make_config(temp_dir))
|
window = MainWindow(config=self.make_config(temp_dir))
|
||||||
@@ -983,6 +988,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertTrue(tab.reset_update_button.isHidden())
|
self.assertTrue(tab.reset_update_button.isHidden())
|
||||||
self.assertEqual(gui.Qt.CustomContextMenu, tab.task_table.contextMenuPolicy())
|
self.assertEqual(gui.Qt.CustomContextMenu, tab.task_table.contextMenuPolicy())
|
||||||
self.assertEqual("startUpdateButton", tab.start_update_button.objectName())
|
self.assertEqual("startUpdateButton", tab.start_update_button.objectName())
|
||||||
|
self.assertIn(gui.COLOR_WARNING, tab.start_update_button.styleSheet())
|
||||||
|
self.assertIn("border", tab.start_update_button.styleSheet())
|
||||||
self.assertFalse(tab.stop_update_button.isEnabled())
|
self.assertFalse(tab.stop_update_button.isEnabled())
|
||||||
self.assertEqual(2, tab.model.rowCount())
|
self.assertEqual(2, tab.model.rowCount())
|
||||||
self.assertEqual("主店", tab.model.index(0, 0).data())
|
self.assertEqual("主店", tab.model.index(0, 0).data())
|
||||||
@@ -990,12 +997,15 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertEqual("新标题10", tab.model.index(0, 2).data())
|
self.assertEqual("新标题10", tab.model.index(0, 2).data())
|
||||||
self.assertEqual("51100639510_new.jpg", tab.model.index(0, 3).data())
|
self.assertEqual("51100639510_new.jpg", tab.model.index(0, 3).data())
|
||||||
self.assertEqual("待更新", tab.model.index(0, 4).data())
|
self.assertEqual("待更新", tab.model.index(0, 4).data())
|
||||||
|
self.assert_foreground(tab.model, 0, 4, gui.COLOR_PENDING)
|
||||||
self.assertEqual("待更新", tab.model.index(0, 5).data())
|
self.assertEqual("待更新", tab.model.index(0, 5).data())
|
||||||
|
self.assert_foreground(tab.model, 0, 5, gui.COLOR_PENDING)
|
||||||
self.assertEqual("任务 2/4 条", tab.summary_label.text())
|
self.assertEqual("任务 2/4 条", tab.summary_label.text())
|
||||||
|
|
||||||
tab.status_filter.setCurrentIndex(tab.status_filter.findData("failed"))
|
tab.status_filter.setCurrentIndex(tab.status_filter.findData("failed"))
|
||||||
self.assertEqual(1, tab.model.rowCount())
|
self.assertEqual(1, tab.model.rowCount())
|
||||||
self.assertEqual("失败", tab.model.index(0, 5).data())
|
self.assertEqual("失败", tab.model.index(0, 5).data())
|
||||||
|
self.assert_foreground(tab.model, 0, 5, gui.COLOR_DANGER)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"更新失败",
|
"更新失败",
|
||||||
tab.model.data(tab.model.index(0, 0), gui.Qt.ToolTipRole),
|
tab.model.data(tab.model.index(0, 0), gui.Qt.ToolTipRole),
|
||||||
@@ -1011,7 +1021,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
tab.status_filter.setCurrentIndex(tab.status_filter.findData("applied"))
|
tab.status_filter.setCurrentIndex(tab.status_filter.findData("applied"))
|
||||||
self.assertEqual(1, tab.model.rowCount())
|
self.assertEqual(1, tab.model.rowCount())
|
||||||
self.assertEqual("已更新", tab.model.index(0, 4).data())
|
self.assertEqual("已更新", tab.model.index(0, 4).data())
|
||||||
|
self.assert_foreground(tab.model, 0, 4, gui.COLOR_SUCCESS)
|
||||||
self.assertEqual("成功", tab.model.index(0, 5).data())
|
self.assertEqual("成功", tab.model.index(0, 5).data())
|
||||||
|
self.assert_foreground(tab.model, 0, 5, gui.COLOR_SUCCESS)
|
||||||
|
|
||||||
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(None))
|
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(None))
|
||||||
tab.status_filter.setCurrentIndex(tab.status_filter.findData("all"))
|
tab.status_filter.setCurrentIndex(tab.status_filter.findData("all"))
|
||||||
@@ -1931,7 +1943,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertEqual("旧标题A", tab.model.index(0, 2).data())
|
self.assertEqual("旧标题A", tab.model.index(0, 2).data())
|
||||||
self.assertEqual("", tab.model.index(0, 3).data())
|
self.assertEqual("", tab.model.index(0, 3).data())
|
||||||
self.assertEqual("待生成", tab.model.index(0, 4).data())
|
self.assertEqual("待生成", tab.model.index(0, 4).data())
|
||||||
|
self.assert_foreground(tab.model, 0, 4, gui.COLOR_PENDING)
|
||||||
self.assertEqual("已生成", tab.model.index(2, 4).data())
|
self.assertEqual("已生成", tab.model.index(2, 4).data())
|
||||||
|
self.assert_foreground(tab.model, 2, 4, gui.COLOR_SUCCESS)
|
||||||
|
|
||||||
tab.status_filter.setCurrentIndex(tab.status_filter.findData("to_generate"))
|
tab.status_filter.setCurrentIndex(tab.status_filter.findData("to_generate"))
|
||||||
self.assertEqual(2, tab.model.rowCount())
|
self.assertEqual(2, tab.model.rowCount())
|
||||||
@@ -1946,6 +1960,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(batch_b))
|
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(batch_b))
|
||||||
self.assertEqual(1, tab.model.rowCount())
|
self.assertEqual(1, tab.model.rowCount())
|
||||||
self.assertEqual("失败", tab.model.index(0, 4).data())
|
self.assertEqual("失败", tab.model.index(0, 4).data())
|
||||||
|
self.assert_foreground(tab.model, 0, 4, gui.COLOR_DANGER)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"生成失败",
|
"生成失败",
|
||||||
tab.model.data(tab.model.index(0, 0), gui.Qt.ToolTipRole),
|
tab.model.data(tab.model.index(0, 0), gui.Qt.ToolTipRole),
|
||||||
@@ -2141,13 +2156,18 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertEqual("alias-a", tab.model.index(0, 1).data())
|
self.assertEqual("alias-a", tab.model.index(0, 1).data())
|
||||||
self.assertEqual("51100639510", tab.model.index(0, 2).data())
|
self.assertEqual("51100639510", tab.model.index(0, 2).data())
|
||||||
self.assertEqual("待采集", tab.model.index(0, 3).data())
|
self.assertEqual("待采集", tab.model.index(0, 3).data())
|
||||||
|
self.assert_foreground(tab.model, 0, 3, gui.COLOR_PENDING)
|
||||||
self.assertEqual("Excel副店", tab.model.index(1, 0).data())
|
self.assertEqual("Excel副店", tab.model.index(1, 0).data())
|
||||||
self.assertEqual("missing", tab.model.index(1, 1).data())
|
self.assertEqual("missing", tab.model.index(1, 1).data())
|
||||||
self.assertEqual("略过", tab.model.index(1, 3).data())
|
self.assertEqual("略过", tab.model.index(1, 3).data())
|
||||||
|
self.assert_foreground(tab.model, 1, 3, gui.COLOR_MUTED)
|
||||||
self.assertIn("2 行", tab.summary_label.text())
|
self.assertIn("2 行", tab.summary_label.text())
|
||||||
self.assertIn("有效2/无效0", tab.summary_label.text())
|
self.assertIn("有效2/无效0", tab.summary_label.text())
|
||||||
self.assertIn("匹配1", tab.summary_label.text())
|
self.assertIn("匹配1", tab.summary_label.text())
|
||||||
self.assertIn("未匹配1", tab.summary_label.text())
|
self.assertIn("未匹配1", tab.summary_label.text())
|
||||||
|
self.assertIn(gui.COLOR_DANGER, tab.summary_label.text())
|
||||||
|
self.assertIn(gui.COLOR_DANGER, tab.show_unmatched_button.styleSheet())
|
||||||
|
self.assertIn("border", tab.show_unmatched_button.styleSheet())
|
||||||
self.assertIn("主店1", tab.match_detail_label.text())
|
self.assertIn("主店1", tab.match_detail_label.text())
|
||||||
self.assertIn("未匹配", tab.empty_label.text())
|
self.assertIn("未匹配", tab.empty_label.text())
|
||||||
|
|
||||||
@@ -2326,8 +2346,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
self.assertEqual("alias-a", tab.model.index(0, 1).data())
|
self.assertEqual("alias-a", tab.model.index(0, 1).data())
|
||||||
self.assertIn("1 文件", tab.summary_label.text())
|
self.assertIn("1 文件", tab.summary_label.text())
|
||||||
self.assertIn("2 行", tab.summary_label.text())
|
self.assertIn("2 行", tab.summary_label.text())
|
||||||
self.assertIn("有效1/无效1", tab.summary_label.text())
|
self.assertIn("有效1/", tab.summary_label.text())
|
||||||
|
self.assertIn("无效1", tab.summary_label.text())
|
||||||
|
self.assertIn(gui.COLOR_DANGER, tab.summary_label.text())
|
||||||
self.assertIn("匹配1", tab.summary_label.text())
|
self.assertIn("匹配1", tab.summary_label.text())
|
||||||
|
self.assertEqual("", tab.show_unmatched_button.styleSheet())
|
||||||
self.assertIn("入库1", statuses[-1])
|
self.assertIn("入库1", statuses[-1])
|
||||||
|
|
||||||
self.assert_removed(temp_dir)
|
self.assert_removed(temp_dir)
|
||||||
|
|||||||
Reference in New Issue
Block a user