37 KiB
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.tsPurpose: Add front-end-only types for the editable base form, parsedDefaultCfgtemplate, draft groups, and row-level editing state. - Create:
frontend/src/views/tools/mmsmapping/utils/defaultCfg.tsPurpose: LoadDefaultCfg.txtas 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.tsPurpose: Hold request defaults plus the pure functions that match template groups toindexCandidates, build the editable draft, validate row completeness, and convert the draft intorequest.indexSelection. - Modify:
frontend/src/views/tools/mmsmapping/components/MappingRequestPanel.vuePurpose: 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.vuePurpose: Keep it result-only formappingJsonandproblems, with copy/layout aligned to the left-bottom output role. - Create:
frontend/src/views/tools/mmsmapping/components/MappingConfigPanel.vuePurpose: Render the right-sideversion/authorform, template/candidate helper info, editable draft rows, and the repeated生成映射action. - Modify:
frontend/src/views/tools/mmsmapping/index.vuePurpose: 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
lintandtype-checkscripts but no unit-test runner. Do not add Vitest/Jest in this task. Usenpm 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:
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:
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:
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:
cd frontend
npm run type-check
Expected: command exits with code 0 and no TypeScript diagnostics.
- Step 5: Commit the utility scaffolding
Run:
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:
<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>
<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
mappingJsonandproblemsonly
Update the header copy in frontend/src/views/tools/mmsmapping/components/MappingResultPanel.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:
.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:
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:
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:
<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:
<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:
<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:
.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:
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:
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:
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:
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:
<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>
.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:
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:
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:
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:
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:
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:
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:
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, andcreateBaseRequestPayload.