文件版本:1.1 建立日期:2026-05-03 最後更新:2026-05-04(合併 v2.1 迭代) 對應 SD:design.md 對應 SA:api-spec.md
| 日期 | 版本 | 變更摘要 | 對應 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/FrameworkArticlePane、FrameworkArticleContent(匯入 Step 2 與編輯頁共用) |
compliance-framework/20260504_2300_framework-version-edit-mode.md / compliance-framework/20260504_2200_batch-delete-loading.md |
// 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)三種情境,內部onMounted依route.params.parseUid+ parse_job 內容決定起始 step。
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)
GroupTreeEditor.vue:
parent_group_uid 決定樹)name / description / order_noparent_group_uid)delete decision;視覺上灰掉並打叉,確認匯入時才實際移除ControlListEditor.vue:
control_id / control_title / description / guidance / order_nogroup_uid 用 dropdown 重綁AssessmentListEditor.vue:
name / description / order_no / control_uid共用 UX 細節:
beforeunload + Vue Router guard:未 confirm 離開頁面跳 confirm dialog(仿 SspDocxImportPage)新增 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'),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:同一瀏覽器多帳號切換不會互相污染。
src/views/compliance-framework/ComplianceFrameworkVersionManage.vue:
| 現況 | 改動 |
|---|---|
onImportVersionClick 開 importVersionDialog |
router.push({ name: 'compliance-framework-import-version' }) |
importVersionDialog template / createDocByImportFile / updateDocByImportFile / 相關 ref |
整段移除 |
| 「匯入版本」按鈕 | 留著但 click handler 改 router.push |
確認 editVersionDialog(編輯既有版本的 metadata)等其他 dialog 不受影響。
新增 src/config/locales/i18n/zh-tw/compliance-framework-version-import.json 與 en/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": "尚未儲存的編輯將會遺失,確定離開?"
}
}Blob URL(URL.createObjectURL),LocalStorage 不存 binaryparsed_result + LocalStorage draft)| 場景 | 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 |
/compliance-framework/framework-version/:uid/edit(name: compliance-framework-version-edit)ComplianceFrameworkVersionManage.vue 的 row action「編輯版本」uid(version uid);query from_uid 可帶 framework uid 用於返回導航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)
每個 patch / delete event 即時 PUT/DELETE BE,無 staging(對齊 module_frame default-edit 風格)。
// FrameworkArticlePane 派發事件 → FrameworkVersionEditView 收
@patch="(payload) => FrameworkVersionEditService.updateGroup(uid, payload)"
@delete="(uid) => FrameworkVersionEditService.deleteControl(uid)"| 條件 | UI 行為 |
|---|---|
publish_status != 'draft' |
readOnly=true — 整頁文字輸入 disabled、按鈕灰;header 顯示「published / deprecated 不可編輯」 |
has_references=true(draft 但有引用) |
readOnly=false 但 allowDelete=false — 文字可改、刪除按鈕隱藏;header 顯示「已被合規資源庫引用」 |
| 兩者皆 false | 完整可改可刪 |
進頁時
getCatalogTree一次取回publish_status+has_references;FE 計算readOnly/allowDelete兩個 computed 控制 UI。
components/compliance-framework/article/FrameworkArticlePane.vue:左 PDF / 右 article 配置 wrapper(編輯頁 + 匯入頁 Step 2 通用)FrameworkArticleContent.vue:實際顯示 groups → controls → assessments 階層 tree + inline edit + 標 delete 視覺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'),compliance-framework-import-version = "匯入框架版本"compliance-framework-import-version-resume = "繼續編輯草稿"compliance-framework-version-edit = "編輯框架版本"