CM-1077.3 — 前端:把「修正來源」與「刪除」接進操作 menu

Repo:FE | 前置:CM-1077.1(判定端點)+ CM-1077.2(寫入端點) | 母卡:CM-1077


§1

本卡範圍

在檢測基準管理頁既有的操作 menu 加兩個項目:修正來源 / 刪除

母卡 CM-1077 的主張是:「能不能改、能不能刪」由客觀事實決定,不是問使用者想不想

  • 從未被使用過 → 可以就地修正來源(版號不動、重抽控制項)、可以刪除
  • 已被使用過 → 一律凍結,只能建新版本;刪除改為停用

.1 提供判定端點(回「有沒有被用過、被誰用」),.2 提供寫入端點(修正來源 / 刪除)。本卡只做前端接線與 UX,不碰 BE。

前置檢查(開工第一件事)

  1. 確認 .1.2 都已完成並可打通——本卡沒有它們就無事可做。跟派工者要端點路徑與回應格式,或直接看 BE repo 的 route 定義。
  2. 確認 CM-1078(清單操作按鈕收進 menu)已驗收。

CM-1078 已落地(協調者 2026-08-04 實查):DetectionProfileManageView.vue 的 menu 結構已在,buildActionMenuItems(row)command: () => openEdit(row) 這種形式組項目,已含 openDetail/openControls/openEdit/openNewVersion/openFork/openCopyToTool/handleDeactivate。所以本卡直接加 menu item 即可,不需要先重構。


§2

🔴 關鍵 UX 規則(決策者已裁定,不可自行改動)

① 不可用的動作要「灰掉 + 說明原因」,不是按下去才報錯

開 menu 前先打 .1 的判定端點。已被使用的版本/主檔,「修正來源」與「刪除」要 disabled,並用 tooltip 說明原因,訊息要帶出使用它的專案名稱

此版已被 2 個掃描任務使用(Agent 測試專用專案),內容已成為稽核紀錄的一部分,不能再修改。要更換內容請建立新版本。

三個要素缺一不可:被用了幾次被哪些專案用該怎麼辦(建新版本)。只寫「已被使用不可修改」不合格——使用者不知道是誰在用、也不知道下一步。

⚠️ 專案名稱只會列出活著的專案.1 已處理軟刪穿透)。已軟刪的專案使用者根本看不到,列出來只會造成困惑。若判定端點回的專案清單是空的但使用次數大於 0(全部使用者都在已刪專案裡),tooltip 要能自然降級成不帶專案名的版本,不要印出「()」這種空括號。

② 「刪除」與「停用」不同時出現

母卡原話:兩顆同時擺會讓人不知道該按哪個——條件互斥,讓系統判斷。

狀態 menu 顯示
從未被使用過 刪除
已被使用過 停用

也就是不是「兩項都放、刪除灰掉」,而是只出現其中一項。既有的 btn_deactivate 項要改成條件顯示。

(注意這與規則 ① 不衝突:① 講的是「修正來源」這種永遠存在、只是有時不能用的動作;② 講的是刪除/停用這組互斥動作,換的是顯示哪一項。)

③ 刪除要二次確認,且確認框要說清楚會刪掉什麼

主檔刪除是硬刪、連帶清掉底下所有版本與控制項。確認框要列出「將一併刪除 N 個版本、M 條控制項」,不要只寫「確定刪除嗎」。數量從 .1 的判定端點取(若端點沒回這兩個數字,跟 .1 的實作者確認要不要補,不要自己另外多打幾支 API 湊)。

版本層刪除的確認框同理,要說清楚刪的是「哪一版」(帶版號)。


§3

動作放置位置

層級 檔案 動作
主檔層(修正/刪除整支基準) DetectionProfileManageView.vue 的操作 menu 修正來源、刪除/停用
版本層(修正某一版來源/刪除某一版) components/ProfileVersionTable.vue 展開的版本列 修正來源、刪除

版本列目前只有一顆「控制項」按鈕(emit('open-controls', data.uid),約 L240-252)。要加動作時考慮版本列的空間——多顆按鈕排在一起會擠,可比照主列表收成一顆 menu 觸發鈕。


§4

FE 座標(協調者 2026-08-04 實查)

src/views/detection-profile/
├── DetectionProfileManageView.vue        約 690 行
│     ├── buildActionMenuItems(row)       ~L346-425  ← 主要修改點,menu 項目在這裡組
│     ├── rowReadonlyReason(row)          ~L331-340  整列級唯讀原因(公版唯讀/已停用)
│     ├── capLabel(key, hasCapability)    ~L343      缺 capability 的 label 後綴
│     ├── actionMenuItems / openActionMenu ~L427-437
│     ├── handleDeactivate(row)           ~L445-463  ← 二次確認的既有範例,照它的形狀寫刪除
│     └── <ProfileVersionTable>           ~L727-730  展開列掛載處
├── DetectionProfileControlsView.vue      控制項詳細頁
├── profileDisplay.js                     顯示名解析共用(fmtDateTime / extractionBadge)
└── components/
    ├── ProfileVersionTable.vue           約 250 行(含模組層 versionCache + invalidateVersionCache)
    ├── ControlExtractionSummary.vue      抽取摘要(CM-1075 已加「來源過期時的重新解析」按鈕)
    ├── ProfileFormDialog.vue             建立/編輯對話框(改來源可能可複用其來源輸入區)
    ├── ProfileNewVersionDialog.vue       版更對話框(改來源的 UI 可直接參考它的形狀)
    ├── ProfileDetailDialog.vue
    └── ProfileCopyDialog.vue

src/service/DetectionToolProfileService.js
      既有:listProfiles / listMenu / getProfile / listVersions / createProfile /
            updateProfile / createNewVersion / forkProfile / copyToTool /
            deactivateProfile / listVersionControls / getExtractionStatus / retryExtraction
      ← 要加:判定(.1)、修正來源(.2)、刪除(.2)三支

src/config/api/api.js                    ~L353-379 是 FR-059/060 的 detection profile 端點區
      既有形狀:DETECTION_TOOL_PROFILE_DETAIL / _NEW_VERSION / _FORK /
                _COPY_TO_TOOL / _DEACTIVATE / _VERSIONS,
                DETECTION_PROFILE_VERSION_CONTROLS / _EXTRACTION
      ← 新端點照同一命名慣例加

src/config/locales/i18n/zh-tw/detection-profile-manage.json
src/config/locales/i18n/en/detection-profile-manage.json
      既有 key 形狀:btn_deactivate / dialog_deactivate_title / dialog_deactivate_msg /
                     toast_deactivate_success / confirm_no / tooltip_inactive_readonly /
                     tooltip_system_readonly / tooltip_no_permission

既有慣例(照抄,不要另起爐灶)

  • 二次確認confirm.require({ header, message, icon, acceptClass: 'p-button-danger', acceptLabel, rejectLabel, accept })handleDeactivate 就是現成範例
  • 危險動作 menu item 帶 class: 'menu-item-danger'
  • 寫入完成後呼叫既有的 onSaved()(它會重載列表 + 失效下拉快取),不要自己另外拼一套重載
  • 版本資料變動後invalidateVersionCache(profileUid),否則展開列會拿到過期快取
  • 權限與唯讀:整列級唯讀走 rowReadonlyReason(row)、capability 走 capLabel(...)。本卡新增的「使用中凍結」是第三種原因,與這兩種並存——三者都可能同時成立,disabled 判斷要取聯集,tooltip 訊息要挑最具體的那一個顯示(使用中凍結 > 已停用 > 公版唯讀 > 缺權限)

§5

🔴 已知框架坑(會再遇到,務必先讀)

PrimeVue 3 的展開列會整個銷毀重建。

BodyRow<component :is="templates['expansion']"> 渲染展開列,而 templates.expansion 就是父層那個具名插槽的函式本身。父層每次 re-render(展開/收合任一列都會改 expandedRows → 觸發 re-render)都會產生新的插槽函式實例,:is 看到「新的元件型別」就把所有已展開列的內容整個銷毀重建

後果:展開第二列時第一列的元件實例被換掉、內部狀態歸零 → 看起來像空的;父層存的 ref 也指向已卸載的舊實例。

:key 或改具名函式 ref 都擋不住——這是插槽層級的重建。唯一解是讓子元件自己管載入 + 模組層快取ProfileVersionTable.vue 已經這樣做了:versionCache 在模組層,onMounted 命中快取時同步套用、不閃 spinner)。

與本卡的關係:若在版本列加的動作會觸發父層 re-render(例如刪完一版要重載列表),就會撞到這條——重建後展開列的狀態要能從快取同步還原,不能靠父層 ref 回寫。刪除/修正完成後的正確做法是 invalidateVersionCache(profileUid) 再讓子元件重新載入,而不是直接改子元件的 local state。

(這是 CM-1066 手測時踩到的真實 bug:多列展開時只剩最後一筆有資料。)


§6

作業紀律

  • FE repo 有自己的 CLAUDE.md,repo-local 慣例以它為準;共用政策以 BE CLAUDE.md 為準
  • 技術棧:Vue 3 Composition API + Vite + Pinia + PrimeVue 3.53
  • 動手前先讀:FE repo 的 CLAUDE.md,以及 BE repo 的 docs/claude/frontend-overview.md(API 慣例 / 設計系統 / 可重用元件 / PrimeVue 3.53 已知 quirks)
  • 不切 branch——git checkout <branch> / git switch <branch> 一律不執行,永遠在當下 branch 工作。branch 不對就停下問,不要自己 fix
  • commit 時顯式 git add <檔名>,禁用 -am
  • 不 push(等 user 明示)
  • BE/FE 各自分開 commit,commit message 對應各自 repo 的脈絡
  • 收尾類動作等 user 明確下令(spec / SUMMARY / Notion 回寫 / 使用手冊)
  • 卡住兩次就停下回報,不要第三次嘗試
  • i18n 繁中/英文要同步補,不要只補一邊
  • 新增前先 grep 既有能力——本頁已有的 confirm / toast / 權限判斷 / 快取失效都要複用,不要另寫一套

§7

完成定義

端點與服務層

主檔層(DetectionProfileManageView.vue

版本層(components/ProfileVersionTable.vue

框架坑回歸

i18n

收尾