# 盤點 ③：FE 角色／使用者／專案成員頁現況

> 2026-10-06 探脈產出，唯讀盤點，供討論稿引用。路徑相對於 `~/Projects/Billows/Audit-Manager/compliance-manager-fe/src/`。

## 1. 角色管理頁

- 矩陣在 `views/role-manage/RoleForm.vue:456-516`，用 PrimeVue `TreeTable`。Checkbox 自繪三態（空／勾／減號），CSS 在 `:531` 起。
- 資料來源 `menuStore`（`:18`）四份：`uiRoutes`（列，BE 回巢狀樹）、`capabilityMenu`（欄 action）、`tenantMenu`、`enableStatusMenu`。載入 `onMounted` → `menuStore.initStaticMenu`（`stores/menuStore.js:268-278`）。
- 列分組直接沿用 `uiRoutes` 巢狀樹（`toTreeNodes :323-330`；`cellStateMap :350-365` 遞迴算三態；群組列聚合子孫 capability `:250-265`）。
- 欄：`getCapabilityActions`（`:83-98`）把 `capabilityMenu` 依 `action` 去重，READ 排第一。
- license 旗標 `isRcLicensed`（`:236-240`）標在 `route_capabilities[]` 每筆，缺席時 fail-open。
- **沒有任何 wildcard 概念**：capability 一律以 id 逐筆存在 `currRole.capabilities[]`。
- `is_platform` 過濾：`isTopTenant :41-45`、`isEditingRootTenantRole :46-54`、`stripPlatformRoutes :343-345`、可指派 id 集合 `:171-180`、送出前濾 `:209-220`。
- API（常數 `config/api/api.js`，兩頁直接用 `BaseService`）：`GET/POST/PUT API.ROLE`（RoleForm `:102/:128/:141`）、存檔後 `GET API.WEB_MENU`（`:148-149`）、列表 `POST API.ROLES`（RoleManage `:77`）、`PUT ROLE_STATUS`（`:94`）、`DELETE ROLE`（`:104`）。常數 `ROLES/ROLE/ROLE_STATUS :29-31`、`UI_ROUTES :32`、`ROLE_MENU :45`、`CAPABILITIES_MENU :481`。

## 2. 使用者管理頁

- 入口 `views/user-manage/UserManageTabView.vue`，兩 tab：`UserManage.vue`（列表）、`UserManageUpload.vue`（匯入）。表單 `UserMTRBACForm.vue`，個人資料 `UserProfileForm.vue`。
- 指派角色 UI 在 `UserMTRBACForm.vue`：`user_roles` DataTable（`:548-578`，欄位租戶／部門／角色／是否預設）；新增編輯 Dialog（`:589-650`）三個 Dropdown：租戶 `:598`、部門 `:613`、角色 `:627`、`is_default` InputSwitch `:642`。換租戶重載部門與角色 `:381-386` → `composables/useTenantScopedMenus.js:29-30`（`ORG_UNIT_MENU`／`ROLE_MENU` 帶 tenant_id）。
- **範圍**：已有部門（`org_unit_id`），但是**扁平 Dropdown 不是樹**。payload 每筆 `{role_uid, tenant_id, org_unit_id, is_default}`（`:216-223`）。
- **起訖日**：只有**使用者層級**的 `effective_date`／`expire_date`（Calendar `:489/:501`，payload `:234-242`）。**每筆角色指派本身沒有起訖日。**
- API：`GET/POST/PUT API.USER`（`:166/:257/:268`）、`POST API.USERS`（UserManage `:74`）、`USER_CHANGE_STATUS`（`:176/:196`）、匯入四支（Upload `:102/:135/:157/:200`）。
- 個人資料頁唯讀顯示角色群組：`components/user-manage/UserTenantGroupList.vue`（91 行）。

## 3. 專案成員設定

- 主元件 `components/grc/project/ProjectParticipantsPanel.vue`（108 行），用在 `views/project/ProjectSettingsView.vue:104` 與 `views/project/ProjectPlanningView.vue:2427`（tab index 3，query `participants`）。
- 挑選 UI `components/grc/ParticipantPicker.vue`（176 行），新建專案也用它（`ProjectCreateView.vue:406`）。
- **四個角色值前端寫死**：`ParticipantPicker.vue:93-97` `roleOptions`（manager/reviewer/auditor/viewer，i18n `lang.participant.role.*`），新增預設 `viewer`（`:55`）。
- 驗證：至少 1 人且至少 1 位 manager（Panel `:66-73`）。
- 存檔不是獨立端點：`PUT API.GRC_PROJECT/{projectId}` body `{participants:[{user_uid, role}]}`（Panel `:76-80`）。可選人員 `menuStore.userMenu`（`GET API.USER_MENU`）。

## 4. 前端權限判定

**能力點這套**
- `userStore.permissions`（`stores/userStore.js:7`，持久化 key `cm_store`）＝ `/user/web-menu` 回來的選單樹，節點 `{label, icon, url, items[], capabilities:[{id, name, resource_type, action, requirement}]}`。
- helper `utils/userUtil.js`：`userCapabilities :24-37`（扁平成 name Set）、`hasCap(name) :42`、`getUserPagePermissions(url) :62-78`。
- `hasCap` 約 13 支檔在用（例 `components/integrations/GoogleDriveIntegrationCard.vue:218-222`、`views/scan-zone/ScanZoneManage.vue:30`）；`getUserPagePermissions` 例 `views/project/ProjectListView.vue:36`。
- **路由守衛不判能力點**：`config/router/index.js` 只有 `requiresAdmin`（`:1057-1066`，判 `userInfo.is_admin`）與 `requiresPlatformAdmin`（`:1071-1100`）。`:464/:873` 註解明寫不依 ui_routes 擋直接輸入網址。側邊選單另依 url 濾 platform-only（`layout/AppMenu.vue:24-45`）。

**專案成員角色這套**：沒有共用 helper，各頁自己拿 `participants` 比 `userInfo.uid`。
- `views/project/ProjectAuditorOverview.vue`：`isProjectManager :185-190`、`isAuditor :197`、按鈕 `:1152/:1160/:1172/:1205/:2095`。
- `views/project/ProjectPlanningView.vue`：`isProjectManager :378-383`、`myParticipantRole :386-391` → `components/grc/ssp/SspTabPanel.vue:76`（`canEdit :92`）。
- `components/grc/FlowPhaseBanner.vue:230`：`isManager` 吃 BE 回的 `stageInfo.user_role`。
- 同型寫法：`RoundAuditReviewView.vue:54-70`、`MyTasksView.vue:208-228`、`ProjectSettingsView.vue:53`。

## 5. 選單

- `/user/web-menu`（`WEB_MENU`）回來直接寫入 `userStore.permissions`，寫入點：`views/auth/Login.vue:205-206`、`OtpVerification.vue:216`、`layout/AppTopbar.vue:323-324`（切租戶）、`views/license/LicenseActivation.vue:164`、`RoleForm.vue:148-149`。
- 渲染 `layout/AppMenu.vue:47-76`。
- 路由：`/auth/user-manage`（`:93-98`）、`/auth/role-manage`（`:101-106`）、`/auth/role-form`（`:109-114`）、`/auth/user-form-mtrbac`（`:117-122`）。

## 6. 可複用元件

- `src/components/` **沒有** Tree、日期區間、矩陣、轉移清單的通用封裝。
- 樹狀：全專案沒有 `TreeSelect`；`<Tree>` 僅 `EditIncludeControlsDialog.vue:302`（控制項專用）。部門資料是扁平清單（`ORG_UNIT_MENU`、`menuStore.orgUnitMenu`），部門管理頁 `views/org-unit/OrgUnitManage.vue` 用 DataTable＋上層 Dropdown，**沒有樹狀元件可直接複用**。
- 日期區間：唯一 `selectionMode="range"` 在 `AiUsageSummary.vue:171`，其餘成對單日 Calendar。
- 矩陣：唯一範本是 RoleForm 內嵌 TreeTable 三態矩陣（`:456-516`，CSS `:531-605`），未抽元件。
- 轉移清單：沒有 `PickList`／`OrderList`。最接近 `ParticipantPicker.vue`、`DeviceMultiSelect.vue`（57 行）。
- 租戶隔離選單快取 `composables/useTenantScopedMenus.js` 可沿用。

## 7. 行數

| 檔案 | 行數 |
|---|---|
| views/role-manage/RoleForm.vue | 605 |
| views/role-manage/RoleManage.vue | 265 |
| views/user-manage/UserMTRBACForm.vue | 651 |
| views/user-manage/UserManage.vue | 401 |
| views/user-manage/UserManageUpload.vue | 470 |
| views/user-manage/UserProfileForm.vue | 691 |
| components/grc/project/ProjectParticipantsPanel.vue | 108 |
| components/grc/ParticipantPicker.vue | 176 |
| **views/project/ProjectPlanningView.vue** | **2613（>800）** |
| **views/project/ProjectAuditorOverview.vue** | **2161（>800）** |
| **views/project/RoundAuditReviewView.vue** | **1077（>800）** |
| views/project/MyTasksView.vue | 741 |
| components/grc/FlowPhaseBanner.vue | 627 |
| components/grc/ssp/SspTabPanel.vue | 510 |
| **config/router/index.js** | **1122（>800）** |

**重點**：wildcard 完全沒有；起訖日只在使用者層不在指派層；部門範圍已存在但扁平下拉無樹；路由守衛不判能力點；專案成員四值寫死、判定邏輯散在 6 支以上檔案無共用 helper。
