From cd54bb676c76187184dce3da8e14a258e6e5cc2f Mon Sep 17 00:00:00 2001 From: yexb <553699424@qq.com> Date: Sat, 9 May 2026 07:53:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20=E6=96=B0=E5=A2=9E=E5=8F=B0?= =?UTF-8?q?=E8=B4=A6=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 addLedger API 接口定义和实现 - 创建工程配置表单组件 (EngineeringForm) - 创建设备配置表单组件 (EquipmentForm) - 创建项目和测点表单组件 (ProjectForm, LineForm) - 实现台账树形结构面板 (LedgerTreePanel) - 添加台账数据验证契约检查脚本 - 集成字典选项和动态表单验证功能 - 实现台账节点增删改查完整流程 - 优化 Echarts 图表组件分组渲染性能 --- .../addLedger/check-api-debug-contract.mjs | 39 + frontend/src/api/tools/addLedger/index.ts | 172 +++++ .../api/tools/addLedger/interface/index.ts | 110 +++ .../src/components/echarts/line/index.vue | 70 +- frontend/src/routers/modules/staticRouter.ts | 8 + .../addLedger/components/EngineeringForm.vue | 119 +++ .../addLedger/components/EquipmentForm.vue | 152 ++++ .../addLedger/components/LedgerTreePanel.vue | 175 +++++ .../tools/addLedger/components/LineForm.vue | 254 +++++++ .../addLedger/components/ProjectForm.vue | 118 +++ .../addLedger/components/ledgerForm.scss | 61 ++ frontend/src/views/tools/addLedger/index.vue | 718 ++++++++++++++++++ frontend/src/views/tools/index.vue | 5 + .../components/MappingResultPanel.vue | 30 +- 14 files changed, 2005 insertions(+), 26 deletions(-) create mode 100644 frontend/src/api/tools/addLedger/check-api-debug-contract.mjs create mode 100644 frontend/src/api/tools/addLedger/index.ts create mode 100644 frontend/src/api/tools/addLedger/interface/index.ts create mode 100644 frontend/src/views/tools/addLedger/components/EngineeringForm.vue create mode 100644 frontend/src/views/tools/addLedger/components/EquipmentForm.vue create mode 100644 frontend/src/views/tools/addLedger/components/LedgerTreePanel.vue create mode 100644 frontend/src/views/tools/addLedger/components/LineForm.vue create mode 100644 frontend/src/views/tools/addLedger/components/ProjectForm.vue create mode 100644 frontend/src/views/tools/addLedger/components/ledgerForm.scss create mode 100644 frontend/src/views/tools/addLedger/index.vue diff --git a/frontend/src/api/tools/addLedger/check-api-debug-contract.mjs b/frontend/src/api/tools/addLedger/check-api-debug-contract.mjs new file mode 100644 index 0000000..7822c77 --- /dev/null +++ b/frontend/src/api/tools/addLedger/check-api-debug-contract.mjs @@ -0,0 +1,39 @@ +/* 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 apiFile = path.join(currentDir, 'index.ts') +const interfaceFile = path.join(currentDir, 'interface', 'index.ts') + +const apiSource = fs.readFileSync(apiFile, 'utf8') +const interfaceSource = fs.readFileSync(interfaceFile, 'utf8') + +const expectations = [ + ['equipment payload maps devType', /devType:\s*params\.dev_type/], + ['equipment payload maps devModel', /devModel:\s*params\.dev_model/], + ['equipment payload maps devAccessMethod', /devAccessMethod:\s*params\.dev_access_method/], + ['equipment payload maps nodeId', /nodeId:\s*params\.node_id/], + ['equipment payload maps nodeProcess', /nodeProcess:\s*resolveOptionalNumber\(params\.node_process\)/], + ['line payload maps lineId', /lineId:\s*resolveOptionalText\(params\.line_id\s*\|\|\s*params\.id\)/], + ['line payload maps lineNo', /lineNo:\s*params\.line_no/], + ['line payload maps volGrade', /volGrade:\s*params\.vol_grade/], + ['line payload maps ctRatio', /ctRatio:\s*params\.ct_ratio/], + ['line payload maps isGovern', /isGovern:\s*params\.is_govern/], + ['tree node supports parentId', /parentId\?:\s*string/], + ['tree node supports parentIds', /parentIds\?:\s*string/], + ['delete response type is boolean', /requestAddLedger\('delete',\s*'\/node'/] +] + +const failures = expectations.filter(([, pattern]) => !pattern.test(`${apiSource}\n${interfaceSource}`)) + +if (failures.length) { + console.error('addLedger API_DEBUG contract check failed:') + for (const [name] of failures) { + console.error(`- ${name}`) + } + process.exit(1) +} + +console.log('addLedger API_DEBUG contract check passed') diff --git a/frontend/src/api/tools/addLedger/index.ts b/frontend/src/api/tools/addLedger/index.ts new file mode 100644 index 0000000..5ba34da --- /dev/null +++ b/frontend/src/api/tools/addLedger/index.ts @@ -0,0 +1,172 @@ +import http from '@/api' +import type { ResultData } from '@/api/interface' +import type { AddLedger } from './interface' + +type AddLedgerRequestMethod = 'get' | 'post' | 'delete' + +const ADD_LEDGER_ROUTE_PATHS = ['/addLedger', '/api/addLedger'] as const +const ADD_LEDGER_BASE_URL = String(import.meta.env.VITE_API_URL || '').trim() + +const resolveOptionalText = (value: unknown) => { + if (value === null || value === undefined) return undefined + const text = String(value).trim() + return text || undefined +} + +const resolveOptionalNumber = (value: unknown) => { + if (value === null || value === undefined || value === '') return undefined + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined +} + +const toAddLedgerProjectPayload = (params: AddLedger.ProjectForm) => ({ + id: resolveOptionalText(params.id), + engineeringId: resolveOptionalText(params.engineeringId || params.parentId), + name: params.name, + area: params.area, + description: params.description +}) + +const toAddLedgerEquipmentPayload = (params: AddLedger.EquipmentForm) => ({ + id: resolveOptionalText(params.id), + projectId: resolveOptionalText(params.projectId || params.parentId), + name: params.name, + ndid: params.ndid, + mac: params.mac, + devType: params.dev_type, + devModel: params.dev_model, + devAccessMethod: params.dev_access_method, + nodeId: params.node_id, + nodeProcess: resolveOptionalNumber(params.node_process), + upgrade: params.upgrade +}) + +const toAddLedgerLinePayload = (params: AddLedger.LineForm) => ({ + lineId: resolveOptionalText(params.line_id || params.id), + deviceId: resolveOptionalText(params.deviceId || params.parentId), + name: params.name, + lineNo: params.line_no, + conType: params.conType, + volGrade: params.vol_grade, + position: params.position, + ctRatio: params.ct_ratio, + ct2Ratio: params.ct2_ratio, + ptRatio: params.pt_ratio, + pt2Ratio: params.pt2_ratio, + shortCircuitCapacity: params.short_circuit_capacity, + basicCapacity: params.basic_capacity, + protocolCapacity: params.protocol_capacity, + devCapacity: params.dev_capacity, + monitorObj: params.monitor_obj, + isGovern: params.is_govern, + monitorUser: params.monitor_user, + isImportant: params.is_important +}) + +const resolveDevProxyTarget = () => { + const proxyConfig = import.meta.env.VITE_PROXY + if (!Array.isArray(proxyConfig)) return '' + + const matchedProxy = proxyConfig.find(item => Array.isArray(item) && item[0] === '/api') + if (!matchedProxy?.[1]) return '' + + return String(matchedProxy[1]).replace(/\/+$/, '') +} + +const buildAddLedgerRequestPaths = (path: string) => { + const requestPaths = new Set() + const devProxyTarget = resolveDevProxyTarget() + + for (const routePath of ADD_LEDGER_ROUTE_PATHS) { + if (ADD_LEDGER_BASE_URL === '/api' && routePath.startsWith('/api/')) { + if (devProxyTarget) { + requestPaths.add(`${devProxyTarget}${routePath}${path}`) + } + + requestPaths.add(`${window.location.origin}${routePath}${path}`) + continue + } + + requestPaths.add(`${routePath}${path}`) + } + + return Array.from(requestPaths) +} + +const isFallbackableAddLedgerError = (error: unknown) => { + const responseCode = typeof error === 'object' && error !== null && 'code' in error ? String(error.code) : '' + const responseMessage = typeof error === 'object' && error !== null && 'message' in error ? String(error.message) : '' + const normalizedMessage = responseMessage.toLowerCase() + + return ( + responseCode === '404' || + normalizedMessage.includes('unknown operate') || + normalizedMessage.includes('not found') || + normalizedMessage.includes('no handler found') + ) +} + +const requestAddLedger = async ( + method: AddLedgerRequestMethod, + path: string, + params?: object +): Promise> => { + let lastError: unknown + const requestPaths = buildAddLedgerRequestPaths(path) + + for (let index = 0; index < requestPaths.length; index += 1) { + const requestPath = requestPaths[index] + + try { + if (method === 'get') { + return await http.get(requestPath, params) + } + + if (method === 'delete') { + return await http.delete(requestPath, params) + } + + return await http.post(requestPath, params) + } catch (error) { + lastError = error + + if (index === requestPaths.length - 1 || !isFallbackableAddLedgerError(error)) { + throw error + } + } + } + + throw lastError +} + +export const getAddLedgerTree = () => { + return requestAddLedger('get', '/tree') +} + +export const getAddLedgerDetail = (params: AddLedger.DetailParams) => { + return requestAddLedger('get', '/detail', params) +} + +export const saveAddLedgerEngineering = (params: AddLedger.EngineeringForm) => { + return requestAddLedger('post', '/engineering/save', params) +} + +export const saveAddLedgerProject = (params: AddLedger.ProjectForm) => { + return requestAddLedger('post', '/project/save', toAddLedgerProjectPayload(params)) +} + +export const saveAddLedgerEquipment = (params: AddLedger.EquipmentForm) => { + return requestAddLedger('post', '/equipment/save', toAddLedgerEquipmentPayload(params)) +} + +export const saveAddLedgerLine = (params: AddLedger.LineForm) => { + return requestAddLedger('post', '/line/save', toAddLedgerLinePayload(params)) +} + +export const getAvailableLineNos = (params: AddLedger.AvailableLineNoParams) => { + return requestAddLedger('get', '/line/availableLineNos', params) +} + +export const deleteAddLedgerNode = (params: AddLedger.DeleteNodeParams) => { + return requestAddLedger('delete', '/node', params) +} diff --git a/frontend/src/api/tools/addLedger/interface/index.ts b/frontend/src/api/tools/addLedger/interface/index.ts new file mode 100644 index 0000000..9a99a46 --- /dev/null +++ b/frontend/src/api/tools/addLedger/interface/index.ts @@ -0,0 +1,110 @@ +export namespace AddLedger { + export type NodeLevel = 0 | 1 | 2 | 3 + + export interface LedgerTreeNode { + id?: string + Id?: string + pid?: string + Pid?: string + pids?: string + Pids?: string + parentId?: string + parentIds?: string + name?: string + Name?: string + level?: NodeLevel + Level?: NodeLevel + state?: number + State?: number + children?: LedgerTreeNode[] + } + + export interface NormalizedTreeNode { + id: string + pid: string + pids: string + name: string + level: NodeLevel + children: NormalizedTreeNode[] + raw: LedgerTreeNode + } + + export interface DetailParams { + id: string + level: NodeLevel + } + + export interface EngineeringForm { + id?: string + name: string + province?: string + city?: string + description?: string + } + + export interface ProjectForm { + id?: string + engineeringId?: string + parentId?: string + name: string + area?: string + description?: string + } + + export interface EquipmentForm { + id?: string + engineeringId?: string + projectId?: string + parentId?: string + name: string + ndid: string + mac: string + dev_type?: string + dev_model: string + dev_access_method?: string + node_id?: string + node_process?: string + upgrade?: number + } + + export interface LineForm { + id?: string + line_id?: string + deviceId?: string + parentId?: string + name: string + line_no?: number + conType?: number + vol_grade?: number + position?: string + ct_ratio?: number + ct2_ratio?: number + pt_ratio?: number + pt2_ratio?: number + short_circuit_capacity?: number + basic_capacity?: number + protocol_capacity?: number + dev_capacity?: number + monitor_obj?: string + is_govern?: number + monitor_user?: string + is_important?: number + } + + export type NodeDetail = EngineeringForm | ProjectForm | EquipmentForm | LineForm + + export interface AvailableLineNoParams { + deviceId: string + lineId?: string + } + + export interface DeleteNodeParams { + id: string + level: NodeLevel + } + + export interface SelectOption { + label: string + value: T + } +} diff --git a/frontend/src/components/echarts/line/index.vue b/frontend/src/components/echarts/line/index.vue index 910a2f8..a517064 100644 --- a/frontend/src/components/echarts/line/index.vue +++ b/frontend/src/components/echarts/line/index.vue @@ -49,6 +49,7 @@ const emit = defineEmits<{ }>() let chart: echarts.ECharts | any = null let isPanPointerDown = false +let currentGroup: string | undefined const getChartViewportRoot = () => chart?.getZr()?.painter?.getViewportRoot?.() as HTMLElement | undefined @@ -199,18 +200,18 @@ const resizeHandler = () => { chart.getZr().painter.getViewportRoot().style.display = '' }) } -const initChart = () => { - if (!props.isInterVal && !props.pieInterVal) { - unbindPanCursorEvents() - chart?.dispose() - } - // chart?.dispose() - chart = echarts.init(chartRef.value as HTMLDivElement) +const updateChartGroup = () => { + if (!chart || props.group === currentGroup) return + + currentGroup = props.group + chart.group = props.group || undefined + if (props.group) { - chart.group = props.group echarts.connect(props.group) } +} +const buildChartOptions = () => { const options = { title: { left: 'center', @@ -295,7 +296,10 @@ const initChart = () => { // console.log(options.series,"获取x轴"); handlerBar(options) - chart.setOption(options, true) + return options +} + +const bindChartEvents = () => { chart.off('datazoom') chart.on('datazoom', (params: any) => { const zoomPayload = Array.isArray(params.batch) ? params.batch[0] : params @@ -309,16 +313,51 @@ const initChart = () => { end }) }) + bindPanCursorEvents() +} + +const renderChart = (isInitial = false) => { + if (!chartRef.value) return + + if (!chart) { + chart = echarts.init(chartRef.value) + updateChartGroup() + bindChartEvents() + } else { + updateChartGroup() + } + + const options = buildChartOptions() + + chart.setOption(options, { + notMerge: isInitial, + lazyUpdate: !isInitial + }) chart.dispatchAction({ type: 'takeGlobalCursor', key: 'dataZoomSelect', dataZoomSelectActive: props.options?.activeTool === 'box-zoom' }) - bindPanCursorEvents() + if (props.options?.activeTool !== 'pan' && props.options?.activeTool !== 'mark') { + resetChartCursor() + } - setTimeout(() => { - chart.resize() - }, 0) + if (isInitial) { + setTimeout(() => { + chart.resize() + }, 0) + } +} + +const initChart = () => { + if (!props.isInterVal && !props.pieInterVal) { + unbindPanCursorEvents() + chart?.dispose() + chart = null + currentGroup = undefined + } + + renderChart(true) } const handlerBar = (options: any) => { if (Array.isArray(options.series)) { @@ -450,9 +489,10 @@ onBeforeUnmount(() => { chart?.dispose() }) watch( - () => props.options, + () => [props.options, props.group], () => { - initChart() + // 缩放按钮只更新坐标和缩放配置,复用实例可避免大数据趋势图反复销毁重建。 + renderChart() } ) diff --git a/frontend/src/routers/modules/staticRouter.ts b/frontend/src/routers/modules/staticRouter.ts index a1dc9e5..8b5906f 100644 --- a/frontend/src/routers/modules/staticRouter.ts +++ b/frontend/src/routers/modules/staticRouter.ts @@ -67,6 +67,14 @@ export const staticRouter: RouteRecordRaw[] = [ title: '模拟数据' } }, + { + path: '/tools/addLedger', + name: 'toolAddLedger', + component: () => import('@/views/tools/addLedger/index.vue'), + meta: { + title: '数据台账' + } + }, { path: '/403', name: '403', diff --git a/frontend/src/views/tools/addLedger/components/EngineeringForm.vue b/frontend/src/views/tools/addLedger/components/EngineeringForm.vue new file mode 100644 index 0000000..f583cea --- /dev/null +++ b/frontend/src/views/tools/addLedger/components/EngineeringForm.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/frontend/src/views/tools/addLedger/components/EquipmentForm.vue b/frontend/src/views/tools/addLedger/components/EquipmentForm.vue new file mode 100644 index 0000000..b6db8ab --- /dev/null +++ b/frontend/src/views/tools/addLedger/components/EquipmentForm.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/frontend/src/views/tools/addLedger/components/LedgerTreePanel.vue b/frontend/src/views/tools/addLedger/components/LedgerTreePanel.vue new file mode 100644 index 0000000..57a170b --- /dev/null +++ b/frontend/src/views/tools/addLedger/components/LedgerTreePanel.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/frontend/src/views/tools/addLedger/components/LineForm.vue b/frontend/src/views/tools/addLedger/components/LineForm.vue new file mode 100644 index 0000000..4d75009 --- /dev/null +++ b/frontend/src/views/tools/addLedger/components/LineForm.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/frontend/src/views/tools/addLedger/components/ProjectForm.vue b/frontend/src/views/tools/addLedger/components/ProjectForm.vue new file mode 100644 index 0000000..1a6f13e --- /dev/null +++ b/frontend/src/views/tools/addLedger/components/ProjectForm.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/frontend/src/views/tools/addLedger/components/ledgerForm.scss b/frontend/src/views/tools/addLedger/components/ledgerForm.scss new file mode 100644 index 0000000..9f6f806 --- /dev/null +++ b/frontend/src/views/tools/addLedger/components/ledgerForm.scss @@ -0,0 +1,61 @@ +.ledger-form-card { + display: flex; + flex-direction: column; + gap: 16px; + min-height: 0; + padding: 16px; +} + +.form-header { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: flex-start; + justify-content: space-between; + border-bottom: 1px solid var(--el-border-color-lighter); + padding-bottom: 12px; +} + +.section-title { + font-size: 15px; + font-weight: 600; + color: var(--el-text-color-primary); +} + +.section-description { + margin-top: 6px; + font-size: 13px; + line-height: 1.6; + color: var(--el-text-color-regular); +} + +.form-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-end; +} + +.form-actions :deep(.el-button) { + margin-left: 0; +} + +.ledger-form { + flex: 1; + min-height: 0; + overflow: auto; +} + +.ledger-form :deep(.el-input-number) { + width: 100%; +} + +.ledger-form :deep(.el-textarea) { + width: 100%; +} + +@media (max-width: 900px) { + .ledger-form.form-two :deep(.el-form-item) { + width: 98%; + } +} diff --git a/frontend/src/views/tools/addLedger/index.vue b/frontend/src/views/tools/addLedger/index.vue new file mode 100644 index 0000000..a18c0de --- /dev/null +++ b/frontend/src/views/tools/addLedger/index.vue @@ -0,0 +1,718 @@ + + + + + diff --git a/frontend/src/views/tools/index.vue b/frontend/src/views/tools/index.vue index 79ad7f8..0903b17 100644 --- a/frontend/src/views/tools/index.vue +++ b/frontend/src/views/tools/index.vue @@ -23,6 +23,11 @@
addData
进入 addData 页面壳子,后续在此扩展补录数据能力和业务交互。
+ + diff --git a/frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue b/frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue index c8164a8..ff84c14 100644 --- a/frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue +++ b/frontend/src/views/tools/mmsmapping/components/MappingResultPanel.vue @@ -197,6 +197,9 @@ + + + @@ -241,8 +244,10 @@ interface SequenceConfigRow { parentDesc: string desc: string name: string + baseflag: string start: string end: string + icdcout: string startValueType: string endValueType: string } @@ -317,8 +322,8 @@ const filteredSequenceRows = computed(() => { if (!keyword) return sequenceConfigRows.value return sequenceConfigRows.value.filter(row => - [row.topKey, row.topDesc, row.desc, row.parentDesc, row.name, row.pathText, row.start, row.end].some(value => - value.toLowerCase().includes(keyword) + [row.topKey, row.topDesc, row.desc, row.parentDesc, row.name, row.baseflag, row.pathText, row.start, row.end].some( + value => value.toLowerCase().includes(keyword) ) ) }) @@ -368,12 +373,6 @@ const toDisplayText = (value: unknown, fallback: string) => { return fallback } -const isZeroValue = (value: unknown) => { - if (typeof value === 'number') return value === 0 - if (typeof value === 'string') return Number(value.trim()) === 0 - return false -} - const getPathText = (path: JsonPath) => (path.length ? path.map(item => String(item)).join(' / ') : '$') const getTopKey = (path: JsonPath) => (path.length ? String(path[0]) : '$') @@ -386,6 +385,13 @@ const resolveTopDesc = (source: unknown, path: JsonPath) => { return isRecord(topValue) ? toDisplayText(topValue.desc, '') : '' } +const isConfigurableBaseflag = (value: unknown) => { + if (typeof value === 'number') return value === 1 || value === 2 + if (typeof value !== 'string') return false + + return ['1', '2'].includes(value.trim()) +} + const collectSequenceRows = (source: unknown, path: JsonPath = [], parentDesc = '', rootSource = source): SequenceConfigRow[] => { if (Array.isArray(source)) { return source.flatMap((item, index) => collectSequenceRows(item, [...path, index], parentDesc, rootSource)) @@ -402,7 +408,7 @@ const collectSequenceRows = (source: unknown, path: JsonPath = [], parentDesc = const hasEnd = Object.prototype.hasOwnProperty.call(source, 'end') if (!hasStart || !hasEnd) return childrenRows - if (isZeroValue(source.start) && isZeroValue(source.end)) return childrenRows + if (!isConfigurableBaseflag(source.baseflag)) return childrenRows return [ { @@ -414,8 +420,10 @@ const collectSequenceRows = (source: unknown, path: JsonPath = [], parentDesc = parentDesc: parentDesc || '未配置上层描述', desc: toDisplayText(source.desc, '未命名序列'), name: toDisplayText(source.name, '未配置 name'), + baseflag: toDisplayText(source.baseflag, ''), start: source.start === undefined || source.start === null ? '' : String(source.start), end: source.end === undefined || source.end === null ? '' : String(source.end), + icdcout: toDisplayText(source.icdcout, ''), startValueType: typeof source.start, endValueType: typeof source.end }, @@ -829,7 +837,7 @@ const confirmSequenceConfig = () => { .sequence-config-item { display: grid; - grid-template-columns: minmax(0, 1fr) 300px; + grid-template-columns: minmax(0, 1fr) 420px; gap: 16px; padding: 8px 0; border: 1px solid #dbe3f0; @@ -872,7 +880,7 @@ const confirmSequenceConfig = () => { .sequence-config-form { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }