feat: add semantic risk indicators

This commit is contained in:
chengma
2026-07-02 09:06:52 +08:00
parent ba6f3e4af4
commit 9bad70f425
6 changed files with 65 additions and 16 deletions
+36 -2
View File
@@ -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 QColor, QPixmap from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
@@ -137,6 +137,34 @@ if QT_IMPORT_ERROR is None:
) )
def _login_status_display(status):
return f"● {status or '未知'}"
def _login_status_color(status):
status_text = str(status or "")
if status_text == "已登录":
return _qcolor(COLOR_SUCCESS)
if status_text == "检测中":
return _qcolor(COLOR_INFO)
if "未登录" in status_text or "失败" in status_text:
return _qcolor(COLOR_DANGER)
return _qcolor(COLOR_MUTED)
def _warning_dot_icon(size=12):
pixmap = QPixmap(size, size)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(COLOR_WARNING))
margin = max(1, size // 6)
painter.drawEllipse(margin, margin, size - margin * 2, size - margin * 2)
painter.end()
return QIcon(pixmap)
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)
@@ -2113,6 +2141,7 @@ if QT_IMPORT_ERROR is None:
self.batch_filter.setObjectName("collectBatchFilter") self.batch_filter.setObjectName("collectBatchFilter")
self.delete_batch_button = QPushButton("删除批次") self.delete_batch_button = QPushButton("删除批次")
self.delete_batch_button.setObjectName("deleteBatchButton") self.delete_batch_button.setObjectName("deleteBatchButton")
self.delete_batch_button.setStyleSheet(_danger_outline_button_style("deleteBatchButton"))
self.delete_batch_button.setEnabled(False) self.delete_batch_button.setEnabled(False)
toolbar = QHBoxLayout() toolbar = QHBoxLayout()
@@ -5286,6 +5315,8 @@ if QT_IMPORT_ERROR is None:
self.add_button = QPushButton("新增") self.add_button = QPushButton("新增")
self.edit_button = QPushButton("编辑") self.edit_button = QPushButton("编辑")
self.delete_button = QPushButton("删除") self.delete_button = QPushButton("删除")
self.delete_button.setObjectName("deleteAccountButton")
self.delete_button.setStyleSheet(_danger_outline_button_style("deleteAccountButton"))
self.launch_button = QPushButton("启动登录") self.launch_button = QPushButton("启动登录")
self.check_button = QPushButton("检测登录") self.check_button = QPushButton("检测登录")
self.shortcut_button = QPushButton("快捷方式") self.shortcut_button = QPushButton("快捷方式")
@@ -5363,11 +5394,13 @@ if QT_IMPORT_ERROR is None:
account.alias, account.alias,
account.region_host, account.region_host,
str(account.debug_port), str(account.debug_port),
status, _login_status_display(status),
account.note or "", account.note or "",
] ]
for column, value in enumerate(values): for column, value in enumerate(values):
item = QTableWidgetItem(value) item = QTableWidgetItem(value)
if column == 4:
item.setForeground(_login_status_color(status))
self.table.setItem(row, column, item) self.table.setItem(row, column, item)
self.empty_label.setText("" if self.account_rows else "暂无账号") self.empty_label.setText("" if self.account_rows else "暂无账号")
self._update_button_state() self._update_button_state()
@@ -5617,6 +5650,7 @@ if QT_IMPORT_ERROR is None:
self.tabs.currentChanged.connect(self._on_tab_changed) self.tabs.currentChanged.connect(self._on_tab_changed)
for title in TAB_TITLES: for title in TAB_TITLES:
self.tabs.addTab(self._build_tab(title), title) self.tabs.addTab(self._build_tab(title), title)
self.tabs.setTabIcon(TAB_TITLES.index("③ 更新shopee"), _warning_dot_icon())
self.setCentralWidget(self.tabs) self.setCentralWidget(self.tabs)
self.statusBar().showMessage("就绪") self.statusBar().showMessage("就绪")
+1 -1
View File
@@ -96,7 +96,7 @@
| 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` 无回归 | 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` 无回归 | DONE |
| T-512 | ③高风险按钮上色 + ①导入校验数字标红 | T-511, T-508, T-202b | ③「开始更新」(写线上)改用 warning `#bc4c00` 系强调(描边或填充;第一版不强制图标,避免 Windows/Qt 字体渲染不稳),与②安全流程 info 蓝主操作区分;「停止」保持中性。①导入汇总栏:未匹配数 >0 时用 danger 且保持现有按钮可点击筛出,=0 保持中性;无效行数 >0 时只标红提示,不承诺在任务表筛出,因为脏行未入库,后续若要点击查看需另做「导入错误明细」。颜色取自 T-511 色板常量 | DONE | | 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 色板常量 | DONE |
| 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 |
| T-516 | ①筛选对齐②③ | T-202, T-303b | 依据 `docs/ux-review.md` P2。① 导入采集补齐与②③一致的店铺/商品ID/状态筛选(至少店铺),保持三个列表页筛选心智一致;不改导入汇总栏与未匹配筛出逻辑,只扩展筛选维度 | TODO | | T-516 | ①筛选对齐②③ | T-202, T-303b | 依据 `docs/ux-review.md` P2。① 导入采集补齐与②③一致的店铺/商品ID/状态筛选(至少店铺),保持三个列表页筛选心智一致;不改导入汇总栏与未匹配筛出逻辑,只扩展筛选维度 | TODO |
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -59,15 +59,15 @@
### 4)登录状态点(④账号管理) ### 4)登录状态点(④账号管理)
- `● 已登录` = success `#1a7f37`;`● 未登录 / 未检测` = muted `#6e7781` 或 danger(未登录用 danger 更醒目)。文字保留,圆点只是加速扫读。 - `● 已登录` = success `#1a7f37`;`● 未登录 / 检测失败` = danger `#cf222e`;`● 未检测 / 已启动 / 检测中` = muted `#6e7781` 或 info `#0969da`。落地时优先在④账号表登录状态单元格显示 `● 状态文本`,并通过 `QTableWidgetItem.setForeground(QColor(...))` 上色,保留文字,不只靠颜色。
### 5)③ Tab 危险区标识(克制) ### 5)③ Tab 危险区标识(克制)
- 给 ③ 更新shopee 加一个**克制**的暖色标识时,不依赖 QSS 硬选第 3 个 Tab。普通 `QTabBar` stylesheet 没有稳定的 nth-tab 选择器;优先用 `QTabWidget.setTabIcon(2, ...)` 设置 warning 小圆点图标,或第一版先跳过 Tab 标识。目的:让用户始终知道自己在"碰线上"的那个 Tab。切忌整条 Tab 刷红。 - 给 ③ 更新shopee 加一个**克制**的暖色标识时,不依赖 QSS 硬选第 3 个 Tab。普通 `QTabBar` stylesheet 没有稳定的 nth-tab 选择器;本轮 T-513 采用 `QTabWidget.setTabIcon(2, ...)` 设置 10-12px warning 小圆点图标,Tab 文案仍保持 `③ 更新shopee`,不改文字色、不整条刷红。目的:让用户始终知道自己在"碰线上"的那个 Tab。
### 6)破坏性按钮(低优先) ### 6)破坏性按钮(低优先)
- `删除批次`(①)、`删除账号`(④)等不可逆动作:按钮文字或描边用 danger `#cf222e`,与普通灰按钮区分。二次确认弹窗仍是主防线,颜色只是二级提示。 - `删除批次`(①)、`删除账号`(④)等不可逆动作:按钮文字和描边用 danger `#cf222e`,与普通灰按钮区分;保持原有启用/禁用和二次确认逻辑不变。二次确认弹窗仍是主防线,颜色只是二级提示。
## 实现映射(PySide6,落地时参考) ## 实现映射(PySide6,落地时参考)
+7
View File
@@ -946,3 +946,10 @@
- 测试:`tests/test_gui.py` 新增开始更新按钮 warning 样式、未匹配按钮 danger 样式、无效行 rich text danger 标记断言。 - 测试:`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 字体目录提示,不影响结果。 - 验证:`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危险标识 / 破坏性按钮上色」。 - 下一步:T-513「登录点 / ③Tab危险标识 / 破坏性按钮上色」。
## 【2026-07-02】T-513 完成 · 登录点、③Tab风险标识与破坏性按钮上色
- 文档先行:补充 `docs/ui-color-design.md` 的 T-513 落地口径,明确④登录状态列显示 `● 状态` 并用 `QTableWidgetItem.setForeground(QColor(...))` 上色;③更新shopee Tab 用 `QTabWidget.setTabIcon(2, ...)` 的小 warning 圆点,不改 Tab 文案/文字色;删除类按钮只用 danger 文字/描边,二次确认仍是主防线。
- 代码:`app/gui.py` 新增 `_login_status_display()`、`_login_status_color()`、`_warning_dot_icon()`;④账号管理登录状态列按状态着色(已登录 success、检测中 info、未登录/检测失败 danger、未知/已启动 muted);③更新shopee Tab 设置 warning 小圆点图标;①「删除批次」和④「删除账号」复用 danger 描边样式。
- 测试:`tests/test_gui.py` 补充主窗口 ③Tab icon、账号登录状态文本与颜色、删除账号/删除批次按钮 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-514「①②③ 首次空状态引导卡片」。
+9 -2
View File
@@ -94,6 +94,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
) )
self.assertEqual("就绪", window.statusBar().currentMessage()) self.assertEqual("就绪", window.statusBar().currentMessage())
self.assertEqual(TAB_STYLE, window.tabs.styleSheet()) self.assertEqual(TAB_STYLE, window.tabs.styleSheet())
self.assertFalse(window.tabs.tabIcon(TAB_TITLES.index("③ 更新shopee")).isNull())
self.assertIn("min-width: 128px", window.tabs.styleSheet()) self.assertIn("min-width: 128px", window.tabs.styleSheet())
self.assertIn("padding: 8px 18px", window.tabs.styleSheet()) self.assertIn("padding: 8px 18px", window.tabs.styleSheet())
self.assertIn("margin-right: 8px", window.tabs.styleSheet()) self.assertIn("margin-right: 8px", window.tabs.styleSheet())
@@ -2007,7 +2008,10 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual(1, tab.table.rowCount()) self.assertEqual(1, tab.table.rowCount())
self.assertEqual("主店", tab.table.item(0, 0).text()) self.assertEqual("主店", tab.table.item(0, 0).text())
self.assertEqual("alias", tab.table.item(0, 1).text()) self.assertEqual("alias", tab.table.item(0, 1).text())
self.assertEqual("未知", tab.table.item(0, 4).text()) self.assertEqual("● 未知", tab.table.item(0, 4).text())
self.assertEqual(gui.COLOR_MUTED, tab.table.item(0, 4).foreground().color().name())
self.assertEqual("deleteAccountButton", tab.delete_button.objectName())
self.assertIn(gui.COLOR_DANGER, tab.delete_button.styleSheet())
visible_values = [ visible_values = [
tab.table.item(0, column).text() tab.table.item(0, column).text()
for column in range(tab.table.columnCount()) for column in range(tab.table.columnCount())
@@ -2093,7 +2097,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
{"alias": "alias", "status": {"logged_in": True, "reason": None}} {"alias": "alias", "status": {"logged_in": True, "reason": None}}
) )
self.assertEqual("已登录", tab.table.item(0, 4).text()) self.assertEqual("● 已登录", tab.table.item(0, 4).text())
self.assertEqual(gui.COLOR_SUCCESS, tab.table.item(0, 4).foreground().color().name())
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
@@ -2157,6 +2162,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
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.assert_foreground(tab.model, 0, 3, gui.COLOR_PENDING)
self.assertEqual("deleteBatchButton", tab.delete_batch_button.objectName())
self.assertIn(gui.COLOR_DANGER, tab.delete_batch_button.styleSheet())
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())