合規框架 PDF 匯入兩階段化 — Frontend Spec

文件版本:1.1 建立日期:2026-05-03 最後更新:2026-05-04(合併 v2.1 迭代) 對應 SDdesign.md 對應 SAapi-spec.md


0. 迭代紀錄

日期 版本 變更摘要 對應 FE Changelog
2026-05-03 1.0 初版 — 兩動態路由 + Step 1/2 元件 + Service + Composable 2026-05-03-feat-framework-pdf-import-v2.md
2026-05-04 1.1 (a) Step 1 簡化只剩 parser + 上傳;metadata 三欄移到 Step 2 基本資料 Tab;(b) 「重新上傳 PDF」入口走 resume 路由 + target_version_uid;(c) 合規框架版本即時編輯頁(路由 compliance-framework-version-edit + FrameworkVersionEditView + FrameworkVersionEditService);(d) 共享元件 components/article/FrameworkArticlePaneFrameworkArticleContent(匯入 Step 2 與編輯頁共用) compliance-framework/20260504_2300_framework-version-edit-mode.md / compliance-framework/20260504_2200_batch-delete-loading.md

1. 路由配置

// src/config/router/index.js (摘錄)
{
  path: '/compliance-framework/import-version',
  name: 'compliance-framework-import-version',
  component: () => import('@/views/compliance-framework/FrameworkImportPage.vue'),
  meta: { /* 沿用 compliance-framework 模組權限 */ },
},
{
  path: '/compliance-framework/import-version/:parseUid',
  name: 'compliance-framework-import-version-resume',
  component: () => import('@/views/compliance-framework/FrameworkImportPage.vue'),
  props: true,
},
{
  // v1.1 新增 — 合規框架版本即時編輯
  path: '/compliance-framework/framework-version/:uid/edit',
  name: 'compliance-framework-version-edit',
  component: () => import('@/views/compliance-framework/FrameworkVersionEditView.vue'),
  props: true,
}

FrameworkImportPage 同元件處理「全新上傳」(無 parseUid)/「接手草稿」(有 parseUid)/「重新上傳覆蓋」(resume + parse_job 帶 target_version_uid)三種情境,內部 onMountedroute.params.parseUid + parse_job 內容決定起始 step。

2. 元件樹

FrameworkImportPage.vue                      (page,含 step 狀態機)
├── DraftBanner.vue                          (Q5C 頁首 banner,列當前 tenant 草稿)
├── Step 1: FrameworkImportUploadStep.vue          (v1.1 簡化)
│   ├── PDFFileDropzone (拖曳/點擊上傳)
│   ├── Dropdown parser_type
│   └── 按鈕「上傳並解析文件」 → POST /parse (僅 parser + file) → router.push <parseUid>
│
│   注:v1.0 在 Step 1 收的 framework_uid / version / release_date / publish_status / parent_version
│        皆已搬到 Step 2「基本資料」Tab;framework_uid 由 caller URL 帶入無需再選。
│
├── Step 2: FrameworkImportPreviewStep.vue   (status=awaiting_review)
│   ├── Splitter
│   │   ├── 左:PDFPreviewIframe (file blob URL 或 file_uid)
│   │   └── 右:TabView
│   │       ├── Tab 0「基本資料」   target_version / target_release_date / target_publish_status (v1.1 移入)
│   │       │                       覆蓋模式下隱藏,只顯示「重新上傳到 vX.Y」badge
│   │       ├── Tab 1「Groups」     GroupTreeEditor.vue
│   │       ├── Tab 2「Controls」   ControlListEditor.vue
│   │       └── Tab 3「AOs」        AssessmentListEditor.vue
│   └── Footer:「捨棄草稿」 + 「確認匯入」(confirm 時把基本資料 Tab 三欄當 metadata 一起送)
└── ErrorPanel.vue                           (status=failed 時呈現)
    └── 顯示 error_code / error_message + 「重新上傳」按鈕(清 parseUid → 退回 Step 1)

2.1 各 editor 元件職責

GroupTreeEditor.vue

  • 用 PrimeVue Tree(draggable)顯示 group 階層(parent_group_uid 決定樹)
  • 點 node 開 inline form 編 name / description / order_no
  • 拖曳重組階層(更新 parent_group_uid
  • 每 row 有 ✕ 按鈕:標 delete decision;視覺上灰掉並打叉,確認匯入時才實際移除

ControlListEditor.vue

  • DataTable + filter by group(dropdown 切換)
  • 點 row 展開(PrimeVue expansion):編 control_id / control_title / description / guidance / order_no
  • group_uid 用 dropdown 重綁
  • ✕ 標 delete

AssessmentListEditor.vue

  • DataTable + filter by control
  • 點 row 編 name / description / order_no / control_uid
  • ✕ 標 delete

共用 UX 細節

  • 編輯狀態用「未儲存」徽章標示;切 tab 不會丟失
  • beforeunload + Vue Router guard:未 confirm 離開頁面跳 confirm dialog(仿 SspDocxImportPage)

3. Service 層

新增 src/service/FrameworkImportService.js(仿 SspDocxImportService.js):

import { BaseService } from '@/service/BaseService'
import API from '@/config/api/api'

class FrameworkImportService extends BaseService {
  listJobs(params = {}) {
    return this.get(API.OSCAL_FRAMEWORK_PARSE_JOBS, params)
  }
  parse(formData) {
    return this.postFormData(`${API.OSCAL_FRAMEWORK_PARSE_JOBS}/parse`, formData)
  }
  getDetail(parseUid) {
    return this.get(`${API.OSCAL_FRAMEWORK_PARSE_JOBS}/${parseUid}`)
  }
  confirm(parseUid, payload) {
    return this.post(`${API.OSCAL_FRAMEWORK_PARSE_JOBS}/${parseUid}/confirm`, payload)
  }
  discard(parseUid) {
    return this.delete(`${API.OSCAL_FRAMEWORK_PARSE_JOBS}/${parseUid}`)
  }
}

export default new FrameworkImportService()

新增 API 常數:

// src/config/api/api.js
OSCAL_FRAMEWORK_PARSE_JOBS: getUrl('/oscal-framework-parse-jobs'),

4. 狀態管理 / Composable

src/composables/useFrameworkImportDraft.js(仿 useSspDocxDraft):

export function useFrameworkImportDraft(parseUid, userUid) {
  const STORAGE_KEY = `framework-import-draft:${parseUid}:${userUid}`

  const draft = ref({
    decisions: [],          // [{type, uid, action: 'delete'}]
    overrides: {            // 覆寫條目,以 uid 為 key
      groups: {},
      controls: {},
      assessments: {},
    },
  })

  function load() { /* localStorage */ }
  function save() { /* localStorage debounced */ }
  function clear() { /* localStorage */ }

  // edits API
  function markDeleted(type, uid) { ... }
  function unmarkDeleted(type, uid) { ... }
  function patch(type, uid, fields) { ... }     // override 欄位合併

  // 對外 export 給 component 收集編輯狀態的 helper
  function buildConfirmPayload() {
    return {
      decisions: draft.value.decisions,
      overrides: {
        groups: Object.values(draft.value.overrides.groups),
        controls: Object.values(draft.value.overrides.controls),
        assessments: Object.values(draft.value.overrides.assessments),
      }
    }
  }

  return { draft, load, save, clear, markDeleted, unmarkDeleted, patch, buildConfirmPayload }
}

Q6 last-write-wins → composable 不需跟 BE sync,純 LocalStorage。 Key 含 userUid:同一瀏覽器多帳號切換不會互相污染。

5. 入口替換

src/views/compliance-framework/ComplianceFrameworkVersionManage.vue

現況 改動
onImportVersionClickimportVersionDialog router.push({ name: 'compliance-framework-import-version' })
importVersionDialog template / createDocByImportFile / updateDocByImportFile / 相關 ref 整段移除
「匯入版本」按鈕 留著但 click handler 改 router.push

確認 editVersionDialog(編輯既有版本的 metadata)等其他 dialog 不受影響。

6. i18n

新增 src/config/locales/i18n/zh-tw/compliance-framework-version-import.jsonen/compliance-framework-version-import.json

{
  "compliance_framework_version_import": {
    "title": "匯入合規框架版本",
    "draft_banner": {
      "summary": "目前有 {total} 筆未完成草稿(其中 {mine} 筆是您的)",
      "view": "查看草稿",
      "open": "繼續編輯"
    },
    "step1": {
      "title": "上傳 PDF",
      "framework": "合規框架",
      "version": "版本",
      "release_date": "發行日期",
      "publish_status": "發佈狀態",
      "parser_type": "PDF 解析器",
      "parent_version": "父版本(建立子版本時填寫)",
      "submit": "開始解析"
    },
    "step2": {
      "title": "預覽編輯",
      "tab_groups": "群組",
      "tab_controls": "控制項",
      "tab_assessments": "評估目標",
      "discard": "捨棄草稿",
      "confirm": "確認匯入",
      "deleted_badge": "已標記刪除"
    },
    "error": {
      "parse_failed": "解析失敗",
      "retry_upload": "重新上傳",
      "empty_result": "解析結果為空,無控制項可匯入",
      "version_exists": "此版本已存在",
      "already_confirmed": "草稿已被 {user} 確認匯入"
    },
    "leave_confirm": "尚未儲存的編輯將會遺失,確定離開?"
  }
}

7. PDF 預覽

  • 上傳的 PDF 轉成 Blob URL(URL.createObjectURL),LocalStorage 不存 binary
  • 中途換 tab / 重整:blob URL 失效,改顯示「已關閉預覽,可重新上傳同份檔以恢復預覽」提示,不影響繼續編輯(編輯來源是 BE parsed_result + LocalStorage draft)

8. 錯誤回饋路徑

場景 UI
Parse 失敗(status=failed) ErrorPanel 顯示 error_code 對照表訊息 + 「重新上傳」按鈕
Confirm 拿到 409 already_confirmed toast 顯示「已被 {created_user_name} 於 {timestamp} 確認匯入」並 router.push 列表頁
Confirm 拿到 400 empty toast 顯示「至少保留 1 個控制項才能匯入」
Confirm 拿到 400 invalid_reference toast 顯示「有控制項或 AO 引用了已刪除的群組,請檢查」
網路錯誤 toast「網路錯誤,請稍後再試」+ retry button

9. 不在 FE Spec 範圍

  • Vue Router 權限攔截規則(RBAC SOP 另行處理)
  • Component 內部 reactive 狀態管理細節(由 FE 開發時 self-evident)
  • PrimeVue Tree drag-drop 細節 API(依官方文件)

10. 合規框架版本即時編輯頁(v1.1 新增)

10.1 路由與入口

  • 路由/compliance-framework/framework-version/:uid/edit(name: compliance-framework-version-edit
  • 入口ComplianceFrameworkVersionManage.vue 的 row action「編輯版本」
  • 內部 propsuid(version uid);query from_uid 可帶 framework uid 用於返回導航

10.2 元件樹

FrameworkVersionEditView.vue
├── Card 標題列(版本號 + publish_status badge + 「重新上傳 PDF」按鈕 + 返回)
├── Splitter
│   ├── 左:PDF iframe (優先 file_uid 走 PDF_FILE_PREVIEW;fallback OSCAL_FRAMEWORK_VERSION_DOWNLOAD)
│   └── 右:FrameworkArticlePane (共享元件)
│         └── FrameworkArticleContent (groups / controls / assessments tree,inline edit + cascade delete 視覺)
└── Confirm dialog(重新上傳 PDF 入口 → router.push 匯入 resume 路由 + parse_job target_version_uid)

10.3 寫入策略

每個 patch / delete event 即時 PUT/DELETE BE,無 staging(對齊 module_frame default-edit 風格)。

// FrameworkArticlePane 派發事件 → FrameworkVersionEditView 收
@patch="(payload) => FrameworkVersionEditService.updateGroup(uid, payload)"
@delete="(uid) => FrameworkVersionEditService.deleteControl(uid)"

10.4 雙軌守門(FE 端對齊 BE)

條件 UI 行為
publish_status != 'draft' readOnly=true — 整頁文字輸入 disabled、按鈕灰;header 顯示「published / deprecated 不可編輯」
has_references=true(draft 但有引用) readOnly=falseallowDelete=false — 文字可改、刪除按鈕隱藏;header 顯示「已被合規資源庫引用」
兩者皆 false 完整可改可刪

進頁時 getCatalogTree 一次取回 publish_status + has_references;FE 計算 readOnly / allowDelete 兩個 computed 控制 UI。

10.5 共享元件 components/compliance-framework/article/

  • FrameworkArticlePane.vue:左 PDF / 右 article 配置 wrapper(編輯頁 + 匯入頁 Step 2 通用)
  • FrameworkArticleContent.vue:實際顯示 groups → controls → assessments 階層 tree + inline edit + 標 delete 視覺

10.6 Service

src/service/FrameworkVersionEditService.js

class FrameworkVersionEditService extends BaseService {
  getCatalogTree(versionUid) { return this.get(`${API.OSCAL_FRAMEWORK_VERSION_CATALOG_TREE}/${versionUid}/catalog-tree`) }
  updateGroup(uid, fields) { return this.put(`${API.OSCAL_CATALOG_GROUP}/${uid}`, fields) }
  deleteGroup(uid) { return this.delete(`${API.OSCAL_CATALOG_GROUP}/${uid}`) }
  updateControl(uid, fields) { return this.put(`${API.OSCAL_CATALOG_CONTROL}/${uid}`, fields) }
  deleteControl(uid) { return this.delete(`${API.OSCAL_CATALOG_CONTROL}/${uid}`) }
  updateAssessment(uid, fields) { return this.put(`${API.OSCAL_CATALOG_CONTROL_ASSESSMENT}/${uid}`, fields) }
  deleteAssessment(uid) { return this.delete(`${API.OSCAL_CATALOG_CONTROL_ASSESSMENT}/${uid}`) }
}

新增 API 常數:

OSCAL_FRAMEWORK_VERSION_CATALOG_TREE: getUrl('/oscal-framework-version'),  // GET /<uid>/catalog-tree
OSCAL_CATALOG_GROUP: getUrl('/oscal-catalog-group'),
OSCAL_CATALOG_CONTROL: getUrl('/oscal-catalog-control'),
OSCAL_CATALOG_CONTROL_ASSESSMENT: getUrl('/oscal-catalog-control-assessment'),

10.7 i18n menu 新 key

  • compliance-framework-import-version = "匯入框架版本"
  • compliance-framework-import-version-resume = "繼續編輯草稿"
  • compliance-framework-version-edit = "編輯框架版本"