Files
CN_Tool_client/docs/superpowers/plans/2026-04-22-mmsmapping-layout-and-config-plan.md
2026-04-23 11:09:06 +08:00

995 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MMS Mapping Layout And Config Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rebuild the `mmsmapping` page into a two-phase ICD parsing and mapping generation workflow with a left-side file/result layout and a right-side configuration workspace driven by `DefaultCfg.txt`.
**Architecture:** Keep the existing `getIcdMmsJson` API contract intact, but replace the raw JSON editor flow with a typed page container, a simplified left-top request panel, a left-bottom result panel, and a new right-side configuration panel. Use two utility modules to parse `DefaultCfg.txt`, generate a draft from `indexCandidates`, validate editable rows, and convert the draft back into `request.indexSelection`; validation relies on `vue-tsc`, `eslint`, and manual browser checks because this repo does not currently ship an automated frontend test runner.
**Tech Stack:** Vue 3 `<script setup>`, TypeScript, Element Plus, Vite, ESLint, `vue-tsc`
---
## File Map
- Modify: `frontend/src/api/tools/mmsmapping/interface/index.ts`
Purpose: Add front-end-only types for the editable base form, parsed `DefaultCfg` template, draft groups, and row-level editing state.
- Create: `frontend/src/views/tools/mmsmapping/utils/defaultCfg.ts`
Purpose: Load `DefaultCfg.txt` as raw text, sanitize trailing commas, parse it into a normalized template object, and expose a single typed parser function.
- Create: `frontend/src/views/tools/mmsmapping/utils/mappingDraft.ts`
Purpose: Hold request defaults plus the pure functions that match template groups to `indexCandidates`, build the editable draft, validate row completeness, and convert the draft into `request.indexSelection`.
- Modify: `frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue`
Purpose: Shrink the left-top panel down to ICD file selection, parse action, reset action, and status tags.
- Modify: `frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue`
Purpose: Keep it result-only for `mappingJson` and `problems`, with copy/layout aligned to the left-bottom output role.
- Create: `frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue`
Purpose: Render the right-side `version`/`author` form, template/candidate helper info, editable draft rows, and the repeated `生成映射` action.
- Modify: `frontend/src/views/tools/mmsmapping/index.vue`
Purpose: Replace the raw JSON flow with two-phase request handling, draft state management, reset behavior, template-error handling, and the new left-stack/right-panel layout.
> Repo note: the current frontend package has `lint` and `type-check` scripts but no unit-test runner. Do not add Vitest/Jest in this task. Use `npm run lint`, `npm run type-check`, and the manual flow checklist in Task 5.
### Task 1: Add Typed Template And Draft Utilities
**Files:**
- Modify: `frontend/src/api/tools/mmsmapping/interface/index.ts`
- Create: `frontend/src/views/tools/mmsmapping/utils/defaultCfg.ts`
- Create: `frontend/src/views/tools/mmsmapping/utils/mappingDraft.ts`
- [ ] **Step 1: Extend the MMS mapping interface file with front-end draft types**
Add the following block near the existing request/response interfaces in `frontend/src/api/tools/mmsmapping/interface/index.ts`:
```ts
export interface BaseRequestForm {
version: string
author: string
}
export interface DefaultCfgReportTemplate {
desc: string
select: string
dataSetList: string[]
lnInstList: string[]
}
export interface DefaultCfgTemplate {
reportList: DefaultCfgReportTemplate[]
}
export interface DraftCandidateReport {
reportName: string
dataSetName: string
availableLnInstValues: string[]
reportDesc?: string
}
export type DraftMatchStatus = 'matched' | 'pending'
export interface MappingDraftRow {
id: string
label: string
reportName: string
dataSetName: string
lnInst: string
}
export interface MappingDraftGroup {
id: string
templateDesc: string
selectKey: string
dataSetList: string[]
templateLabels: string[]
candidateGroupKey: string
candidateGroupDesc: string
matchStatus: DraftMatchStatus
candidateReports: DraftCandidateReport[]
rows: MappingDraftRow[]
}
```
- [ ] **Step 2: Create the loose-JSON parser for `DefaultCfg.txt`**
Create `frontend/src/views/tools/mmsmapping/utils/defaultCfg.ts` with this implementation:
```ts
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
import defaultCfgText from '../DefaultCfg.txt?raw'
interface DefaultCfgRawReportItem {
desc?: string
Select?: string
DataSetList?: string[]
LnInstList?: string[]
}
interface DefaultCfgRawFile {
ReportList?: DefaultCfgRawReportItem[]
}
const sanitizeLooseJson = (source: string) => source.replace(/,\s*([}\]])/g, '$1')
export const parseDefaultCfgTemplate = (): MmsMapping.DefaultCfgTemplate => {
const parsed = JSON.parse(sanitizeLooseJson(defaultCfgText)) as DefaultCfgRawFile
const reportList = (parsed.ReportList || []).map(item => ({
desc: item.desc?.trim() || 'Default Report Group',
select: item.Select?.trim() || '',
dataSetList: (item.DataSetList || []).filter(Boolean),
lnInstList: (item.LnInstList || []).filter(Boolean)
}))
return { reportList }
}
```
- [ ] **Step 3: Create request defaults, matching, draft building, validation, and payload conversion**
Create `frontend/src/views/tools/mmsmapping/utils/mappingDraft.ts` with this implementation:
```ts
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
export const DEFAULT_REQUEST_OPTIONS = {
saveToDisk: false,
prettyJson: true,
outputDir: ''
} satisfies Pick<MmsMapping.GetIcdMmsJsonRequestPayload, 'saveToDisk' | 'prettyJson' | 'outputDir'>
export const createBaseRequestPayload = (
form: MmsMapping.BaseRequestForm
): Omit<MmsMapping.GetIcdMmsJsonRequestPayload, 'indexSelection'> => ({
version: form.version.trim() || '1.0',
author: form.author.trim() || 'system',
...DEFAULT_REQUEST_OPTIONS
})
const getIntersectionSize = (left: string[], right: string[]) => {
const rightSet = new Set(right.filter(Boolean))
return left.filter(item => rightSet.has(item)).length
}
const matchCandidateGroup = (
template: MmsMapping.DefaultCfgReportTemplate,
candidates: MmsMapping.IndexCandidateGroup[]
) => {
const scored = candidates
.map(candidate => {
const templateLabelScore = getIntersectionSize(template.lnInstList, candidate.templateLabels || [])
const dataSetScore = getIntersectionSize(
template.dataSetList,
(candidate.reports || []).map(report => report.dataSetName || '')
)
return {
candidate,
score:
(candidate.groupDesc === template.desc ? 100 : 0) +
templateLabelScore * 10 +
dataSetScore * 5
}
})
.filter(item => item.score > 0)
.sort((left, right) => right.score - left.score)
if (!scored.length) return null
if (scored.length > 1 && scored[0].score === scored[1].score) return null
return scored[0].candidate
}
export const buildDraftGroups = (
template: MmsMapping.DefaultCfgTemplate,
candidates: MmsMapping.IndexCandidateGroup[]
): MmsMapping.MappingDraftGroup[] =>
template.reportList.map((reportTemplate, groupIndex) => {
const matchedCandidate = matchCandidateGroup(reportTemplate, candidates)
const candidateReports = (matchedCandidate?.reports || []).map(report => ({
reportName: report.reportName || '',
dataSetName: report.dataSetName || '',
reportDesc: report.reportDesc,
availableLnInstValues: report.availableLnInstValues || []
}))
return {
id: `${reportTemplate.select || 'group'}-${groupIndex}`,
templateDesc: reportTemplate.desc,
selectKey: reportTemplate.select,
dataSetList: reportTemplate.dataSetList,
templateLabels: reportTemplate.lnInstList,
candidateGroupKey: matchedCandidate?.groupKey || '',
candidateGroupDesc: matchedCandidate?.groupDesc || '',
matchStatus: matchedCandidate ? 'matched' : 'pending',
candidateReports,
rows: reportTemplate.lnInstList.map((label, rowIndex) => ({
id: `${reportTemplate.select || 'group'}-${groupIndex}-${rowIndex}`,
label,
reportName: candidateReports[0]?.reportName || '',
dataSetName: candidateReports[0]?.dataSetName || '',
lnInst: ''
}))
}
})
export const validateDraftGroups = (groups: MmsMapping.MappingDraftGroup[]) => {
const problems: string[] = []
groups.forEach(group => {
if (!group.candidateGroupKey) {
problems.push(`${group.templateDesc} 尚未绑定候选分组`)
}
group.rows.forEach(row => {
if (!row.reportName || !row.dataSetName || !row.lnInst) {
problems.push(`${group.templateDesc} / ${row.label} 的映射配置不完整`)
return
}
const matchedReport = group.candidateReports.find(
report => report.reportName === row.reportName && report.dataSetName === row.dataSetName
)
if (!matchedReport) {
problems.push(`${group.templateDesc} / ${row.label} 选择了非法的 reportName 或 dataSetName`)
return
}
if (!matchedReport.availableLnInstValues.includes(row.lnInst)) {
problems.push(`${group.templateDesc} / ${row.label} 选择了非法的 lnInst`)
}
})
})
return problems
}
export const buildIndexSelectionPayload = (
groups: MmsMapping.MappingDraftGroup[]
): MmsMapping.IndexSelectionGroup[] =>
groups
.filter(group => group.candidateGroupKey)
.map(group => ({
groupKey: group.candidateGroupKey,
groupDesc: group.candidateGroupDesc || group.templateDesc,
bindings: group.rows.map(row => ({
reportName: row.reportName,
dataSetName: row.dataSetName,
label: row.label,
lnInst: row.lnInst
}))
}))
```
- [ ] **Step 4: Run type-check after adding the new types and utility modules**
Run:
```powershell
cd frontend
npm run type-check
```
Expected: command exits with code `0` and no TypeScript diagnostics.
- [ ] **Step 5: Commit the utility scaffolding**
Run:
```powershell
git add frontend/src/api/tools/mmsmapping/interface/index.ts frontend/src/views/tools/mmsmapping/utils/defaultCfg.ts frontend/src/views/tools/mmsmapping/utils/mappingDraft.ts
git commit -m "feat: add mmsmapping draft utilities"
```
### Task 2: Simplify The Left-Side Request And Result Panels
**Files:**
- Modify: `frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue`
- Modify: `frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue`
- [ ] **Step 1: Replace the request panel API so it only supports file selection and ICD parsing**
Update `frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue` to use this props/emits contract and action area:
```vue
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
name: 'MappingRequestPanel'
})
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
defineProps<{
selectedIcdFileName: string
isSubmitting: boolean
icdFileAccept: string
requestStatusText: string
requestStatusTagType: TagType
canReset: boolean
}>()
const emit = defineEmits<{
(event: 'file-change', value: Event): void
(event: 'parse'): void
(event: 'reset'): void
}>()
const icdFileInputRef = ref<HTMLInputElement | null>(null)
const openIcdFilePicker = () => {
icdFileInputRef.value?.click()
}
</script>
```
```vue
<template>
<section class="mapping-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">ICD 解析</h2>
<p class="panel-description">左上仅负责文件选择与解析解析完成后在右侧生成默认模板</p>
</div>
<el-tag :type="requestStatusTagType" effect="light">{{ requestStatusText }}</el-tag>
</div>
<div class="panel-content">
<div class="panel-section file-action-row">
<div class="file-select-row">
<el-input :model-value="selectedIcdFileName" readonly placeholder="请选择 `.icd`、`.cid`、`.scd` 或 `.xml` 文件" class="file-input" />
<el-button type="primary" :loading="isSubmitting" @click="openIcdFilePicker">选择 ICD</el-button>
<input ref="icdFileInputRef" class="hidden-file-input" type="file" :accept="icdFileAccept" @change="event => emit('file-change', event)" />
</div>
<el-button type="primary" plain :loading="isSubmitting" :disabled="!selectedIcdFileName" @click="emit('parse')">解析 ICD</el-button>
<el-button :disabled="!canReset" @click="emit('reset')">清空</el-button>
</div>
</div>
</section>
</template>
```
- [ ] **Step 2: Keep the result panel focused on `mappingJson` and `problems` only**
Update the header copy in `frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue`:
```vue
<div class="panel-header">
<div>
<h2 class="panel-title">调试输出</h2>
<p class="panel-description">左下只展示最近一次接口返回的 `mappingJson` `problems`</p>
</div>
<el-tag :type="responseStatusTagType" effect="light">{{ responseStatusText }}</el-tag>
</div>
```
Keep the existing tab body structure, but do not reintroduce `icdDocument` or request JSON rendering.
- [ ] **Step 3: Trim panel styles so the left-top card no longer reserves textarea space**
Remove the obsolete request textarea blocks from `frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue` and keep only these shared styles:
```scss
.panel-content {
display: flex;
flex: 1;
flex-direction: column;
gap: 16px;
min-height: 0;
}
.file-action-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
padding: 16px;
}
.file-select-row {
display: flex;
gap: 12px;
align-items: center;
min-width: 0;
flex: 1;
}
```
- [ ] **Step 4: Run lint on the two touched panel components**
Run:
```powershell
cd frontend
npm run lint -- src/views/tools/mmsmapping/components/MappingRequestPanel.vue src/views/tools/mmsmapping/components/MappingResultPanel.vue
```
Expected: command exits with code `0` and no ESLint diagnostics for those files.
- [ ] **Step 5: Commit the left-side panel refactor**
Run:
```powershell
git add frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue
git commit -m "refactor: simplify mmsmapping side panels"
```
### Task 3: Build The Right-Side Mapping Configuration Panel
**Files:**
- Create: `frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue`
- [ ] **Step 1: Create the component shell with typed props, emits, and immutable patch helpers**
Create `frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue` with this script scaffold:
```vue
<script setup lang="ts">
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
defineOptions({
name: 'MappingConfigPanel'
})
const props = defineProps<{
requestForm: MmsMapping.BaseRequestForm
draftGroups: MmsMapping.MappingDraftGroup[]
candidateGroups: MmsMapping.IndexCandidateGroup[]
isSubmitting: boolean
canGenerate: boolean
templateError: string
}>()
const emit = defineEmits<{
(event: 'update:requestForm', value: MmsMapping.BaseRequestForm): void
(event: 'update:draftGroups', value: MmsMapping.MappingDraftGroup[]): void
(event: 'generate'): void
}>()
const patchRequestForm = (key: keyof MmsMapping.BaseRequestForm, value: string) => {
emit('update:requestForm', {
...props.requestForm,
[key]: value
})
}
const patchDraftRow = (groupId: string, rowId: string, key: keyof MmsMapping.MappingDraftRow, value: string) => {
emit(
'update:draftGroups',
props.draftGroups.map(group =>
group.id !== groupId
? group
: {
...group,
rows: group.rows.map(row => (row.id !== rowId ? row : { ...row, [key]: value }))
}
)
)
}
const patchCandidateGroup = (groupId: string, nextGroupKey: string) => {
const nextCandidate = props.candidateGroups.find(candidate => candidate.groupKey === nextGroupKey)
const candidateReports = (nextCandidate?.reports || []).map(report => ({
reportName: report.reportName || '',
dataSetName: report.dataSetName || '',
reportDesc: report.reportDesc,
availableLnInstValues: report.availableLnInstValues || []
}))
emit(
'update:draftGroups',
props.draftGroups.map(group =>
group.id !== groupId
? group
: {
...group,
candidateGroupKey: nextGroupKey,
candidateGroupDesc: nextCandidate?.groupDesc || '',
matchStatus: nextCandidate ? 'matched' : 'pending',
candidateReports,
rows: group.rows.map(row => ({
...row,
reportName: '',
dataSetName: '',
lnInst: ''
}))
}
)
)
}
const getLnInstOptions = (group: MmsMapping.MappingDraftGroup, row: MmsMapping.MappingDraftRow) => {
const matchedReport = group.candidateReports.find(
report => report.reportName === row.reportName && report.dataSetName === row.dataSetName
)
return matchedReport?.availableLnInstValues || []
}
</script>
```
- [ ] **Step 2: Add the right-top base form, template error banner, and generate action**
Use this top section template in `MappingConfigPanel.vue`:
```vue
<template>
<section class="mapping-panel config-panel">
<div class="panel-header">
<div>
<h2 class="panel-title">系统配置</h2>
<p class="panel-description">右侧按 `DefaultCfg.txt` 自动生成默认模板用户可基于候选辅助信息反复修改并多次生成映射</p>
</div>
<el-button type="primary" :loading="isSubmitting" :disabled="!canGenerate || !!templateError" @click="emit('generate')">生成映射</el-button>
</div>
<div class="panel-content">
<el-alert v-if="templateError" :title="templateError" type="error" :closable="false" />
<div class="panel-section result-card">
<div class="section-title">请求基础字段</div>
<el-form label-position="top" class="request-form">
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="Version">
<el-input :model-value="requestForm.version" @update:model-value="value => patchRequestForm('version', value)" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Author">
<el-input :model-value="requestForm.author" @update:model-value="value => patchRequestForm('author', value)" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
```
- [ ] **Step 3: Render candidate helper info and editable rows for every template group**
Append this group-rendering block inside the same template:
```vue
<div v-for="group in draftGroups" :key="group.id" class="panel-section result-card draft-group-card">
<div class="draft-group-header">
<div>
<div class="section-title">{{ group.templateDesc }}</div>
<p class="section-description">模板标签{{ group.templateLabels.join('、') || '无' }}</p>
<p class="section-description">模板数据集{{ group.dataSetList.join('、') || '无' }}</p>
</div>
<el-tag :type="group.matchStatus === 'matched' ? 'success' : 'warning'" effect="light">
{{ group.matchStatus === 'matched' ? '已匹配候选组' : '待确认候选组' }}
</el-tag>
</div>
<el-form-item label="候选分组">
<el-select :model-value="group.candidateGroupKey" placeholder="请选择候选分组" @update:model-value="value => patchCandidateGroup(group.id, value)">
<el-option
v-for="candidate in candidateGroups"
:key="candidate.groupKey"
:label="candidate.groupDesc || candidate.groupKey || 'Unnamed group'"
:value="candidate.groupKey || ''"
/>
</el-select>
</el-form-item>
<div class="candidate-report-list">
<div v-for="report in group.candidateReports" :key="`${report.reportName}-${report.dataSetName}`" class="candidate-report-item">
<div class="candidate-report-name">{{ report.reportName }} / {{ report.dataSetName }}</div>
<div class="candidate-report-desc">{{ report.reportDesc || '无描述' }}</div>
<div class="candidate-report-lninst">可选 lnInst{{ report.availableLnInstValues.join('、') || '无' }}</div>
</div>
</div>
<el-table :data="group.rows" border class="draft-table">
<el-table-column prop="label" label="标签" min-width="140" />
<el-table-column label="reportName" min-width="180">
<template #default="{ row }">
<el-select :model-value="row.reportName" placeholder="选择 reportName" @update:model-value="value => patchDraftRow(group.id, row.id, 'reportName', value)">
<el-option v-for="report in group.candidateReports" :key="report.reportName" :label="report.reportName" :value="report.reportName" />
</el-select>
</template>
</el-table-column>
<el-table-column label="dataSetName" min-width="180">
<template #default="{ row }">
<el-select :model-value="row.dataSetName" placeholder="选择 dataSetName" @update:model-value="value => patchDraftRow(group.id, row.id, 'dataSetName', value)">
<el-option
v-for="report in group.candidateReports.filter(report => !row.reportName || report.reportName === row.reportName)"
:key="`${report.reportName}-${report.dataSetName}`"
:label="report.dataSetName"
:value="report.dataSetName"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="lnInst" min-width="160">
<template #default="{ row }">
<el-select :model-value="row.lnInst" placeholder="选择 lnInst" @update:model-value="value => patchDraftRow(group.id, row.id, 'lnInst', value)">
<el-option
v-for="lnInst in getLnInstOptions(group, row)"
:key="`${group.id}-${row.id}-${lnInst}`"
:label="lnInst"
:value="lnInst"
/>
</el-select>
</template>
</el-table-column>
</el-table>
</div>
</div>
</section>
</template>
```
Then add the minimum styles needed to keep the panel scrollable:
```scss
.config-panel {
min-height: 0;
}
.candidate-report-list {
display: grid;
gap: 8px;
margin-bottom: 16px;
}
.candidate-report-item {
padding: 12px;
border: 1px solid #dbe3f0;
border-radius: 10px;
background: #ffffff;
}
.draft-table {
width: 100%;
}
```
- [ ] **Step 4: Run type-check to validate the new configuration component**
Run:
```powershell
cd frontend
npm run type-check
```
Expected: command exits with code `0`; if it fails only because `index.vue` is not wired yet, proceed directly to Task 4 before re-running.
- [ ] **Step 5: Commit the new configuration panel**
Run:
```powershell
git add frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue
git commit -m "feat: add mmsmapping config panel"
```
### Task 4: Rebuild `index.vue` Around Parse-And-Generate Flow
**Files:**
- Modify: `frontend/src/views/tools/mmsmapping/index.vue`
- [ ] **Step 1: Replace raw JSON state with typed form, candidate, draft, and template-error state**
In `frontend/src/views/tools/mmsmapping/index.vue`, replace `requestJsonText`, `defaultRequestPayload`, and the old JSON parsing helpers with this state block. Keep the existing `unwrapApiPayload`, `getErrorMessage`, `handleIcdFileChange`, `mappingJsonPreview`, `problemList`, and status-tag computed blocks, but rewire them to the new request flow:
```ts
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { ResultData } from '@/api/interface'
import { getIcdMmsJsonApi } from '@/api/tools/mmsmapping'
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
import MappingRequestPanel from './components/MappingRequestPanel.vue'
import MappingResultPanel from './components/MappingResultPanel.vue'
import MappingConfigPanel from './components/MappingConfigPanel.vue'
import { parseDefaultCfgTemplate } from './utils/defaultCfg'
import {
buildDraftGroups,
buildIndexSelectionPayload,
createBaseRequestPayload,
validateDraftGroups
} from './utils/mappingDraft'
const selectedIcdFile = ref<File | null>(null)
const responsePayload = ref<MmsMapping.MappingTaskResponse | null>(null)
const activeResultTab = ref<'mapping' | 'problem'>('mapping')
const requestForm = ref<MmsMapping.BaseRequestForm>({
version: '1.0',
author: 'system'
})
const parsedCandidates = ref<MmsMapping.IndexCandidateGroup[]>([])
const configDraft = ref<MmsMapping.MappingDraftGroup[]>([])
const templateError = ref('')
const defaultCfgTemplate = ref<MmsMapping.DefaultCfgTemplate>({ reportList: [] })
const isParsing = ref(false)
const isGenerating = ref(false)
const icdFileAccept = '.icd,.cid,.scd,.xml'
try {
defaultCfgTemplate.value = parseDefaultCfgTemplate()
} catch {
templateError.value = 'DefaultCfg.txt 解析失败,请检查模板内容'
}
const isSubmitting = computed(() => isParsing.value || isGenerating.value)
const canGenerate = computed(() => Boolean(selectedIcdFile.value && configDraft.value.length && !templateError.value))
const selectedIcdFileName = computed(() => selectedIcdFile.value?.name || '')
```
- [ ] **Step 2: Add separate parse and generate handlers**
Use these handlers in `index.vue`:
```ts
const handleParseIcd = async () => {
if (!selectedIcdFile.value) {
ElMessage.warning('请先选择 ICD 文件')
return
}
if (templateError.value) {
ElMessage.error(templateError.value)
return
}
isParsing.value = true
responsePayload.value = null
try {
const response = await getIcdMmsJsonApi({
icdFile: selectedIcdFile.value,
request: {
...createBaseRequestPayload(requestForm.value),
indexSelection: []
}
})
const payload = unwrapApiPayload(response)
responsePayload.value = payload
parsedCandidates.value = payload.indexCandidates || []
configDraft.value = buildDraftGroups(defaultCfgTemplate.value, parsedCandidates.value)
activeResultTab.value = payload.mappingJson ? 'mapping' : 'problem'
ElMessage.success(payload.message || 'ICD 解析完成')
} catch (error) {
responsePayload.value = null
parsedCandidates.value = []
configDraft.value = []
ElMessage.error(getErrorMessage(error))
} finally {
isParsing.value = false
}
}
const handleGenerateMapping = async () => {
if (!selectedIcdFile.value) {
ElMessage.warning('请先选择 ICD 文件')
return
}
const draftProblems = validateDraftGroups(configDraft.value)
if (draftProblems.length) {
ElMessage.warning(draftProblems[0])
responsePayload.value = {
status: 'NEED_INDEX_SELECTION',
message: '当前配置不完整,请继续修正',
problems: draftProblems
}
activeResultTab.value = 'problem'
return
}
isGenerating.value = true
responsePayload.value = null
try {
const response = await getIcdMmsJsonApi({
icdFile: selectedIcdFile.value,
request: {
...createBaseRequestPayload(requestForm.value),
indexSelection: buildIndexSelectionPayload(configDraft.value)
}
})
const payload = unwrapApiPayload(response)
responsePayload.value = payload
activeResultTab.value = payload.mappingJson ? 'mapping' : 'problem'
if (payload.status === 'FAILED') {
ElMessage.error(payload.message || '映射生成失败')
return
}
ElMessage.success(payload.message || '映射生成完成')
} catch (error) {
responsePayload.value = null
ElMessage.error(getErrorMessage(error))
} finally {
isGenerating.value = false
}
}
const resetPage = () => {
selectedIcdFile.value = null
responsePayload.value = null
parsedCandidates.value = []
configDraft.value = []
activeResultTab.value = 'mapping'
requestForm.value = {
version: '1.0',
author: 'system'
}
}
```
- [ ] **Step 3: Rebuild the template and layout to left-stack the request/result panels and mount the new configuration panel**
Replace the page template and layout styles in `index.vue` with:
```vue
<template>
<div class="table-box mms-mapping-page">
<div class="mms-mapping-layout">
<div class="left-panel-stack">
<MappingRequestPanel
:selected-icd-file-name="selectedIcdFileName"
:is-submitting="isSubmitting"
:icd-file-accept="icdFileAccept"
:request-status-text="requestStatusText"
:request-status-tag-type="requestStatusTagType"
:can-reset="Boolean(selectedIcdFile || responsePayload || configDraft.length)"
@file-change="handleIcdFileChange"
@parse="handleParseIcd"
@reset="resetPage"
/>
<MappingResultPanel
v-model:active-result-tab="activeResultTab"
:response-status-text="responseStatusText"
:response-status-tag-type="responseStatusTagType"
:mapping-meta-text="mappingMetaText"
:mapping-json-preview="mappingJsonPreview"
:problem-tab-label="problemTabLabel"
:problem-list="problemList"
:problem-empty-text="problemEmptyText"
/>
</div>
<MappingConfigPanel
v-model:request-form="requestForm"
v-model:draft-groups="configDraft"
:candidate-groups="parsedCandidates"
:is-submitting="isSubmitting"
:can-generate="canGenerate"
:template-error="templateError"
@generate="handleGenerateMapping"
/>
</div>
</div>
</template>
```
```scss
.mms-mapping-layout {
display: grid;
grid-template-columns: minmax(320px, 0.9fr) minmax(0, 1.1fr);
gap: 16px;
width: 100%;
height: 100%;
overflow: hidden;
}
.left-panel-stack {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 16px;
min-height: 0;
}
@media (max-width: 1280px) {
.mms-mapping-layout {
grid-template-columns: 1fr;
}
}
```
- [ ] **Step 4: Run lint and type-check on the full frontend after container integration**
Run:
```powershell
cd frontend
npm run lint
npm run type-check
```
Expected: both commands exit with code `0`; the lint step may rewrite formatting, so inspect the diff before committing.
- [ ] **Step 5: Commit the new page flow**
Run:
```powershell
git add frontend/src/views/tools/mmsmapping/index.vue frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue frontend/src/views/tools/mmsmapping/utils/defaultCfg.ts frontend/src/views/tools/mmsmapping/utils/mappingDraft.ts frontend/src/api/tools/mmsmapping/interface/index.ts
git commit -m "feat: rebuild mmsmapping page workflow"
```
### Task 5: Verify The Two-Phase Workflow Manually
**Files:**
- Verify only: `frontend/src/views/tools/mmsmapping/index.vue`
- Verify only: `frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vue`
- Verify only: `frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue`
- Verify only: `frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vue`
- Verify only: `frontend/src/views/tools/mmsmapping/utils/defaultCfg.ts`
- Verify only: `frontend/src/views/tools/mmsmapping/utils/mappingDraft.ts`
- [ ] **Step 1: Run the full static verification suite one more time**
Run:
```powershell
cd frontend
npm run lint
npm run type-check
```
Expected: both commands exit with code `0`.
- [ ] **Step 2: Start the frontend and open the MMS mapping route**
Run:
```powershell
cd frontend
npm run dev
```
Expected: Vite starts successfully and prints a local URL. Open the page route that resolves to `/tools/mmsMapping`.
- [ ] **Step 3: Verify the parse flow**
Manual checklist:
```text
1. 进入页面后,左上仅看到文件选择、解析按钮和状态标签。
2. 选择一个合法 ICD 文件后,左上状态变为“待提交”或同类准备状态。
3. 点击“解析 ICD”后右侧出现 version/author 表单、默认模板分组、候选辅助信息。
4. 左下不出现 icdDocument 树;只显示 mappingJson/problems 页签。
5. 若后端返回 NEED_INDEX_SELECTION左下默认切到 problems。
```
Expected: all five observations are true.
- [ ] **Step 4: Verify repeated mapping generation without re-parsing**
Manual checklist:
```text
1. 在右侧选择一个模板分组,补齐 reportName、dataSetName、lnInst。
2. 点击“生成映射”,确认左下显示新的 mappingJson 或新的 problems。
3. 不重新点击“解析 ICD”直接修改右侧任意一行的 lnInst。
4. 再次点击“生成映射”,确认左下结果刷新为第二次生成结果。
5. 若第二次返回 NEED_INDEX_SELECTION 或 FAILED右侧已编辑内容仍然保留。
```
Expected: repeated generation works on the same parsed candidate set.
- [ ] **Step 5: Verify reset and file replacement behavior**
Manual checklist:
```text
1. 点击“清空”,确认左下结果、右侧草稿、当前候选缓存全部清空。
2. 重新选择另一个 ICD 文件,确认旧的候选和草稿不会继续显示。
3. 重新点击“解析 ICD”后右侧根据新文件重新生成默认模板。
```
Expected: reset and file replacement force a fresh parse cycle.
## Self-Review
- Spec coverage: the tasks cover left-top simplification, left-bottom result-only output, right-side auto-generated template editing, hidden request defaults, repeated generation, candidate matching, template-parse failure handling, and lint/type-check/manual validation.
- Placeholder scan: no `TODO`/`TBD`/“later” markers remain; every task includes exact file paths, code blocks, commands, and expected results.
- Type consistency: the plan uses the same names throughout: `BaseRequestForm`, `DefaultCfgTemplate`, `MappingDraftGroup`, `parseDefaultCfgTemplate`, `buildDraftGroups`, `validateDraftGroups`, `buildIndexSelectionPayload`, and `createBaseRequestPayload`.