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

37 KiB
Raw Permalink Blame History

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:

    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 mappingJson and problems only

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, and createBaseRequestPayload.