995 lines
37 KiB
Markdown
995 lines
37 KiB
Markdown
# 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`.
|