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

Repo：FE　｜　前置：CM-1077.1（判定端點）＋ CM-1077.2（寫入端點）　｜　母卡：CM-1077

---

## 本卡範圍

在檢測基準管理頁既有的操作 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 即可，不需要先重構。**

---

## 🔴 關鍵 UX 規則（決策者已裁定，不可自行改動）

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

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

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

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

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

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

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

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

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

（注意這與規則 ① 不衝突：① 講的是「修正來源」這種永遠存在、只是有時不能用的動作；② 講的是刪除／停用這組互斥動作，換的是顯示哪一項。）

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

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

版本層刪除的確認框同理，要說清楚刪的是「哪一版」（帶版號）。

---

## 動作放置位置

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

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

---

## 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 訊息要挑最具體的那一個顯示（使用中凍結 > 已停用 > 公版唯讀 > 缺權限）

---

## 🔴 已知框架坑（會再遇到，務必先讀）

**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：多列展開時只剩最後一筆有資料。）

---

## 作業紀律

- **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 / 權限判斷 / 快取失效都要複用，不要另寫一套

---

## 完成定義

### 端點與服務層

- [ ] `api.js` 加入判定端點與寫入端點常數，命名與既有 `DETECTION_TOOL_PROFILE_*` / `DETECTION_PROFILE_VERSION_*` 一致
- [ ] `DetectionToolProfileService.js` 加入對應方法，回傳形狀與既有方法一致

### 主檔層（`DetectionProfileManageView.vue`）

- [ ] menu 出現「修正來源」項
- [ ] **從未被使用過的基準**：menu 出現「刪除」，**不出現**「停用」
- [ ] **已被使用過的基準**：menu 出現「停用」，**不出現**「刪除」（不是灰掉，是不出現）
- [ ] 已被使用過的基準，「修正來源」為 `disabled` 且 hover 有 tooltip
- [ ] tooltip 訊息**實際帶得出專案名稱**（手測時找一支被真實任務用過的基準確認，不是看程式碼有寫就算）
- [ ] 判定端點回的專案清單為空但使用次數 > 0 時，tooltip 降級為不帶專案名的句子，畫面上**沒有空括號**
- [ ] 刪除有二次確認，確認框**列出「將一併刪除 N 個版本、M 條控制項」的具體數字**
- [ ] 刪除成功後列表重載、成功 toast 出現
- [ ] 「使用中凍結」與既有的「公版唯讀」「已停用」「缺 capability」四種原因同時成立時，disabled 生效且 tooltip 顯示最具體的那一個

### 版本層（`components/ProfileVersionTable.vue`）

- [ ] 展開的版本列出現「修正來源」與「刪除」（或收在 menu 內）
- [ ] 已被使用過的版本，「修正來源」`disabled` ＋ tooltip 帶專案名稱；「刪除」不出現
- [ ] 版本刪除的確認框帶得出**是哪一版**（版號）
- [ ] 刪除／修正完成後呼叫 `invalidateVersionCache(profileUid)`，重新展開看得到最新狀態

### 框架坑回歸

- [ ] **同時展開三列以上**，每一列的版本資料都在（不是只剩最後一筆）
- [ ] 在其中一列做刪除／修正後，其他已展開列的內容**沒有變空**

### i18n

- [ ] `zh-tw` 與 `en` 兩份 `detection-profile-manage.json` 都補齊，key 數量一致
- [ ] 切到英文介面實際看過一遍，沒有裸露的 `lang.detection_profile.xxx` key

### 收尾

- [ ] 手測全部走過一遍（不是只跑 lint）
- [ ] FE repo 顯式 `git add` 後 commit，**未 push**
- [ ] 回報實際遇到的偏差（端點形狀與預期不同、UX 規則實作時發現的矛盾等）
