refactor(parsePqdif): 重构PQDIF解析页面为ProTable管理界面
- 将原有网格布局替换为ProTable组件进行数据管理 - 新增PQDIF路径表单对话框用于创建和编辑记录 - 实现PQDIF记录的增删改查功能 - 添加批量删除和JSON导出功能 - 集成文件上传和解析流程 - 更新API接口支持路径管理和解析结果存储 - 移除旧的请求面板、摘要面板和结果面板组件 - 调整MMS映射组件中的XML生成事件处理逻辑
This commit is contained in:
@@ -1,6 +1,21 @@
|
||||
import http from '@/api'
|
||||
import type { ParsePqdif } from './interface'
|
||||
|
||||
const PQDIF_PATH_BASE_URL = '/api/parse-pqdif/pqdif-paths'
|
||||
|
||||
const buildPqdifPathFormData = (request: ParsePqdif.PqdifPathSaveParams, pqdifFile?: File | null) => {
|
||||
const formData = new FormData()
|
||||
|
||||
if (pqdifFile) {
|
||||
formData.append('pqdifFile', pqdifFile)
|
||||
}
|
||||
|
||||
// 关键业务节点:记录上传接口固定读取 request 这个 JSON part,字段名不能调整。
|
||||
formData.append('request', new Blob([JSON.stringify(request)], { type: 'application/json' }))
|
||||
|
||||
return formData
|
||||
}
|
||||
|
||||
export const parsePqdifApi = (pqdifFile: File) => {
|
||||
const formData = new FormData()
|
||||
|
||||
@@ -11,3 +26,47 @@ export const parsePqdifApi = (pqdifFile: File) => {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
export const listPqdifPathsApi = (params: ParsePqdif.PqdifPathListParams = {}) => {
|
||||
const request = { ...params }
|
||||
delete request.pageNum
|
||||
delete request.pageSize
|
||||
|
||||
return http.post<ParsePqdif.PqdifPathRecord[]>(`${PQDIF_PATH_BASE_URL}/list`, request)
|
||||
}
|
||||
|
||||
export const addPqdifPathApi = (params: ParsePqdif.PqdifPathSaveParams, pqdifFile?: File | null) => {
|
||||
if (pqdifFile) {
|
||||
return http.post<boolean>(`${PQDIF_PATH_BASE_URL}/add`, buildPqdifPathFormData(params, pqdifFile), {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
return http.post<boolean>(`${PQDIF_PATH_BASE_URL}/add`, params)
|
||||
}
|
||||
|
||||
export const updatePqdifPathApi = (params: ParsePqdif.PqdifPathSaveParams, pqdifFile?: File | null) => {
|
||||
if (pqdifFile) {
|
||||
return http.post<boolean>(`${PQDIF_PATH_BASE_URL}/update`, buildPqdifPathFormData(params, pqdifFile), {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
|
||||
return http.post<boolean>(`${PQDIF_PATH_BASE_URL}/update`, params)
|
||||
}
|
||||
|
||||
export const deletePqdifPathsApi = (ids: string[]) => {
|
||||
return http.post<boolean>(`${PQDIF_PATH_BASE_URL}/delete`, ids)
|
||||
}
|
||||
|
||||
export const getPqdifParseMsgApi = (id: string) => {
|
||||
return http.post<ParsePqdif.PqdifParseMsg>(`${PQDIF_PATH_BASE_URL}/${id}/parse-msg`)
|
||||
}
|
||||
|
||||
export const getPqdifParseDetailApi = (id: string) => {
|
||||
return http.post<ParsePqdif.PqdifParseDetail>(`${PQDIF_PATH_BASE_URL}/${id}/parse-detail`)
|
||||
}
|
||||
|
||||
export const savePqdifParseResultApi = (id: string, params: ParsePqdif.SaveParseResultParams) => {
|
||||
return http.post<boolean>(`${PQDIF_PATH_BASE_URL}/${id}/parse-result`, params)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export namespace ParsePqdif {
|
||||
export type ParseStatus = 'SUCCESS' | 'FAILED' | (string & {})
|
||||
export type SeriesDataStatus = 'DATA_SUCCESS' | 'DATA_FAILED' | (string & {})
|
||||
export type ParseResultValue = 0 | 1
|
||||
|
||||
export interface RecordItem {
|
||||
recordIndex?: number
|
||||
@@ -53,4 +54,48 @@ export namespace ParsePqdif {
|
||||
records?: RecordItem[]
|
||||
observations?: ObservationItem[]
|
||||
}
|
||||
|
||||
export interface PqdifPathListParams {
|
||||
keyword?: string
|
||||
result?: ParseResultValue | ''
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface PqdifPathRecord {
|
||||
id?: string
|
||||
name?: string
|
||||
filePath?: string
|
||||
recordCount?: number
|
||||
observationCount?: number
|
||||
sampleValueCount?: number
|
||||
state?: number
|
||||
result?: ParseResultValue
|
||||
msg?: Record<string, unknown> | null
|
||||
createBy?: string
|
||||
createTime?: string
|
||||
updateBy?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface PqdifPathSaveParams {
|
||||
id?: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface PqdifParseDetail {
|
||||
id?: string
|
||||
name?: string
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
export type PqdifParseMsg = Record<string, unknown> | null
|
||||
|
||||
export interface SaveParseResultParams {
|
||||
recordCount?: number
|
||||
observationCount?: number
|
||||
sampleValueCount?: number
|
||||
result: ParseResultValue
|
||||
msg?: Record<string, unknown> | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
:should-open-icd-consistency-problems="shouldOpenIcdConsistencyProblems"
|
||||
:show-description="false"
|
||||
@export-mapping="handleExportMappingEvent"
|
||||
@generate-xml-mapping="handleGenerateXmlMapping"
|
||||
@generate-xml-mapping="handleGenerateXmlMappingEvent"
|
||||
@icd-check="handleIcdConsistencyCheck"
|
||||
@confirm-icd-consistency-problems="handleConfirmIcdConsistencyProblemsEvent"
|
||||
@remove-icd-consistency-problem="handleRemoveIcdConsistencyProblemEvent"
|
||||
@@ -224,6 +224,13 @@ const handleExportMappingEvent = (...args: unknown[]) => {
|
||||
handleExportMapping(type)
|
||||
}
|
||||
|
||||
const handleGenerateXmlMappingEvent = (...args: unknown[]) => {
|
||||
const [dataType] = args
|
||||
|
||||
if (typeof dataType !== 'number') return
|
||||
handleGenerateXmlMapping(dataType)
|
||||
}
|
||||
|
||||
const handleConfirmIcdConsistencyProblemsEvent = () => {
|
||||
handleConfirmIcdConsistencyProblems()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* eslint-env node */
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = path.resolve(currentDir, '../../../..')
|
||||
const files = {
|
||||
resultPanel: path.resolve(rootDir, 'views/tools/mmsMapping/components/MappingResultPanel.vue'),
|
||||
checkDialog: path.resolve(rootDir, 'views/tools/mmsMapping/components/IcdPathCheckDialog.vue'),
|
||||
flow: path.resolve(rootDir, 'views/tools/mmsMapping/utils/useMmsMappingFlow.ts')
|
||||
}
|
||||
|
||||
const read = file => fs.readFileSync(file, 'utf8')
|
||||
const resultPanelSource = read(files.resultPanel)
|
||||
const checkDialogSource = read(files.checkDialog)
|
||||
const flowSource = read(files.flow)
|
||||
|
||||
const checks = [
|
||||
['XML mapping opens data type dialog before generation', () => /openXmlDataTypeDialog/.test(resultPanelSource)],
|
||||
['XML data type defaults to provincial platform version', () => /xmlDataType\s*=\s*ref<number>\(2\)/.test(resultPanelSource)],
|
||||
[
|
||||
'XML data type dialog exposes MMS and provincial platform options',
|
||||
() =>
|
||||
/label="1"[\s\S]*MMS版本/.test(resultPanelSource) &&
|
||||
/label="2"[\s\S]*省级平台版本/.test(resultPanelSource)
|
||||
],
|
||||
[
|
||||
'XML generation event carries selected data type',
|
||||
() =>
|
||||
/event:\s*'generate-xml-mapping',\s*value:\s*number/.test(resultPanelSource) &&
|
||||
/emit\('generate-xml-mapping',\s*xmlDataType\.value\)/.test(resultPanelSource)
|
||||
],
|
||||
[
|
||||
'ICD path check dialog forwards XML data type to shared flow',
|
||||
() =>
|
||||
/@generate-xml-mapping="handleGenerateXmlMappingEvent"/.test(checkDialogSource) &&
|
||||
/const\s+handleGenerateXmlMappingEvent\s*=\s*\(\.\.\.args:\s*unknown\[\]\)/.test(checkDialogSource) &&
|
||||
/handleGenerateXmlMapping\(dataType\)/.test(checkDialogSource)
|
||||
],
|
||||
[
|
||||
'XML flow defaults and sends configType as backend data type',
|
||||
() =>
|
||||
/const\s+handleGenerateXmlMapping\s*=\s*async\s*\(dataType\s*=\s*2\)/.test(flowSource) &&
|
||||
/mappingJson,\s*configType:\s*dataType/.test(flowSource)
|
||||
]
|
||||
]
|
||||
|
||||
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
|
||||
|
||||
if (failures.length) {
|
||||
console.error('mmsMapping XML data type contract failed:')
|
||||
for (const failure of failures) {
|
||||
console.error(`- ${failure}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('mmsMapping XML data type contract passed')
|
||||
@@ -354,7 +354,7 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
return 'json'
|
||||
}
|
||||
|
||||
const handleGenerateXmlMapping = async () => {
|
||||
const handleGenerateXmlMapping = async (dataType = 2) => {
|
||||
const mappingJson = responsePayload.value?.mappingJson?.trim()
|
||||
|
||||
if (!mappingJson) {
|
||||
@@ -370,7 +370,8 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
||||
// 关键业务节点:XML 映射依赖本次接口返回的完整 mappingJson,避免使用旧结果生成不一致的 XML 文件。
|
||||
const response = await getXmlFromJsonApi({
|
||||
request: {
|
||||
mappingJson
|
||||
mappingJson,
|
||||
configType: dataType
|
||||
}
|
||||
})
|
||||
const payload = unwrapApiPayload<MmsMapping.MappingTaskResponse>(response)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
:icon="Connection"
|
||||
:loading="isGeneratingXml"
|
||||
:disabled="!canGenerateXmlMapping"
|
||||
@click="emit('generate-xml-mapping')"
|
||||
@click="openXmlDataTypeDialog"
|
||||
>
|
||||
XML映射
|
||||
</el-button>
|
||||
@@ -239,6 +239,23 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="xmlDataTypeDialogVisible"
|
||||
title="XML映射类型"
|
||||
width="420px"
|
||||
destroy-on-close
|
||||
top="18vh"
|
||||
>
|
||||
<el-radio-group v-model="xmlDataType" class="xml-data-type-options">
|
||||
<el-radio :label="1">MMS版本</el-radio>
|
||||
<el-radio :label="2">省级平台版本</el-radio>
|
||||
</el-radio-group>
|
||||
<template #footer>
|
||||
<el-button @click="xmlDataTypeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="isGeneratingXml" @click="confirmXmlDataType">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="sequenceDialogVisible"
|
||||
title="序列配置"
|
||||
@@ -417,7 +434,7 @@ const props = withDefaults(defineProps<{
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:activeResultTab', value: ResultTab): void
|
||||
(event: 'export-mapping', value: ExportMappingType): void
|
||||
(event: 'generate-xml-mapping'): void
|
||||
(event: 'generate-xml-mapping', value: number): void
|
||||
(event: 'update-mapping-json', value: string): void
|
||||
(event: 'update:sequenceDialogVisible', value: boolean): void
|
||||
(event: 'sequence-config-complete'): void
|
||||
@@ -446,6 +463,8 @@ const icdConsistencyProblemCount = computed(() => props.icdConsistencyProblemLis
|
||||
const problemDialogVisible = ref(false)
|
||||
const icdConsistencyProblemDialogVisible = ref(false)
|
||||
const matchResultDialogVisible = ref(false)
|
||||
const xmlDataTypeDialogVisible = ref(false)
|
||||
const xmlDataType = ref<number>(2)
|
||||
const sequenceConfigRows = ref<SequenceConfigRow[]>([])
|
||||
const sequenceSearchKeyword = ref('')
|
||||
const sequenceDialogVisible = computed({
|
||||
@@ -634,6 +653,16 @@ const closeSequenceDialog = () => {
|
||||
sequenceDialogVisible.value = false
|
||||
}
|
||||
|
||||
const openXmlDataTypeDialog = () => {
|
||||
xmlDataType.value = 2
|
||||
xmlDataTypeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const confirmXmlDataType = () => {
|
||||
xmlDataTypeDialogVisible.value = false
|
||||
emit('generate-xml-mapping', xmlDataType.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.sequenceDialogVisible,
|
||||
visible => {
|
||||
@@ -1001,6 +1030,12 @@ const confirmSequenceConfig = () => {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.xml-data-type-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visibleProxy"
|
||||
:title="recordName || 'PQDIF解析详情'"
|
||||
width="960px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="pqdif-path-detail-dialog"
|
||||
>
|
||||
<div v-loading="loading" class="detail-dialog-body">
|
||||
<el-alert v-if="parseError" :title="parseError" type="warning" show-icon :closable="false" />
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="原始文件路径">{{ detail?.filePath || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||||
<el-tab-pane label="解析结构" name="structured">
|
||||
<PqdifResultPanel
|
||||
v-model:active-tab="resultActiveTab"
|
||||
:records="parseResult?.records || []"
|
||||
:observations="parseResult?.observations || []"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="原始JSON" name="json">
|
||||
<pre class="json-content">{{ formattedJson }}</pre>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visibleProxy = false">关闭</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:loading="saving"
|
||||
:disabled="!canSaveParseResult"
|
||||
@click="emit('save-result')"
|
||||
>
|
||||
保存解析结果
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { ParsePqdif } from '@/api/tools/parsePqdif/interface'
|
||||
import PqdifResultPanel from './PqdifResultPanel.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'PqdifPathDetailDialog'
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
recordName: string
|
||||
loading: boolean
|
||||
saving: boolean
|
||||
detail: ParsePqdif.PqdifParseDetail | null
|
||||
parseResult: ParsePqdif.ParseResponse | null
|
||||
parseError: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:visible', value: boolean): void
|
||||
(event: 'save-result'): void
|
||||
}>()
|
||||
|
||||
const activeTab = ref('structured')
|
||||
const resultActiveTab = ref<'records' | 'observations'>('records')
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: value => emit('update:visible', value)
|
||||
})
|
||||
|
||||
const formattedJson = computed(() => {
|
||||
if (!props.parseResult) return '暂无完整解析结果'
|
||||
return JSON.stringify(props.parseResult, null, 4)
|
||||
})
|
||||
|
||||
const canSaveParseResult = computed(() => Boolean(props.detail?.id && props.parseResult))
|
||||
|
||||
watch(
|
||||
() => props.parseResult,
|
||||
result => {
|
||||
resultActiveTab.value = result?.observations?.length ? 'observations' : 'records'
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
visible => {
|
||||
if (visible) activeTab.value = 'structured'
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-dialog-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
min-height: 0;
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.json-content {
|
||||
max-height: 520px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background: #0f172a;
|
||||
color: #e5e7eb;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visibleProxy"
|
||||
:title="mode === 'create' ? '新增PQDIF记录' : '编辑PQDIF记录'"
|
||||
width="560px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="pqdif-path-form-dialog"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="formModel" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="PQDIF名称" prop="name">
|
||||
<el-input v-model="formModel.name" maxlength="120" show-word-limit placeholder="请输入PQDIF名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="PQDIF文件">
|
||||
<div class="file-select-row">
|
||||
<el-input :model-value="selectedFileName" readonly placeholder="可选择 .pqd 或 .pqdif 文件" />
|
||||
<el-button type="primary" plain :icon="FolderOpened" :disabled="saving" @click="openFilePicker">
|
||||
选择文件
|
||||
</el-button>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
class="hidden-file-input"
|
||||
type="file"
|
||||
accept=".pqd,.pqdif"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="saving" @click="visibleProxy = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FolderOpened } from '@element-plus/icons-vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import type { ParsePqdif } from '@/api/tools/parsePqdif/interface'
|
||||
|
||||
defineOptions({
|
||||
name: 'PqdifPathFormDialog'
|
||||
})
|
||||
|
||||
const SUPPORTED_PQDIF_EXTENSIONS = ['pqd', 'pqdif']
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
mode: 'create' | 'edit'
|
||||
record: ParsePqdif.PqdifPathRecord | null
|
||||
saving: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:visible', value: boolean): void
|
||||
(event: 'submit', value: { payload: ParsePqdif.PqdifPathSaveParams; file: File | null }): void
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const formModel = reactive({
|
||||
id: '',
|
||||
name: ''
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
name: [{ required: true, message: '请输入PQDIF名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const visibleProxy = computed({
|
||||
get: () => props.visible,
|
||||
set: value => emit('update:visible', value)
|
||||
})
|
||||
|
||||
const selectedFileName = computed(() => selectedFile.value?.name || '')
|
||||
|
||||
const getFileExtension = (fileName: string) => fileName.split('.').pop()?.toLowerCase() || ''
|
||||
|
||||
const isSupportedPqdifFile = (fileName: string) => SUPPORTED_PQDIF_EXTENSIONS.includes(getFileExtension(fileName))
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.clearValidate()
|
||||
selectedFile.value = null
|
||||
formModel.id = ''
|
||||
formModel.name = ''
|
||||
}
|
||||
|
||||
const fillForm = () => {
|
||||
formModel.id = props.record?.id || ''
|
||||
formModel.name = props.record?.name || ''
|
||||
selectedFile.value = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.mode, props.record?.id, props.record?.name],
|
||||
([visible]) => {
|
||||
if (visible) fillForm()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
const handleFileChange = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0] || null
|
||||
|
||||
input.value = ''
|
||||
if (!file) return
|
||||
|
||||
if (!isSupportedPqdifFile(file.name)) {
|
||||
ElMessage.warning('仅支持 .pqd 或 .pqdif 格式文件')
|
||||
return
|
||||
}
|
||||
|
||||
selectedFile.value = file
|
||||
if (!formModel.name.trim()) {
|
||||
formModel.name = file.name
|
||||
}
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
|
||||
if (!valid) return
|
||||
if (props.mode === 'create' && !selectedFile.value) {
|
||||
ElMessage.warning('新增PQDIF记录必须选择 .pqd 或 .pqdif 文件')
|
||||
return
|
||||
}
|
||||
if (props.mode === 'edit' && !formModel.id) {
|
||||
ElMessage.warning('当前PQDIF记录缺少 ID,不能保存修改')
|
||||
return
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
payload: {
|
||||
id: formModel.id || undefined,
|
||||
name: formModel.name.trim()
|
||||
},
|
||||
file: selectedFile.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-select-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,217 +0,0 @@
|
||||
<template>
|
||||
<section class="mapping-panel request-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<div class="panel-title-tabs">
|
||||
<span class="panel-title-tab">PQDIF解析</span>
|
||||
</div>
|
||||
<p class="panel-description">选择 .pqd 或 .pqdif 文件后,调用后端解析接口读取结构和采样摘要。</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="selectedFileName"
|
||||
readonly
|
||||
placeholder="请选择 .pqd 或 .pqdif 文件"
|
||||
class="file-input"
|
||||
/>
|
||||
<el-button type="primary" :icon="FolderOpened" :disabled="isParsing" @click="openFilePicker">
|
||||
选择文件
|
||||
</el-button>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
class="hidden-file-input"
|
||||
type="file"
|
||||
accept=".pqd,.pqdif"
|
||||
@change="event => emit('file-change', event)"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Search" :loading="isParsing" :disabled="!canParse" @click="emit('parse')">
|
||||
开始解析
|
||||
</el-button>
|
||||
<el-button type="danger" plain :icon="Delete" :disabled="!canReset" @click="emit('reset')">清空</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFileName" class="file-meta">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span class="file-meta__name">{{ selectedFileName }}</span>
|
||||
<span v-if="selectedFileSizeText" class="file-meta__size">{{ selectedFileSizeText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Delete, Document, FolderOpened, Search } from '@element-plus/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'PqdifRequestPanel'
|
||||
})
|
||||
|
||||
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
|
||||
|
||||
defineProps<{
|
||||
selectedFileName: string
|
||||
selectedFileSizeText: string
|
||||
isParsing: boolean
|
||||
canParse: boolean
|
||||
canReset: boolean
|
||||
requestStatusText: string
|
||||
requestStatusTagType: TagType
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'file-change', value: Event): void
|
||||
(event: 'parse'): void
|
||||
(event: 'reset'): void
|
||||
}>()
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mapping-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
.panel-title-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.panel-title-tab {
|
||||
position: relative;
|
||||
height: 36px;
|
||||
padding: 0 2px;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 13px;
|
||||
line-height: 36px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.panel-title-tab::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
background-color: var(--el-color-primary);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.panel-description {
|
||||
margin: 8px 0 0;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.file-action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.file-select-row {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.file-meta__name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-meta__size {
|
||||
flex: 0 0 auto;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mapping-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.file-select-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-action-row {
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<section class="mapping-panel result-panel">
|
||||
<section class="result-panel">
|
||||
<div class="panel-header">
|
||||
<div class="result-view-tabs">
|
||||
<button
|
||||
@@ -156,11 +156,13 @@ const formatFirstValues = (values?: number[]) => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mapping-panel {
|
||||
.result-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
@@ -218,10 +220,6 @@ const formatFirstValues = (values?: number[]) => {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -320,7 +318,7 @@ const formatFirstValues = (values?: number[]) => {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mapping-panel {
|
||||
.result-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<section class="mapping-panel summary-panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title-tabs">
|
||||
<span class="panel-title-tab">解析摘要</span>
|
||||
</div>
|
||||
<el-tag v-if="result" :type="statusTagType" effect="light">{{ statusText }}</el-tag>
|
||||
</div>
|
||||
|
||||
<template v-if="result">
|
||||
<el-alert
|
||||
v-if="result.message"
|
||||
:title="result.message"
|
||||
:type="isBusinessFailed ? 'error' : 'success'"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<div class="summary-metric-list">
|
||||
<div v-for="metric in summaryMetrics" :key="metric.label" class="summary-metric-item">
|
||||
<span class="summary-metric-item__label">{{ metric.label }}</span>
|
||||
<span class="summary-metric-item__value">{{ metric.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="文件名">{{ result.fileName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="native版本">{{ result.nativeVersion || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<el-empty v-else description="暂无解析摘要" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ParsePqdif } from '@/api/tools/parsePqdif/interface'
|
||||
|
||||
defineOptions({
|
||||
name: 'PqdifSummaryPanel'
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
result: ParsePqdif.ParseResponse | null
|
||||
isBusinessFailed: boolean
|
||||
}>()
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (props.result?.status === 'SUCCESS') return '成功'
|
||||
if (props.result?.status === 'FAILED') return '失败'
|
||||
return props.result?.status || '-'
|
||||
})
|
||||
|
||||
const statusTagType = computed(() => {
|
||||
if (props.result?.status === 'SUCCESS') return 'success'
|
||||
if (props.result?.status === 'FAILED') return 'danger'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
const summaryMetrics = computed(() => [
|
||||
{ label: 'Record', value: props.result?.recordCount ?? 0 },
|
||||
{ label: 'Observation', value: props.result?.observationCount ?? 0 },
|
||||
{ label: '采样上限', value: props.result?.sampleValueCount ?? 0 },
|
||||
{ label: '业务状态', value: props.result?.status || '-' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mapping-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 24px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-title-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.panel-title-tab {
|
||||
position: relative;
|
||||
height: 36px;
|
||||
padding: 0 2px;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 13px;
|
||||
line-height: 36px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.panel-title-tab::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
background-color: var(--el-color-primary);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.summary-metric-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.summary-metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.summary-metric-item__label {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.summary-metric-item__value {
|
||||
overflow: hidden;
|
||||
color: #1f2937;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -7,9 +7,7 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = path.resolve(currentDir, '../../../..')
|
||||
const files = {
|
||||
page: path.resolve(rootDir, 'views/tools/parsePqdif/index.vue'),
|
||||
flow: path.resolve(rootDir, 'views/tools/parsePqdif/utils/useParsePqdifFlow.ts'),
|
||||
requestPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifRequestPanel.vue'),
|
||||
summaryPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifSummaryPanel.vue'),
|
||||
formDialog: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifPathFormDialog.vue'),
|
||||
resultPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifResultPanel.vue'),
|
||||
api: path.resolve(rootDir, 'api/tools/parsePqdif/index.ts'),
|
||||
apiTypes: path.resolve(rootDir, 'api/tools/parsePqdif/interface/index.ts')
|
||||
@@ -19,39 +17,73 @@ const exists = file => fs.existsSync(file)
|
||||
const read = file => (exists(file) ? fs.readFileSync(file, 'utf8') : '')
|
||||
|
||||
const pageSource = read(files.page)
|
||||
const flowSource = read(files.flow)
|
||||
const formDialogSource = read(files.formDialog)
|
||||
const apiSource = read(files.api)
|
||||
const apiTypesSource = read(files.apiTypes)
|
||||
|
||||
const checks = [
|
||||
['parsePqdif API file exists', () => exists(files.api)],
|
||||
['parsePqdif API type file exists', () => exists(files.apiTypes)],
|
||||
['parsePqdif API uses documented endpoint', () => /\/api\/parse-pqdif\/parse/.test(apiSource)],
|
||||
['parsePqdif API appends documented pqdifFile field', () => /append\('pqdifFile',\s*pqdifFile\)/.test(apiSource)],
|
||||
['parsePqdif API sends multipart form data', () => /multipart\/form-data/.test(apiSource)],
|
||||
['parse API keeps documented endpoint', () => /\/api\/parse-pqdif\/parse/.test(apiSource)],
|
||||
['path API uses documented base endpoint', () => /\/api\/parse-pqdif\/pqdif-paths/.test(apiSource)],
|
||||
['path API includes list endpoint', () => /\/list/.test(apiSource) && /listPqdifPathsApi/.test(apiSource)],
|
||||
['path API includes add endpoint', () => /\/add/.test(apiSource) && /addPqdifPathApi/.test(apiSource)],
|
||||
['path API includes update endpoint', () => /\/update/.test(apiSource) && /updatePqdifPathApi/.test(apiSource)],
|
||||
['path API includes delete endpoint', () => /\/delete/.test(apiSource) && /deletePqdifPathsApi/.test(apiSource)],
|
||||
['path API includes parse-msg endpoint', () => /parse-msg/.test(apiSource) && /getPqdifParseMsgApi/.test(apiSource)],
|
||||
['path API includes parse-detail endpoint', () => /parse-detail/.test(apiSource) && /getPqdifParseDetailApi/.test(apiSource)],
|
||||
['path API includes parse-result endpoint', () => /parse-result/.test(apiSource) && /savePqdifParseResultApi/.test(apiSource)],
|
||||
['multipart upload appends documented pqdifFile field', () => /append\('pqdifFile',\s*pqdifFile\)/.test(apiSource)],
|
||||
['multipart upload appends documented request JSON part', () => /append\('request'/.test(apiSource) && /application\/json/.test(apiSource)],
|
||||
['parsePqdif types include response status', () => /export\s+type\s+ParseStatus/.test(apiTypesSource)],
|
||||
['parsePqdif types include observations', () => /interface\s+ObservationItem/.test(apiTypesSource)],
|
||||
['parsePqdif flow exists', () => exists(files.flow)],
|
||||
['parsePqdif flow imports API', () => /parsePqdifApi/.test(flowSource)],
|
||||
['parsePqdif flow validates .pqd and .pqdif suffixes', () => /SUPPORTED_PQDIF_EXTENSIONS/.test(flowSource)],
|
||||
['request panel exists', () => exists(files.requestPanel)],
|
||||
['summary panel exists', () => exists(files.summaryPanel)],
|
||||
['result panel exists', () => exists(files.resultPanel)],
|
||||
['page uses mmsMapping-style parse layout', () => /parse-pqdif-grid/.test(pageSource)],
|
||||
['request panel reuses mapping panel class', () => /class="mapping-panel/.test(read(files.requestPanel))],
|
||||
['request panel keeps compact action section', () => /panel-section file-action-row/.test(read(files.requestPanel))],
|
||||
['summary panel exposes metric cards', () => /summary-metric-list/.test(read(files.summaryPanel))],
|
||||
['result panel exposes records and observations tabs', () => /result-view-tabs/.test(read(files.resultPanel))],
|
||||
['result panel highlights failed series rows', () => /DATA_FAILED/.test(read(files.resultPanel))],
|
||||
[
|
||||
'page imports parsePqdif components',
|
||||
() =>
|
||||
/PqdifRequestPanel/.test(pageSource) &&
|
||||
/PqdifSummaryPanel/.test(pageSource) &&
|
||||
/PqdifResultPanel/.test(pageSource)
|
||||
],
|
||||
['parsePqdif types include stored path record', () => /interface\s+PqdifPathRecord/.test(apiTypesSource)],
|
||||
['parsePqdif types include parse detail', () => /interface\s+PqdifParseDetail/.test(apiTypesSource)],
|
||||
['stored path record includes documented filePath', () => /interface\s+PqdifPathRecord[\s\S]*filePath\?:\s*string/.test(apiTypesSource)],
|
||||
['parse detail uses documented filePath field', () => /interface\s+PqdifParseDetail[\s\S]*filePath\?:\s*string/.test(apiTypesSource)],
|
||||
['save parse result request excludes undocumented jsonStr field', () => !/interface\s+SaveParseResultParams[\s\S]*jsonStr\?:/.test(apiTypesSource)],
|
||||
['save parse result request excludes undocumented nativeVersion field', () => !/interface\s+SaveParseResultParams[\s\S]*nativeVersion\?:/.test(apiTypesSource)],
|
||||
['page keeps route component name', () => /name:\s*'ParsePqdifView'/.test(pageSource)],
|
||||
['page uses parsePqdif flow composable', () => /useParsePqdifFlow/.test(pageSource)]
|
||||
['page imports shared ProTable', () => /import\s+ProTable\s+from\s+'@\/components\/ProTable\/index\.vue'/.test(pageSource)],
|
||||
['page uses ProTable request API', () => /<ProTable[\s\S]*ref="proTable"[\s\S]*:request-api="getTableList"/.test(pageSource)],
|
||||
['page keeps default refresh and column setting tools', () => /:tool-button="\['refresh', 'setting'\]"/.test(pageSource)],
|
||||
['page exposes create and batch delete actions', () => /openCreateDialog/.test(pageSource) && /handleBatchDelete/.test(pageSource)],
|
||||
[
|
||||
'page table header exports selected PQDIF records as JSON',
|
||||
() => /<template\s+#tableHeader="scope">[\s\S]*handleExportSelectedJson\(scope\.selectedList\)/.test(pageSource)
|
||||
],
|
||||
[
|
||||
'PQDIF JSON export reads saved parse msg before download',
|
||||
() => /getPqdifParseMsgApi/.test(pageSource) && /parseResult/.test(pageSource) && /downloadTextFile/.test(pageSource)
|
||||
],
|
||||
[
|
||||
'PQDIF JSON export downloads json file',
|
||||
() => /handleExportSelectedJson[\s\S]*\.json[\s\S]*application\/json;charset=utf-8/.test(pageSource)
|
||||
],
|
||||
['page imports parse API for create flow', () => /parsePqdifApi/.test(pageSource)],
|
||||
[
|
||||
'page create flow parses uploaded file before saving documented parse result',
|
||||
() =>
|
||||
/parsePqdifApi\(file\)/.test(pageSource) &&
|
||||
/savePqdifParseResultApi\([^)]*buildSaveParseResultPayload\(parseResult\)/.test(pageSource)
|
||||
],
|
||||
['page stores full parse structure under documented msg object', () => /msg:\s*\{[\s\S]*parseResult[\s\S]*\}/.test(pageSource)],
|
||||
[
|
||||
'page shows PQDIF storage path immediately after name column',
|
||||
() =>
|
||||
/prop:\s*'name'[\s\S]*label:\s*'PQDIF名称'[\s\S]*prop:\s*'filePath'[\s\S]*label:\s*'PQDIF存储路径'/.test(pageSource) &&
|
||||
!/\{[^{}]*prop:\s*'filePath'[^{}]*isShow:\s*false[^{}]*\}/.test(pageSource)
|
||||
],
|
||||
['page hides row detail and summary actions', () => !/openDetailDialog/.test(pageSource) && !/openMsgDialog/.test(pageSource)],
|
||||
['page exposes row edit and delete actions', () => /openEditDialog/.test(pageSource) && /handleDeletePqdifPath/.test(pageSource)],
|
||||
['page adapts array list API to ProTable page structure', () => /records:\s*records\.slice/.test(pageSource) && /total:\s*records\.length/.test(pageSource)],
|
||||
['form dialog exists', () => exists(files.formDialog)],
|
||||
['form dialog validates .pqd and .pqdif suffixes', () => /SUPPORTED_PQDIF_EXTENSIONS/.test(formDialogSource)],
|
||||
['form dialog requires file when creating record', () => /props\.mode\s*===\s*'create'[\s\S]*!selectedFile\.value/.test(formDialogSource)],
|
||||
['form dialog backfills current record name while editing', () => /formModel\.name\s*=\s*props\.record\?\.name\s*\|\|\s*''/.test(formDialogSource)],
|
||||
['form dialog refreshes when current edit record changes', () => /props\.record\?\.id/.test(formDialogSource) && /props\.record\?\.name/.test(formDialogSource)],
|
||||
['form dialog emits payload and file', () => /event:\s*'submit'/.test(formDialogSource) && /file:\s*File\s*\|\s*null/.test(formDialogSource)],
|
||||
['result panel still exposes records and observations tabs', () => /result-view-tabs/.test(read(files.resultPanel))],
|
||||
['result panel highlights failed series rows', () => /DATA_FAILED/.test(read(files.resultPanel))]
|
||||
]
|
||||
|
||||
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
|
||||
|
||||
@@ -1,58 +1,359 @@
|
||||
<template>
|
||||
<div class="table-box parse-pqdif-page">
|
||||
<section class="parse-pqdif-grid">
|
||||
<PqdifRequestPanel
|
||||
:selected-file-name="selectedFileName"
|
||||
:selected-file-size-text="selectedFileSizeText"
|
||||
:is-parsing="isParsing"
|
||||
:can-parse="canParse"
|
||||
:can-reset="canReset"
|
||||
:request-status-text="requestStatusText"
|
||||
:request-status-tag-type="requestStatusTagType"
|
||||
@file-change="handleFileChange"
|
||||
@parse="parseSelectedFile"
|
||||
@reset="resetPage"
|
||||
/>
|
||||
<ProTable
|
||||
ref="proTable"
|
||||
:columns="columns"
|
||||
:request-api="getTableList"
|
||||
:request-error="handleTableRequestError"
|
||||
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
|
||||
:tool-button="['refresh', 'setting']"
|
||||
row-key="id"
|
||||
>
|
||||
<template #tableHeader="scope">
|
||||
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:icon="Delete"
|
||||
:disabled="!scope.isSelected"
|
||||
@click="handleBatchDelete(scope.selectedListIds)"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:icon="Download"
|
||||
:disabled="!scope.isSelected"
|
||||
@click="handleExportSelectedJson(scope.selectedList)"
|
||||
>
|
||||
导出JSON
|
||||
</el-button>
|
||||
</template>
|
||||
<template #operation="{ row }">
|
||||
<el-button link type="primary" :icon="Edit" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" :icon="Delete" @click="handleDeletePqdifPath(row)">删除</el-button>
|
||||
</template>
|
||||
</ProTable>
|
||||
|
||||
<PqdifSummaryPanel :result="parseResult" :is-business-failed="isBusinessFailed" />
|
||||
|
||||
<PqdifResultPanel
|
||||
v-model:active-tab="activeResultTab"
|
||||
:records="records"
|
||||
:observations="observations"
|
||||
class="parse-pqdif-result"
|
||||
/>
|
||||
</section>
|
||||
<PqdifPathFormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:mode="formMode"
|
||||
:record="currentFormRecord"
|
||||
:saving="formSaving"
|
||||
@submit="handleSavePqdifPath"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PqdifRequestPanel from './components/PqdifRequestPanel.vue'
|
||||
import PqdifResultPanel from './components/PqdifResultPanel.vue'
|
||||
import PqdifSummaryPanel from './components/PqdifSummaryPanel.vue'
|
||||
import { useParsePqdifFlow } from './utils/useParsePqdifFlow'
|
||||
<script setup lang="tsx">
|
||||
import { Delete, Download, Edit, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox, type TagProps } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { ResultData } from '@/api/interface'
|
||||
import ProTable from '@/components/ProTable/index.vue'
|
||||
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||
import {
|
||||
addPqdifPathApi,
|
||||
deletePqdifPathsApi,
|
||||
getPqdifParseMsgApi,
|
||||
listPqdifPathsApi,
|
||||
parsePqdifApi,
|
||||
savePqdifParseResultApi,
|
||||
updatePqdifPathApi
|
||||
} from '@/api/tools/parsePqdif'
|
||||
import type { ParsePqdif } from '@/api/tools/parsePqdif/interface'
|
||||
import PqdifPathFormDialog from './components/PqdifPathFormDialog.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'ParsePqdifView'
|
||||
})
|
||||
|
||||
const {
|
||||
selectedFileName,
|
||||
selectedFileSizeText,
|
||||
parseResult,
|
||||
records,
|
||||
observations,
|
||||
isParsing,
|
||||
isBusinessFailed,
|
||||
activeResultTab,
|
||||
requestStatusText,
|
||||
requestStatusTagType,
|
||||
canParse,
|
||||
canReset,
|
||||
handleFileChange,
|
||||
parseSelectedFile,
|
||||
resetPage
|
||||
} = useParsePqdifFlow()
|
||||
type TagType = TagProps['type']
|
||||
|
||||
const proTable = ref<ProTableInstance>()
|
||||
const formDialogVisible = ref(false)
|
||||
const formSaving = ref(false)
|
||||
const formMode = ref<'create' | 'edit'>('create')
|
||||
const currentFormRecord = ref<ParsePqdif.PqdifPathRecord | null>(null)
|
||||
|
||||
const resultOptions = [
|
||||
{ label: '成功', value: 1, tagType: 'success' },
|
||||
{ label: '失败', value: 0, tagType: 'danger' }
|
||||
]
|
||||
|
||||
function unwrapApiPayload<T>(response: ResultData<T> | T): T {
|
||||
if (response && typeof response === 'object' && 'data' in response) {
|
||||
return (response as ResultData<T>).data
|
||||
}
|
||||
|
||||
return response as T
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||
if (error instanceof Error && error.message) return error.message
|
||||
if (error && typeof error === 'object' && 'message' in error) return String((error as { message?: unknown }).message || fallback)
|
||||
return fallback
|
||||
}
|
||||
|
||||
const getResultText = (result?: number) => {
|
||||
if (result === 1) return '成功'
|
||||
if (result === 0) return '失败'
|
||||
return '-'
|
||||
}
|
||||
|
||||
const getResultTagType = (result?: number): TagType => {
|
||||
if (result === 1) return 'success'
|
||||
if (result === 0) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const buildSaveParseResultPayload = (parseResult: ParsePqdif.ParseResponse): ParsePqdif.SaveParseResultParams => ({
|
||||
recordCount: parseResult.recordCount,
|
||||
observationCount: parseResult.observationCount,
|
||||
sampleValueCount: parseResult.sampleValueCount,
|
||||
result: parseResult.status === 'FAILED' ? 0 : 1,
|
||||
// 关键业务节点:2026-06-22 接口只接收 msg 对象,完整解析结构放入 msg.parseResult 供详情页回显。
|
||||
msg: {
|
||||
summary: parseResult.message || (parseResult.status === 'FAILED' ? 'PQDIF解析失败' : 'PQDIF解析完成'),
|
||||
status: parseResult.status,
|
||||
fileName: parseResult.fileName,
|
||||
nativeVersion: parseResult.nativeVersion,
|
||||
parseResult
|
||||
}
|
||||
})
|
||||
|
||||
const getParseResultFromMsg = (msg: ParsePqdif.PqdifParseMsg): unknown => {
|
||||
if (!msg || typeof msg !== 'object') return null
|
||||
|
||||
return 'parseResult' in msg ? msg.parseResult : null
|
||||
}
|
||||
|
||||
const buildPqdifPathJsonRecord = (row: ParsePqdif.PqdifPathRecord, msg: ParsePqdif.PqdifParseMsg) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
filePath: row.filePath,
|
||||
recordCount: row.recordCount,
|
||||
observationCount: row.observationCount,
|
||||
sampleValueCount: row.sampleValueCount,
|
||||
state: row.state,
|
||||
result: row.result,
|
||||
msg,
|
||||
parseResult: getParseResultFromMsg(msg),
|
||||
createBy: row.createBy,
|
||||
createTime: row.createTime,
|
||||
updateBy: row.updateBy,
|
||||
updateTime: row.updateTime
|
||||
})
|
||||
|
||||
const downloadTextFile = (content: string, fileName: string, mimeType: string) => {
|
||||
const blob = new Blob([content], { type: mimeType })
|
||||
const objectUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
|
||||
link.href = objectUrl
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
|
||||
const fetchPqdifPathRecords = async (params: ParsePqdif.PqdifPathListParams = {}) => {
|
||||
const response = await listPqdifPathsApi(params)
|
||||
return unwrapApiPayload<ParsePqdif.PqdifPathRecord[]>(response) || []
|
||||
}
|
||||
|
||||
const resolveCreatedPqdifRecord = (
|
||||
beforeRecords: ParsePqdif.PqdifPathRecord[],
|
||||
afterRecords: ParsePqdif.PqdifPathRecord[],
|
||||
recordName: string
|
||||
) => {
|
||||
const beforeIds = new Set(beforeRecords.map(record => record.id).filter(Boolean))
|
||||
|
||||
return (
|
||||
afterRecords.find(record => record.id && !beforeIds.has(record.id) && record.name === recordName) ||
|
||||
afterRecords.find(record => record.id && record.name === recordName)
|
||||
)
|
||||
}
|
||||
|
||||
const columns = computed<ColumnProps<ParsePqdif.PqdifPathRecord>[]>(() => [
|
||||
{ type: 'selection', fixed: 'left', width: 70 },
|
||||
{
|
||||
prop: 'name',
|
||||
label: 'PQDIF名称',
|
||||
minWidth: 220,
|
||||
search: {
|
||||
el: 'input',
|
||||
label: 'PQDIF名称',
|
||||
order: 8
|
||||
}
|
||||
},
|
||||
{ prop: 'filePath', label: 'PQDIF存储路径', minWidth: 260, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'result',
|
||||
label: '解析结果',
|
||||
width: 120,
|
||||
render: scope => (
|
||||
<el-tag type={getResultTagType(scope.row.result)} effect="light">
|
||||
{getResultText(scope.row.result)}
|
||||
</el-tag>
|
||||
),
|
||||
enum: resultOptions,
|
||||
isFilterEnum: false,
|
||||
search: {
|
||||
el: 'select',
|
||||
label: '解析结果',
|
||||
order: 7
|
||||
}
|
||||
},
|
||||
{ prop: 'recordCount', label: 'Record', width: 110 },
|
||||
{ prop: 'observationCount', label: 'Observation', width: 130 },
|
||||
{ prop: 'sampleValueCount', label: '采样上限', width: 120 },
|
||||
{ prop: 'createBy', label: '创建人', width: 120, isShow: false },
|
||||
{ prop: 'createTime', label: '创建时间', minWidth: 170 },
|
||||
{ prop: 'updateTime', label: '更新时间', minWidth: 170, isShow: false },
|
||||
{ prop: 'operation', label: '操作', fixed: 'right', width: 150 }
|
||||
])
|
||||
|
||||
const getTableList = async (params: ParsePqdif.PqdifPathListParams = {}) => {
|
||||
const records = await fetchPqdifPathRecords(params)
|
||||
const pageNum = params.pageNum || 1
|
||||
const pageSize = params.pageSize || 10
|
||||
const startIndex = (pageNum - 1) * pageSize
|
||||
|
||||
// 关键业务节点:后端当前返回记录数组,这里适配 ProTable 分页结构以复用统一表格交互。
|
||||
return {
|
||||
data: {
|
||||
records: records.slice(startIndex, startIndex + pageSize),
|
||||
total: records.length,
|
||||
current: pageNum,
|
||||
size: pageSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refreshPqdifPaths = () => {
|
||||
proTable.value?.clearSelection()
|
||||
proTable.value?.getTableList()
|
||||
}
|
||||
|
||||
const handleTableRequestError = (error: unknown) => {
|
||||
ElMessage.error(getErrorMessage(error, 'PQDIF记录列表查询失败'))
|
||||
}
|
||||
|
||||
const openCreateDialog = () => {
|
||||
formMode.value = 'create'
|
||||
currentFormRecord.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEditDialog = (row: ParsePqdif.PqdifPathRecord) => {
|
||||
formMode.value = 'edit'
|
||||
currentFormRecord.value = row
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSavePqdifPath = async ({ payload, file }: { payload: ParsePqdif.PqdifPathSaveParams; file: File | null }) => {
|
||||
formSaving.value = true
|
||||
|
||||
try {
|
||||
if (formMode.value === 'create') {
|
||||
if (!file) {
|
||||
ElMessage.warning('新增PQDIF记录必须选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
const beforeRecords = await fetchPqdifPathRecords()
|
||||
await addPqdifPathApi(payload, file)
|
||||
|
||||
// 关键业务节点:新增记录后立即解析同一文件,并把解析摘要与完整 JSON 写回新记录。
|
||||
const parseResponse = await parsePqdifApi(file)
|
||||
const parseResult = unwrapApiPayload<ParsePqdif.ParseResponse>(parseResponse)
|
||||
const afterRecords = await fetchPqdifPathRecords()
|
||||
const createdRecord = resolveCreatedPqdifRecord(beforeRecords, afterRecords, payload.name)
|
||||
|
||||
if (!createdRecord?.id) {
|
||||
throw new Error('PQDIF记录新增成功,但未能定位新记录,解析结果未保存')
|
||||
}
|
||||
|
||||
await savePqdifParseResultApi(createdRecord.id, buildSaveParseResultPayload(parseResult))
|
||||
ElMessage.success('PQDIF记录新增、解析并保存成功')
|
||||
} else {
|
||||
await updatePqdifPathApi(payload, file)
|
||||
ElMessage.success('PQDIF记录编辑成功')
|
||||
}
|
||||
|
||||
formDialogVisible.value = false
|
||||
refreshPqdifPaths()
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, 'PQDIF记录保存失败'))
|
||||
} finally {
|
||||
formSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deletePqdifPaths = async (ids: string[], successMessage: string) => {
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请选择要删除的PQDIF记录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm('删除后PQDIF记录将被置为无效状态,是否继续?', '删除PQDIF记录', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await deletePqdifPathsApi(ids)
|
||||
ElMessage.success(successMessage)
|
||||
refreshPqdifPaths()
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, 'PQDIF记录删除失败'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeletePqdifPath = async (row: ParsePqdif.PqdifPathRecord) => {
|
||||
if (!row.id) {
|
||||
ElMessage.warning('当前PQDIF记录缺少 ID,不能删除')
|
||||
return
|
||||
}
|
||||
|
||||
await deletePqdifPaths([row.id], 'PQDIF记录删除成功')
|
||||
}
|
||||
|
||||
const handleBatchDelete = async (ids: string[]) => {
|
||||
await deletePqdifPaths(ids, 'PQDIF记录批量删除成功')
|
||||
}
|
||||
|
||||
const handleExportSelectedJson = async (records: ParsePqdif.PqdifPathRecord[]) => {
|
||||
if (!records.length) {
|
||||
ElMessage.warning('请选择要导出的 PQDIF 记录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const exportRecords = await Promise.all(
|
||||
records.map(async row => {
|
||||
const msg = row.id ? unwrapApiPayload<ParsePqdif.PqdifParseMsg>(await getPqdifParseMsgApi(row.id)) : null
|
||||
|
||||
return buildPqdifPathJsonRecord(row, msg)
|
||||
})
|
||||
)
|
||||
const json = JSON.stringify(exportRecords, null, 4)
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||||
|
||||
downloadTextFile(json, `pqdif-path-${timestamp}.json`, 'application/json;charset=utf-8')
|
||||
ElMessage.success('JSON 已导出')
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, 'PQDIF 记录 JSON 导出失败'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -62,23 +363,14 @@ const {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.parse-pqdif-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 420px;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
:deep(.pqdif-path-form-dialog .el-dialog__header) {
|
||||
margin-right: 0;
|
||||
padding: 12px 16px;
|
||||
background: #536fe5;
|
||||
}
|
||||
|
||||
.parse-pqdif-result {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.parse-pqdif-grid {
|
||||
grid-template-columns: 1fr;
|
||||
overflow-y: auto;
|
||||
}
|
||||
:deep(.pqdif-path-form-dialog .el-dialog__title),
|
||||
:deep(.pqdif-path-form-dialog .el-dialog__headerbtn .el-dialog__close) {
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { ResultData } from '@/api/interface'
|
||||
import { parsePqdifApi } from '@/api/tools/parsePqdif'
|
||||
import type { ParsePqdif } from '@/api/tools/parsePqdif/interface'
|
||||
|
||||
type ResultTab = 'records' | 'observations'
|
||||
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
|
||||
|
||||
const SUPPORTED_PQDIF_EXTENSIONS = ['pqd', 'pqdif']
|
||||
|
||||
const unwrapApiPayload = <T>(response: ResultData<T> | T): T => {
|
||||
if (response && typeof response === 'object' && 'data' in response) {
|
||||
return (response as ResultData<T>).data
|
||||
}
|
||||
|
||||
return response as T
|
||||
}
|
||||
|
||||
const getFileExtension = (fileName: string) => fileName.split('.').pop()?.toLowerCase() || ''
|
||||
|
||||
const formatFileSize = (size: number) => {
|
||||
if (!Number.isFinite(size) || size <= 0) return '0 B'
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||
return `${(size / 1024 / 1024).toFixed(2)} MB`
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: unknown) => {
|
||||
if (error instanceof Error && error.message) return error.message
|
||||
if (error && typeof error === 'object' && 'message' in error) return String((error as { message?: unknown }).message || '')
|
||||
return 'PQDIF解析接口调用失败,请检查后端服务和文件内容'
|
||||
}
|
||||
|
||||
export const useParsePqdifFlow = () => {
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const parseResult = ref<ParsePqdif.ParseResponse | null>(null)
|
||||
const isParsing = ref(false)
|
||||
const activeResultTab = ref<ResultTab>('records')
|
||||
|
||||
const selectedFileName = computed(() => selectedFile.value?.name || '')
|
||||
const selectedFileSizeText = computed(() => (selectedFile.value ? formatFileSize(selectedFile.value.size) : ''))
|
||||
const hasResult = computed(() => Boolean(parseResult.value))
|
||||
const isBusinessFailed = computed(() => parseResult.value?.status === 'FAILED')
|
||||
const records = computed(() => parseResult.value?.records || [])
|
||||
const observations = computed(() => parseResult.value?.observations || [])
|
||||
const canParse = computed(() => Boolean(selectedFile.value && !isParsing.value))
|
||||
const canReset = computed(() => Boolean(selectedFile.value || parseResult.value) && !isParsing.value)
|
||||
|
||||
const requestStatusText = computed(() => {
|
||||
if (isParsing.value) return '解析中'
|
||||
if (parseResult.value?.status === 'SUCCESS') return '解析成功'
|
||||
if (parseResult.value?.status === 'FAILED') return '解析失败'
|
||||
if (selectedFile.value) return '待解析'
|
||||
return '未选择文件'
|
||||
})
|
||||
|
||||
const requestStatusTagType = computed<TagType>(() => {
|
||||
if (isParsing.value) return 'warning'
|
||||
if (parseResult.value?.status === 'SUCCESS') return 'success'
|
||||
if (parseResult.value?.status === 'FAILED') return 'danger'
|
||||
if (selectedFile.value) return 'primary'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
const isSupportedPqdifFile = (fileName: string) => SUPPORTED_PQDIF_EXTENSIONS.includes(getFileExtension(fileName))
|
||||
|
||||
const setSelectedFile = (file: File | null) => {
|
||||
if (!file) return
|
||||
|
||||
if (!isSupportedPqdifFile(file.name)) {
|
||||
ElMessage.warning('仅支持 .pqd 或 .pqdif 格式文件')
|
||||
return
|
||||
}
|
||||
|
||||
selectedFile.value = file
|
||||
parseResult.value = null
|
||||
activeResultTab.value = 'records'
|
||||
}
|
||||
|
||||
const handleFileChange = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0] || null
|
||||
|
||||
setSelectedFile(file)
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
const parseSelectedFile = async () => {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请先选择 PQDIF 文件')
|
||||
return
|
||||
}
|
||||
|
||||
isParsing.value = true
|
||||
try {
|
||||
// 关键业务节点:HTTP code 成功不等于解析成功,业务失败需要读取 data.status 和 data.message。
|
||||
const response = await parsePqdifApi(selectedFile.value)
|
||||
const payload = unwrapApiPayload(response)
|
||||
|
||||
parseResult.value = payload
|
||||
activeResultTab.value = payload.observations?.length ? 'observations' : 'records'
|
||||
|
||||
if (payload.status === 'FAILED') {
|
||||
ElMessage.error(payload.message || 'PQDIF解析失败')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success(payload.message || 'PQDIF解析完成')
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error))
|
||||
} finally {
|
||||
isParsing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetPage = () => {
|
||||
selectedFile.value = null
|
||||
parseResult.value = null
|
||||
activeResultTab.value = 'records'
|
||||
}
|
||||
|
||||
return {
|
||||
selectedFile,
|
||||
selectedFileName,
|
||||
selectedFileSizeText,
|
||||
parseResult,
|
||||
records,
|
||||
observations,
|
||||
isParsing,
|
||||
isBusinessFailed,
|
||||
hasResult,
|
||||
activeResultTab,
|
||||
requestStatusText,
|
||||
requestStatusTagType,
|
||||
canParse,
|
||||
canReset,
|
||||
handleFileChange,
|
||||
parseSelectedFile,
|
||||
resetPage
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user