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 http from '@/api'
|
||||||
import type { ParsePqdif } from './interface'
|
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) => {
|
export const parsePqdifApi = (pqdifFile: File) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
@@ -11,3 +26,47 @@ export const parsePqdifApi = (pqdifFile: File) => {
|
|||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
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 namespace ParsePqdif {
|
||||||
export type ParseStatus = 'SUCCESS' | 'FAILED' | (string & {})
|
export type ParseStatus = 'SUCCESS' | 'FAILED' | (string & {})
|
||||||
export type SeriesDataStatus = 'DATA_SUCCESS' | 'DATA_FAILED' | (string & {})
|
export type SeriesDataStatus = 'DATA_SUCCESS' | 'DATA_FAILED' | (string & {})
|
||||||
|
export type ParseResultValue = 0 | 1
|
||||||
|
|
||||||
export interface RecordItem {
|
export interface RecordItem {
|
||||||
recordIndex?: number
|
recordIndex?: number
|
||||||
@@ -53,4 +54,48 @@ export namespace ParsePqdif {
|
|||||||
records?: RecordItem[]
|
records?: RecordItem[]
|
||||||
observations?: ObservationItem[]
|
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"
|
:should-open-icd-consistency-problems="shouldOpenIcdConsistencyProblems"
|
||||||
:show-description="false"
|
:show-description="false"
|
||||||
@export-mapping="handleExportMappingEvent"
|
@export-mapping="handleExportMappingEvent"
|
||||||
@generate-xml-mapping="handleGenerateXmlMapping"
|
@generate-xml-mapping="handleGenerateXmlMappingEvent"
|
||||||
@icd-check="handleIcdConsistencyCheck"
|
@icd-check="handleIcdConsistencyCheck"
|
||||||
@confirm-icd-consistency-problems="handleConfirmIcdConsistencyProblemsEvent"
|
@confirm-icd-consistency-problems="handleConfirmIcdConsistencyProblemsEvent"
|
||||||
@remove-icd-consistency-problem="handleRemoveIcdConsistencyProblemEvent"
|
@remove-icd-consistency-problem="handleRemoveIcdConsistencyProblemEvent"
|
||||||
@@ -224,6 +224,13 @@ const handleExportMappingEvent = (...args: unknown[]) => {
|
|||||||
handleExportMapping(type)
|
handleExportMapping(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGenerateXmlMappingEvent = (...args: unknown[]) => {
|
||||||
|
const [dataType] = args
|
||||||
|
|
||||||
|
if (typeof dataType !== 'number') return
|
||||||
|
handleGenerateXmlMapping(dataType)
|
||||||
|
}
|
||||||
|
|
||||||
const handleConfirmIcdConsistencyProblemsEvent = () => {
|
const handleConfirmIcdConsistencyProblemsEvent = () => {
|
||||||
handleConfirmIcdConsistencyProblems()
|
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'
|
return 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGenerateXmlMapping = async () => {
|
const handleGenerateXmlMapping = async (dataType = 2) => {
|
||||||
const mappingJson = responsePayload.value?.mappingJson?.trim()
|
const mappingJson = responsePayload.value?.mappingJson?.trim()
|
||||||
|
|
||||||
if (!mappingJson) {
|
if (!mappingJson) {
|
||||||
@@ -370,7 +370,8 @@ export const useMmsMappingFlow = (options: UseMmsMappingFlowOptions = {}) => {
|
|||||||
// 关键业务节点:XML 映射依赖本次接口返回的完整 mappingJson,避免使用旧结果生成不一致的 XML 文件。
|
// 关键业务节点:XML 映射依赖本次接口返回的完整 mappingJson,避免使用旧结果生成不一致的 XML 文件。
|
||||||
const response = await getXmlFromJsonApi({
|
const response = await getXmlFromJsonApi({
|
||||||
request: {
|
request: {
|
||||||
mappingJson
|
mappingJson,
|
||||||
|
configType: dataType
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const payload = unwrapApiPayload<MmsMapping.MappingTaskResponse>(response)
|
const payload = unwrapApiPayload<MmsMapping.MappingTaskResponse>(response)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
:icon="Connection"
|
:icon="Connection"
|
||||||
:loading="isGeneratingXml"
|
:loading="isGeneratingXml"
|
||||||
:disabled="!canGenerateXmlMapping"
|
:disabled="!canGenerateXmlMapping"
|
||||||
@click="emit('generate-xml-mapping')"
|
@click="openXmlDataTypeDialog"
|
||||||
>
|
>
|
||||||
XML映射
|
XML映射
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -239,6 +239,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</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
|
<el-dialog
|
||||||
v-model="sequenceDialogVisible"
|
v-model="sequenceDialogVisible"
|
||||||
title="序列配置"
|
title="序列配置"
|
||||||
@@ -417,7 +434,7 @@ const props = withDefaults(defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'update:activeResultTab', value: ResultTab): void
|
(event: 'update:activeResultTab', value: ResultTab): void
|
||||||
(event: 'export-mapping', value: ExportMappingType): 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-mapping-json', value: string): void
|
||||||
(event: 'update:sequenceDialogVisible', value: boolean): void
|
(event: 'update:sequenceDialogVisible', value: boolean): void
|
||||||
(event: 'sequence-config-complete'): void
|
(event: 'sequence-config-complete'): void
|
||||||
@@ -446,6 +463,8 @@ const icdConsistencyProblemCount = computed(() => props.icdConsistencyProblemLis
|
|||||||
const problemDialogVisible = ref(false)
|
const problemDialogVisible = ref(false)
|
||||||
const icdConsistencyProblemDialogVisible = ref(false)
|
const icdConsistencyProblemDialogVisible = ref(false)
|
||||||
const matchResultDialogVisible = ref(false)
|
const matchResultDialogVisible = ref(false)
|
||||||
|
const xmlDataTypeDialogVisible = ref(false)
|
||||||
|
const xmlDataType = ref<number>(2)
|
||||||
const sequenceConfigRows = ref<SequenceConfigRow[]>([])
|
const sequenceConfigRows = ref<SequenceConfigRow[]>([])
|
||||||
const sequenceSearchKeyword = ref('')
|
const sequenceSearchKeyword = ref('')
|
||||||
const sequenceDialogVisible = computed({
|
const sequenceDialogVisible = computed({
|
||||||
@@ -634,6 +653,16 @@ const closeSequenceDialog = () => {
|
|||||||
sequenceDialogVisible.value = false
|
sequenceDialogVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openXmlDataTypeDialog = () => {
|
||||||
|
xmlDataType.value = 2
|
||||||
|
xmlDataTypeDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmXmlDataType = () => {
|
||||||
|
xmlDataTypeDialogVisible.value = false
|
||||||
|
emit('generate-xml-mapping', xmlDataType.value)
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.sequenceDialogVisible,
|
() => props.sequenceDialogVisible,
|
||||||
visible => {
|
visible => {
|
||||||
@@ -1001,6 +1030,12 @@ const confirmSequenceConfig = () => {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xml-data-type-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-search-bar {
|
.dialog-search-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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>
|
<template>
|
||||||
<section class="mapping-panel result-panel">
|
<section class="result-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div class="result-view-tabs">
|
<div class="result-view-tabs">
|
||||||
<button
|
<button
|
||||||
@@ -156,11 +156,13 @@ const formatFirstValues = (values?: number[]) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.mapping-panel {
|
.result-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -218,10 +220,6 @@ const formatFirstValues = (values?: number[]) => {
|
|||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-panel {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-content {
|
.panel-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -320,7 +318,7 @@ const formatFirstValues = (values?: number[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.mapping-panel {
|
.result-panel {
|
||||||
padding: 20px;
|
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 rootDir = path.resolve(currentDir, '../../../..')
|
||||||
const files = {
|
const files = {
|
||||||
page: path.resolve(rootDir, 'views/tools/parsePqdif/index.vue'),
|
page: path.resolve(rootDir, 'views/tools/parsePqdif/index.vue'),
|
||||||
flow: path.resolve(rootDir, 'views/tools/parsePqdif/utils/useParsePqdifFlow.ts'),
|
formDialog: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifPathFormDialog.vue'),
|
||||||
requestPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifRequestPanel.vue'),
|
|
||||||
summaryPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifSummaryPanel.vue'),
|
|
||||||
resultPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifResultPanel.vue'),
|
resultPanel: path.resolve(rootDir, 'views/tools/parsePqdif/components/PqdifResultPanel.vue'),
|
||||||
api: path.resolve(rootDir, 'api/tools/parsePqdif/index.ts'),
|
api: path.resolve(rootDir, 'api/tools/parsePqdif/index.ts'),
|
||||||
apiTypes: path.resolve(rootDir, 'api/tools/parsePqdif/interface/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 read = file => (exists(file) ? fs.readFileSync(file, 'utf8') : '')
|
||||||
|
|
||||||
const pageSource = read(files.page)
|
const pageSource = read(files.page)
|
||||||
const flowSource = read(files.flow)
|
const formDialogSource = read(files.formDialog)
|
||||||
const apiSource = read(files.api)
|
const apiSource = read(files.api)
|
||||||
const apiTypesSource = read(files.apiTypes)
|
const apiTypesSource = read(files.apiTypes)
|
||||||
|
|
||||||
const checks = [
|
const checks = [
|
||||||
['parsePqdif API file exists', () => exists(files.api)],
|
['parsePqdif API file exists', () => exists(files.api)],
|
||||||
['parsePqdif API type file exists', () => exists(files.apiTypes)],
|
['parsePqdif API type file exists', () => exists(files.apiTypes)],
|
||||||
['parsePqdif API uses documented endpoint', () => /\/api\/parse-pqdif\/parse/.test(apiSource)],
|
['parse API keeps documented endpoint', () => /\/api\/parse-pqdif\/parse/.test(apiSource)],
|
||||||
['parsePqdif API appends documented pqdifFile field', () => /append\('pqdifFile',\s*pqdifFile\)/.test(apiSource)],
|
['path API uses documented base endpoint', () => /\/api\/parse-pqdif\/pqdif-paths/.test(apiSource)],
|
||||||
['parsePqdif API sends multipart form data', () => /multipart\/form-data/.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 response status', () => /export\s+type\s+ParseStatus/.test(apiTypesSource)],
|
||||||
['parsePqdif types include observations', () => /interface\s+ObservationItem/.test(apiTypesSource)],
|
['parsePqdif types include stored path record', () => /interface\s+PqdifPathRecord/.test(apiTypesSource)],
|
||||||
['parsePqdif flow exists', () => exists(files.flow)],
|
['parsePqdif types include parse detail', () => /interface\s+PqdifParseDetail/.test(apiTypesSource)],
|
||||||
['parsePqdif flow imports API', () => /parsePqdifApi/.test(flowSource)],
|
['stored path record includes documented filePath', () => /interface\s+PqdifPathRecord[\s\S]*filePath\?:\s*string/.test(apiTypesSource)],
|
||||||
['parsePqdif flow validates .pqd and .pqdif suffixes', () => /SUPPORTED_PQDIF_EXTENSIONS/.test(flowSource)],
|
['parse detail uses documented filePath field', () => /interface\s+PqdifParseDetail[\s\S]*filePath\?:\s*string/.test(apiTypesSource)],
|
||||||
['request panel exists', () => exists(files.requestPanel)],
|
['save parse result request excludes undocumented jsonStr field', () => !/interface\s+SaveParseResultParams[\s\S]*jsonStr\?:/.test(apiTypesSource)],
|
||||||
['summary panel exists', () => exists(files.summaryPanel)],
|
['save parse result request excludes undocumented nativeVersion field', () => !/interface\s+SaveParseResultParams[\s\S]*nativeVersion\?:/.test(apiTypesSource)],
|
||||||
['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)
|
|
||||||
],
|
|
||||||
['page keeps route component name', () => /name:\s*'ParsePqdifView'/.test(pageSource)],
|
['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)
|
const failures = checks.filter(([, check]) => !check()).map(([name]) => name)
|
||||||
|
|||||||
@@ -1,58 +1,359 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="table-box parse-pqdif-page">
|
<div class="table-box parse-pqdif-page">
|
||||||
<section class="parse-pqdif-grid">
|
<ProTable
|
||||||
<PqdifRequestPanel
|
ref="proTable"
|
||||||
:selected-file-name="selectedFileName"
|
:columns="columns"
|
||||||
:selected-file-size-text="selectedFileSizeText"
|
:request-api="getTableList"
|
||||||
:is-parsing="isParsing"
|
:request-error="handleTableRequestError"
|
||||||
:can-parse="canParse"
|
:search-col="{ xs: 1, sm: 2, md: 2, lg: 4, xl: 4 }"
|
||||||
:can-reset="canReset"
|
:tool-button="['refresh', 'setting']"
|
||||||
:request-status-text="requestStatusText"
|
row-key="id"
|
||||||
:request-status-tag-type="requestStatusTagType"
|
>
|
||||||
@file-change="handleFileChange"
|
<template #tableHeader="scope">
|
||||||
@parse="parseSelectedFile"
|
<el-button type="primary" :icon="Plus" @click="openCreateDialog">新增</el-button>
|
||||||
@reset="resetPage"
|
<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" />
|
<PqdifPathFormDialog
|
||||||
|
v-model:visible="formDialogVisible"
|
||||||
<PqdifResultPanel
|
:mode="formMode"
|
||||||
v-model:active-tab="activeResultTab"
|
:record="currentFormRecord"
|
||||||
:records="records"
|
:saving="formSaving"
|
||||||
:observations="observations"
|
@submit="handleSavePqdifPath"
|
||||||
class="parse-pqdif-result"
|
|
||||||
/>
|
/>
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="tsx">
|
||||||
import PqdifRequestPanel from './components/PqdifRequestPanel.vue'
|
import { Delete, Download, Edit, Plus } from '@element-plus/icons-vue'
|
||||||
import PqdifResultPanel from './components/PqdifResultPanel.vue'
|
import { ElMessage, ElMessageBox, type TagProps } from 'element-plus'
|
||||||
import PqdifSummaryPanel from './components/PqdifSummaryPanel.vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useParsePqdifFlow } from './utils/useParsePqdifFlow'
|
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({
|
defineOptions({
|
||||||
name: 'ParsePqdifView'
|
name: 'ParsePqdifView'
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
type TagType = TagProps['type']
|
||||||
selectedFileName,
|
|
||||||
selectedFileSizeText,
|
const proTable = ref<ProTableInstance>()
|
||||||
parseResult,
|
const formDialogVisible = ref(false)
|
||||||
records,
|
const formSaving = ref(false)
|
||||||
observations,
|
const formMode = ref<'create' | 'edit'>('create')
|
||||||
isParsing,
|
const currentFormRecord = ref<ParsePqdif.PqdifPathRecord | null>(null)
|
||||||
isBusinessFailed,
|
|
||||||
activeResultTab,
|
const resultOptions = [
|
||||||
requestStatusText,
|
{ label: '成功', value: 1, tagType: 'success' },
|
||||||
requestStatusTagType,
|
{ label: '失败', value: 0, tagType: 'danger' }
|
||||||
canParse,
|
]
|
||||||
canReset,
|
|
||||||
handleFileChange,
|
function unwrapApiPayload<T>(response: ResultData<T> | T): T {
|
||||||
parseSelectedFile,
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
resetPage
|
return (response as ResultData<T>).data
|
||||||
} = useParsePqdifFlow()
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -62,23 +363,14 @@ const {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parse-pqdif-grid {
|
:deep(.pqdif-path-form-dialog .el-dialog__header) {
|
||||||
display: grid;
|
margin-right: 0;
|
||||||
grid-template-columns: minmax(0, 1fr) 420px;
|
padding: 12px 16px;
|
||||||
gap: 12px;
|
background: #536fe5;
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.parse-pqdif-result {
|
:deep(.pqdif-path-form-dialog .el-dialog__title),
|
||||||
grid-column: 1 / -1;
|
:deep(.pqdif-path-form-dialog .el-dialog__headerbtn .el-dialog__close) {
|
||||||
min-height: 0;
|
color: #ffffff;
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.parse-pqdif-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</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