2026-02-12 21:19:23 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="ems-dashboard-editor-container" style="background-color:#ffffff">
|
|
|
|
|
|
<div class="header-row">
|
|
|
|
|
|
<div class="title-block">
|
|
|
|
|
|
<div class="page-title">单站监控项目点位配置</div>
|
2026-02-13 21:46:12 +08:00
|
|
|
|
<div class="page-desc">
|
|
|
|
|
|
<span class="site-label">站点:</span>
|
|
|
|
|
|
<span>{{ siteName || siteId || '-' }}</span>
|
|
|
|
|
|
</div>
|
2026-02-12 21:19:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
title="当前字段清单与点位映射均从后端表读取,保存后可直接用于单站监控查询。"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
style="margin-top: 16px;"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-02-15 16:24:29 +08:00
|
|
|
|
<el-tabs v-model="activeConfigTab" style="margin-top: 16px;">
|
|
|
|
|
|
<el-tab-pane label="点位配置" name="point">
|
|
|
|
|
|
<div class="mode-switch-row">
|
|
|
|
|
|
<el-radio-group v-model="configMode" size="small">
|
|
|
|
|
|
<el-radio-button label="table">列表配置</el-radio-button>
|
|
|
|
|
|
<el-radio-button label="quick">快速配置</el-radio-button>
|
|
|
|
|
|
<el-radio-button label="section">分组填报</el-radio-button>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
<div class="save-status-text">{{ saveStatusText }}</div>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="quickFilter"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
class="quick-filter-input"
|
|
|
|
|
|
placeholder="按字段名/展示名/设备名筛选"
|
|
|
|
|
|
/>
|
2026-04-12 15:18:00 +08:00
|
|
|
|
<div class="filter-actions" :class="{ 'single-battery-actions-visible': showSingleBatteryImportActions }">
|
2026-04-01 14:27:35 +08:00
|
|
|
|
<el-button size="small" :disabled="!siteId" @click="openImportConfigDialog">导入配置</el-button>
|
|
|
|
|
|
<el-button size="small" type="primary" :disabled="!siteId" @click="exportConfig">导出配置</el-button>
|
2026-04-15 22:44:21 +08:00
|
|
|
|
<el-button v-if="showSingleBatteryImportActions" size="small" type="success" :disabled="!siteId || singleBatteryInitLoading" @click="openSingleBatteryInitDialog">
|
|
|
|
|
|
{{ singleBatteryInitLoading ? '初始化中...' : '初始化单体电池配置' }}
|
2026-04-12 15:18:00 +08:00
|
|
|
|
</el-button>
|
2026-04-01 14:27:35 +08:00
|
|
|
|
<input
|
|
|
|
|
|
ref="configInput"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept=".csv"
|
|
|
|
|
|
style="display: none"
|
|
|
|
|
|
@change="handleConfigFileChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-02-15 16:24:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tabs v-model="activeTopMenu" style="margin-top: 16px;">
|
2026-02-12 21:19:23 +08:00
|
|
|
|
<el-tab-pane
|
|
|
|
|
|
v-for="topMenu in topMenuList"
|
|
|
|
|
|
:key="topMenu.code"
|
|
|
|
|
|
:label="topMenu.name"
|
|
|
|
|
|
:name="topMenu.code"
|
|
|
|
|
|
>
|
2026-02-15 16:24:29 +08:00
|
|
|
|
<template v-if="configMode === 'section'">
|
|
|
|
|
|
<div v-if="!topMenu.children || topMenu.children.length === 0" class="section-wrapper">
|
|
|
|
|
|
<el-collapse
|
|
|
|
|
|
accordion
|
|
|
|
|
|
class="section-collapse"
|
|
|
|
|
|
:value="getSectionActiveValue(buildSectionPanelKey(topMenu.code, 'ROOT'), getFilteredSectionBlocks(topMenu.items))"
|
|
|
|
|
|
@input="setSectionActiveValue(buildSectionPanelKey(topMenu.code, 'ROOT'), $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-collapse-item
|
|
|
|
|
|
v-for="sectionBlock in getFilteredSectionBlocks(topMenu.items)"
|
|
|
|
|
|
:key="sectionBlock.key"
|
|
|
|
|
|
:name="sectionBlock.key"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template slot="title">
|
|
|
|
|
|
<div class="section-title-row">
|
|
|
|
|
|
<span>{{ sectionBlock.section }}</span>
|
|
|
|
|
|
<span class="section-badge">{{ sectionBlock.fields.length }} 个字段</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="section-field-list">
|
|
|
|
|
|
<div v-for="group in sectionBlock.fields" :key="group.key" class="section-field-card">
|
|
|
|
|
|
<div class="quick-card-header">
|
|
|
|
|
|
<div class="quick-title-block">
|
|
|
|
|
|
<div class="quick-title">{{ group.name }}</div>
|
|
|
|
|
|
<div class="quick-sub-title">{{ group.field }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-button type="text" size="mini" @click="clearGroupMapping(group)">清空</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-for="row in group.rows" :key="row.code" class="quick-row">
|
|
|
|
|
|
<div class="quick-device-name">{{ row.deviceName || '默认映射' }}</div>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="row.point"
|
|
|
|
|
|
:readonly="!!row.deviceId"
|
|
|
|
|
|
:placeholder="row.deviceId ? '点击选择点位' : '请输入点位'"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
class="quick-input"
|
|
|
|
|
|
@focus="handlePointInputFocus(row)"
|
|
|
|
|
|
@mousedown.native.prevent="handlePointInputFocus(row)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-input v-model.trim="row.fixedValue" placeholder="固定展示值" clearable class="quick-input" />
|
|
|
|
|
|
<el-checkbox v-model="row.useFixedDisplay" :true-label="1" :false-label="0">固定展示</el-checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
|
</el-collapse>
|
|
|
|
|
|
<el-empty v-if="getFilteredSectionBlocks(topMenu.items).length === 0" description="暂无可配置项" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tabs
|
|
|
|
|
|
v-else
|
|
|
|
|
|
v-model="activeChildMap[topMenu.code]"
|
|
|
|
|
|
class="child-menu-tabs"
|
|
|
|
|
|
type="border-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-tab-pane
|
|
|
|
|
|
v-for="child in topMenu.children"
|
|
|
|
|
|
:key="child.code"
|
|
|
|
|
|
:label="child.name"
|
|
|
|
|
|
:name="child.code"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="section-wrapper">
|
|
|
|
|
|
<el-collapse
|
|
|
|
|
|
accordion
|
|
|
|
|
|
class="section-collapse"
|
|
|
|
|
|
:value="getSectionActiveValue(buildSectionPanelKey(topMenu.code, child.code), getFilteredSectionBlocks(child.items))"
|
|
|
|
|
|
@input="setSectionActiveValue(buildSectionPanelKey(topMenu.code, child.code), $event)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-collapse-item
|
|
|
|
|
|
v-for="sectionBlock in getFilteredSectionBlocks(child.items)"
|
|
|
|
|
|
:key="sectionBlock.key"
|
|
|
|
|
|
:name="sectionBlock.key"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template slot="title">
|
|
|
|
|
|
<div class="section-title-row">
|
|
|
|
|
|
<span>{{ sectionBlock.section }}</span>
|
|
|
|
|
|
<span class="section-badge">{{ sectionBlock.fields.length }} 个字段</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div class="section-field-list">
|
|
|
|
|
|
<div v-for="group in sectionBlock.fields" :key="group.key" class="section-field-card">
|
|
|
|
|
|
<div class="quick-card-header">
|
|
|
|
|
|
<div class="quick-title-block">
|
|
|
|
|
|
<div class="quick-title">{{ group.name }}</div>
|
|
|
|
|
|
<div class="quick-sub-title">{{ group.field }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-button type="text" size="mini" @click="clearGroupMapping(group)">清空</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-for="row in group.rows" :key="row.code" class="quick-row">
|
|
|
|
|
|
<div class="quick-device-name">{{ row.deviceName || '默认映射' }}</div>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="row.point"
|
|
|
|
|
|
:readonly="!!row.deviceId"
|
|
|
|
|
|
:placeholder="row.deviceId ? '点击选择点位' : '请输入点位'"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
class="quick-input"
|
|
|
|
|
|
@focus="handlePointInputFocus(row)"
|
|
|
|
|
|
@mousedown.native.prevent="handlePointInputFocus(row)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-input v-model.trim="row.fixedValue" placeholder="固定展示值" clearable class="quick-input" />
|
|
|
|
|
|
<el-checkbox v-model="row.useFixedDisplay" :true-label="1" :false-label="0">固定展示</el-checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
|
</el-collapse>
|
|
|
|
|
|
<el-empty v-if="getFilteredSectionBlocks(child.items).length === 0" description="暂无可配置项" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template v-else-if="configMode === 'quick'">
|
|
|
|
|
|
<div v-if="!topMenu.children || topMenu.children.length === 0" class="quick-wrapper">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="group in getFilteredQuickGroups(topMenu.items)"
|
|
|
|
|
|
:key="group.key"
|
|
|
|
|
|
class="quick-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="quick-card-header">
|
|
|
|
|
|
<div class="quick-title-block">
|
|
|
|
|
|
<div class="quick-title">{{ group.name }}</div>
|
|
|
|
|
|
<div class="quick-sub-title">{{ group.section }} | {{ group.field }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-button type="text" size="mini" @click="clearGroupMapping(group)">清空</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-for="row in group.rows" :key="row.code" class="quick-row">
|
|
|
|
|
|
<div class="quick-device-name">{{ row.deviceName || '默认映射' }}</div>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="row.point"
|
|
|
|
|
|
:readonly="!!row.deviceId"
|
|
|
|
|
|
:placeholder="row.deviceId ? '点击选择点位' : '请输入点位'"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
class="quick-input"
|
|
|
|
|
|
@focus="handlePointInputFocus(row)"
|
|
|
|
|
|
@mousedown.native.prevent="handlePointInputFocus(row)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-input v-model.trim="row.fixedValue" placeholder="固定展示值" clearable class="quick-input" />
|
|
|
|
|
|
<el-checkbox v-model="row.useFixedDisplay" :true-label="1" :false-label="0">固定展示</el-checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-empty v-if="getFilteredQuickGroups(topMenu.items).length === 0" description="暂无可配置项" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tabs
|
|
|
|
|
|
v-else
|
|
|
|
|
|
v-model="activeChildMap[topMenu.code]"
|
|
|
|
|
|
class="child-menu-tabs"
|
|
|
|
|
|
type="border-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-tab-pane
|
|
|
|
|
|
v-for="child in topMenu.children"
|
|
|
|
|
|
:key="child.code"
|
|
|
|
|
|
:label="child.name"
|
|
|
|
|
|
:name="child.code"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="quick-wrapper">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="group in getFilteredQuickGroups(child.items)"
|
|
|
|
|
|
:key="group.key"
|
|
|
|
|
|
class="quick-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="quick-card-header">
|
|
|
|
|
|
<div class="quick-title-block">
|
|
|
|
|
|
<div class="quick-title">{{ group.name }}</div>
|
|
|
|
|
|
<div class="quick-sub-title">{{ group.section }} | {{ group.field }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-button type="text" size="mini" @click="clearGroupMapping(group)">清空</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-for="row in group.rows" :key="row.code" class="quick-row">
|
|
|
|
|
|
<div class="quick-device-name">{{ row.deviceName || '默认映射' }}</div>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="row.point"
|
|
|
|
|
|
:readonly="!!row.deviceId"
|
|
|
|
|
|
:placeholder="row.deviceId ? '点击选择点位' : '请输入点位'"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
class="quick-input"
|
|
|
|
|
|
@focus="handlePointInputFocus(row)"
|
|
|
|
|
|
@mousedown.native.prevent="handlePointInputFocus(row)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-input v-model.trim="row.fixedValue" placeholder="固定展示值" clearable class="quick-input" />
|
|
|
|
|
|
<el-checkbox v-model="row.useFixedDisplay" :true-label="1" :false-label="0">固定展示</el-checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-empty v-if="getFilteredQuickGroups(child.items).length === 0" description="暂无可配置项" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-02-12 21:19:23 +08:00
|
|
|
|
<el-table
|
2026-02-15 16:24:29 +08:00
|
|
|
|
v-else-if="!topMenu.children || topMenu.children.length === 0"
|
2026-02-12 21:19:23 +08:00
|
|
|
|
class="common-table"
|
|
|
|
|
|
:data="topMenu.items"
|
|
|
|
|
|
stripe
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="section" label="分组" min-width="180" />
|
|
|
|
|
|
<el-table-column prop="name" label="页面展示名称" min-width="280" />
|
|
|
|
|
|
<el-table-column prop="field" label="字段名" min-width="260" />
|
2026-02-15 16:24:29 +08:00
|
|
|
|
<el-table-column prop="deviceName" label="设备" min-width="220">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span>{{ scope.row.deviceName || '-' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-02-13 21:46:12 +08:00
|
|
|
|
<el-table-column label="点位" min-width="300">
|
2026-02-12 21:19:23 +08:00
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="scope.row.point"
|
2026-02-15 16:24:29 +08:00
|
|
|
|
:readonly="!!scope.row.deviceId"
|
|
|
|
|
|
:placeholder="scope.row.deviceId ? '点击选择点位' : '请输入点位'"
|
2026-02-12 21:19:23 +08:00
|
|
|
|
clearable
|
2026-02-13 21:46:12 +08:00
|
|
|
|
class="short-input"
|
2026-02-15 16:24:29 +08:00
|
|
|
|
@focus="handlePointInputFocus(scope.row)"
|
|
|
|
|
|
@mousedown.native.prevent="handlePointInputFocus(scope.row)"
|
2026-02-12 21:19:23 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-02-13 21:46:12 +08:00
|
|
|
|
<el-table-column label="固定展示值" min-width="300">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="scope.row.fixedValue"
|
|
|
|
|
|
placeholder="请输入固定展示值"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
class="short-input"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="固定展示" width="120" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-checkbox v-model="scope.row.useFixedDisplay" :true-label="1" :false-label="0" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="120" fixed="right">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
2026-02-15 16:24:29 +08:00
|
|
|
|
:disabled="!!scope.row.deviceId"
|
2026-02-13 21:46:12 +08:00
|
|
|
|
@click="handleDeleteItem(scope.$index, topMenu.items)"
|
|
|
|
|
|
>
|
|
|
|
|
|
删除
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-02-12 21:19:23 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tabs
|
|
|
|
|
|
v-else
|
|
|
|
|
|
v-model="activeChildMap[topMenu.code]"
|
|
|
|
|
|
class="child-menu-tabs"
|
|
|
|
|
|
type="border-card"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-tab-pane
|
|
|
|
|
|
v-for="child in topMenu.children"
|
|
|
|
|
|
:key="child.code"
|
|
|
|
|
|
:label="child.name"
|
|
|
|
|
|
:name="child.code"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table class="common-table" :data="child.items" stripe>
|
|
|
|
|
|
<el-table-column prop="section" label="分组" min-width="180" />
|
|
|
|
|
|
<el-table-column prop="name" label="页面展示名称" min-width="280" />
|
|
|
|
|
|
<el-table-column prop="field" label="字段名" min-width="260" />
|
2026-02-15 16:24:29 +08:00
|
|
|
|
<el-table-column prop="deviceName" label="设备" min-width="220">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span>{{ scope.row.deviceName || '-' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-02-13 21:46:12 +08:00
|
|
|
|
<el-table-column label="点位" min-width="300">
|
2026-02-12 21:19:23 +08:00
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="scope.row.point"
|
2026-02-15 16:24:29 +08:00
|
|
|
|
:readonly="!!scope.row.deviceId"
|
|
|
|
|
|
:placeholder="scope.row.deviceId ? '点击选择点位' : '请输入点位'"
|
2026-02-12 21:19:23 +08:00
|
|
|
|
clearable
|
2026-02-13 21:46:12 +08:00
|
|
|
|
class="short-input"
|
2026-02-15 16:24:29 +08:00
|
|
|
|
@focus="handlePointInputFocus(scope.row)"
|
|
|
|
|
|
@mousedown.native.prevent="handlePointInputFocus(scope.row)"
|
2026-02-13 21:46:12 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="固定展示值" min-width="300">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model.trim="scope.row.fixedValue"
|
|
|
|
|
|
placeholder="请输入固定展示值"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
class="short-input"
|
2026-02-12 21:19:23 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-02-13 21:46:12 +08:00
|
|
|
|
<el-table-column label="固定展示" width="120" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-checkbox v-model="scope.row.useFixedDisplay" :true-label="1" :false-label="0" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="120" fixed="right">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
2026-02-15 16:24:29 +08:00
|
|
|
|
:disabled="!!scope.row.deviceId"
|
2026-02-13 21:46:12 +08:00
|
|
|
|
@click="handleDeleteItem(scope.$index, child.items)"
|
|
|
|
|
|
>
|
|
|
|
|
|
删除
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-02-12 21:19:23 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</el-tab-pane>
|
2026-02-15 16:24:29 +08:00
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tab-pane label="状态枚举映射" name="workStatusEnum">
|
|
|
|
|
|
<el-card class="enum-mapping-card" shadow="never">
|
|
|
|
|
|
<div slot="header" class="enum-mapping-header">
|
|
|
|
|
|
<span>状态枚举映射</span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<el-button type="text" size="mini" @click="resetWorkStatusEnumMappings">恢复默认</el-button>
|
|
|
|
|
|
<el-button type="text" size="mini" @click="addWorkStatusEnumMapping">新增映射</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-radio-group v-model="activeEnumScope" size="mini" class="enum-scope-group">
|
|
|
|
|
|
<el-radio-button v-for="scope in enumScopeOptions" :key="scope.key" :label="scope.key">{{ scope.label }}</el-radio-button>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
<el-table class="common-table" :data="getEnumMappingsByScope(activeEnumScope)" size="mini" stripe>
|
|
|
|
|
|
<el-table-column label="系统状态编码" width="130">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-input v-model.trim="scope.row.enumCode" placeholder="如 3" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="系统状态名称" width="160">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-input v-model.trim="scope.row.enumName" placeholder="如 待机" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="站点上送值" width="180">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-input v-model.trim="scope.row.dataEnumCode" placeholder="如 1 / 3 / 4" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="描述" min-width="220">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-input v-model.trim="scope.row.enumDesc" placeholder="可选描述" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="90" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button type="text" size="mini" @click="removeWorkStatusEnumMapping(scope.row)">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="enum-mapping-tip">说明:按上方状态项分别维护“站点上送值 -> 系统状态编码/名称”的映射关系。</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-tab-pane>
|
2026-02-12 21:19:23 +08:00
|
|
|
|
</el-tabs>
|
2026-02-15 16:24:29 +08:00
|
|
|
|
<el-dialog
|
|
|
|
|
|
title="选择点位"
|
|
|
|
|
|
:visible.sync="pointSelectorVisible"
|
|
|
|
|
|
width="900px"
|
|
|
|
|
|
:close-on-click-modal="false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form :inline="true" class="select-container">
|
|
|
|
|
|
<el-form-item label="点位ID">
|
2026-04-01 14:27:35 +08:00
|
|
|
|
<el-input v-model.trim="pointSelectorQuery.pointId" clearable placeholder="请输入点位ID(支持模糊)" style="width: 180px" />
|
2026-02-15 16:24:29 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="点位描述">
|
|
|
|
|
|
<el-input v-model.trim="pointSelectorQuery.pointDesc" clearable placeholder="请输入点位描述" style="width: 180px" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" @click="searchPointSelector">搜索</el-button>
|
|
|
|
|
|
<el-button @click="resetPointSelector">重置</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
v-loading="pointSelectorLoading"
|
|
|
|
|
|
:data="pointSelectorList"
|
|
|
|
|
|
class="common-table"
|
|
|
|
|
|
stripe
|
|
|
|
|
|
max-height="460px"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="pointId" label="点位ID" min-width="260" />
|
|
|
|
|
|
<el-table-column prop="pointName" label="点位名称" min-width="160" />
|
|
|
|
|
|
<el-table-column prop="pointDesc" label="点位描述" min-width="220" />
|
|
|
|
|
|
<el-table-column prop="deviceCategory" label="设备类型" width="120" />
|
|
|
|
|
|
<el-table-column prop="deviceId" label="设备ID" min-width="140" />
|
|
|
|
|
|
<el-table-column label="操作" width="90" align="center" fixed="right">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button type="text" size="mini" @click="selectPointFromDialog(scope.row)">选择</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-show="pointSelectorTotal > 0"
|
|
|
|
|
|
background
|
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
|
:current-page="pointSelectorQuery.pageNum"
|
|
|
|
|
|
:page-size="pointSelectorQuery.pageSize"
|
|
|
|
|
|
:total="pointSelectorTotal"
|
|
|
|
|
|
style="margin-top: 12px; text-align: center"
|
|
|
|
|
|
@current-change="handlePointSelectorPageChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-dialog>
|
2026-04-12 15:18:00 +08:00
|
|
|
|
<el-dialog
|
2026-04-15 22:44:21 +08:00
|
|
|
|
title="初始化单体电池配置"
|
|
|
|
|
|
:visible.sync="singleBatteryInitDialogVisible"
|
|
|
|
|
|
width="560px"
|
|
|
|
|
|
append-to-body
|
|
|
|
|
|
class="ems-dialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form label-width="110px" size="small">
|
|
|
|
|
|
<el-form-item label="电池堆">
|
|
|
|
|
|
<el-select v-model="singleBatteryInitForm.stackDeviceId" placeholder="请选择电池堆" style="width: 100%" @change="handleSingleBatteryStackChange">
|
|
|
|
|
|
<el-option v-for="item in singleBatteryStackOptions" :key="item.id || item.deviceId || item" :label="item.name || item.deviceName || item.id || item.deviceId || item" :value="item.id || item.deviceId || item" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="电池簇">
|
|
|
|
|
|
<el-select v-model="singleBatteryInitForm.clusterDeviceId" placeholder="请选择电池簇" style="width: 100%">
|
|
|
|
|
|
<el-option v-for="item in singleBatteryClusterOptions" :key="item.id || item.deviceId || item" :label="item.name || item.deviceName || item.id || item.deviceId || item" :value="item.id || item.deviceId || item" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="目标数量">
|
|
|
|
|
|
<el-input-number v-model="singleBatteryInitForm.targetCount" :min="1" :precision="0" controls-position="right" style="width: 220px" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<div slot="footer" class="dialog-footer">
|
|
|
|
|
|
<el-button @click="singleBatteryInitDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="singleBatteryInitLoading" @click="submitSingleBatteryInit">确定初始化</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
<el-dialog
|
|
|
|
|
|
title="初始化结果"
|
|
|
|
|
|
:visible.sync="singleBatteryInitResultVisible"
|
2026-04-12 15:18:00 +08:00
|
|
|
|
width="980px"
|
|
|
|
|
|
append-to-body
|
|
|
|
|
|
class="ems-dialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-alert
|
2026-04-15 22:44:21 +08:00
|
|
|
|
:title="singleBatteryInitResult.message || '初始化完成'"
|
|
|
|
|
|
:type="singleBatteryInitResult.committed ? 'success' : 'warning'"
|
2026-04-12 15:18:00 +08:00
|
|
|
|
:closable="false"
|
|
|
|
|
|
style="margin-bottom: 16px;"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-descriptions :column="3" border size="small">
|
2026-04-15 22:44:21 +08:00
|
|
|
|
<el-descriptions-item label="站点ID">{{ singleBatteryInitResult.siteId || '-' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="初始化范围">{{ singleBatteryInitResult.scopeType === 'stack' ? '电池堆' : '电池簇' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="范围设备ID">{{ singleBatteryInitResult.scopeDeviceId || '-' }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="目标数量">{{ singleBatteryInitResult.targetCount || 0 }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="现有单体">{{ singleBatteryInitResult.existingBatteryCount || 0 }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="已初始化单体">{{ singleBatteryInitResult.initializedBatteryCount || 0 }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="新增单体">{{ singleBatteryInitResult.insertedBatteryCount || 0 }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="命中点位">{{ singleBatteryInitResult.pointHitCount || 0 }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="固定值回填">{{ singleBatteryInitResult.fixedValueFallbackCount || 0 }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="新增映射">{{ singleBatteryInitResult.insertedMappingCount || 0 }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="更新映射">{{ singleBatteryInitResult.updatedMappingCount || 0 }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="是否成功">{{ singleBatteryInitResult.committed ? '是' : '否' }}</el-descriptions-item>
|
2026-04-12 15:18:00 +08:00
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
</el-dialog>
|
2026-02-12 21:19:23 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-02-15 16:24:29 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getPointMatchList,
|
|
|
|
|
|
getSingleMonitorProjectPointMapping,
|
|
|
|
|
|
getSingleMonitorWorkStatusEnumMappings,
|
2026-04-15 22:44:21 +08:00
|
|
|
|
initializeSingleBatteryMonitorMappings,
|
2026-02-15 16:24:29 +08:00
|
|
|
|
saveSingleMonitorProjectPointMapping,
|
|
|
|
|
|
saveSingleMonitorWorkStatusEnumMappings
|
|
|
|
|
|
} from '@/api/ems/site'
|
2026-04-15 22:44:21 +08:00
|
|
|
|
import { getClusterNameList, getStackNameList } from '@/api/ems/dzjk'
|
2026-02-12 21:19:23 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'MonitorPointMapping',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2026-02-13 21:46:12 +08:00
|
|
|
|
siteId: '',
|
|
|
|
|
|
siteName: '',
|
2026-02-15 16:24:29 +08:00
|
|
|
|
activeConfigTab: 'point',
|
|
|
|
|
|
activeEnumScope: 'PCS|workStatus',
|
|
|
|
|
|
enumScopeOptions: [
|
|
|
|
|
|
{ key: 'PCS|workStatus', label: 'PCS-工作状态' },
|
|
|
|
|
|
{ key: 'PCS|gridStatus', label: 'PCS-并网状态' },
|
|
|
|
|
|
{ key: 'PCS|deviceStatus', label: 'PCS-设备状态' },
|
|
|
|
|
|
{ key: 'PCS|controlMode', label: 'PCS-控制模式' },
|
|
|
|
|
|
{ key: 'STACK|workStatus', label: 'BMS总览-工作状态' },
|
|
|
|
|
|
{ key: 'STACK|pcsCommunicationStatus', label: 'BMS总览-与PCS通信' },
|
|
|
|
|
|
{ key: 'STACK|emsCommunicationStatus', label: 'BMS总览-与EMS通信' },
|
|
|
|
|
|
{ key: 'CLUSTER|workStatus', label: 'BMS电池簇-工作状态' },
|
|
|
|
|
|
{ key: 'CLUSTER|pcsCommunicationStatus', label: 'BMS电池簇-与PCS通信' },
|
|
|
|
|
|
{ key: 'CLUSTER|emsCommunicationStatus', label: 'BMS电池簇-与EMS通信' }
|
|
|
|
|
|
],
|
|
|
|
|
|
configMode: 'table',
|
|
|
|
|
|
quickFilter: '',
|
2026-02-12 21:19:23 +08:00
|
|
|
|
activeTopMenu: 'HOME',
|
|
|
|
|
|
activeChildMap: {},
|
2026-02-15 16:24:29 +08:00
|
|
|
|
sectionActiveMap: {},
|
2026-02-13 21:46:12 +08:00
|
|
|
|
topMenuList: [],
|
2026-02-15 16:24:29 +08:00
|
|
|
|
deletedFieldCodes: [],
|
|
|
|
|
|
currentPointRow: null,
|
|
|
|
|
|
pointSelectorVisible: false,
|
|
|
|
|
|
pointSelectorLoading: false,
|
|
|
|
|
|
pointSelectorTotal: 0,
|
|
|
|
|
|
pointSelectorList: [],
|
|
|
|
|
|
pointSelectorQuery: {
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
siteId: '',
|
|
|
|
|
|
deviceCategory: '',
|
|
|
|
|
|
deviceId: '',
|
|
|
|
|
|
pointId: '',
|
|
|
|
|
|
pointDesc: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
autoSaveTimer: null,
|
|
|
|
|
|
autoSaveDelay: 900,
|
2026-04-01 14:27:35 +08:00
|
|
|
|
pointSelectorQueryTimer: null,
|
|
|
|
|
|
pointSelectorQueryDelay: 280,
|
2026-02-15 16:24:29 +08:00
|
|
|
|
suppressAutoSave: false,
|
|
|
|
|
|
isSaving: false,
|
|
|
|
|
|
saveStatusText: '自动保存已开启',
|
2026-04-15 22:44:21 +08:00
|
|
|
|
singleBatteryInitLoading: false,
|
|
|
|
|
|
singleBatteryInitDialogVisible: false,
|
|
|
|
|
|
singleBatteryInitResultVisible: false,
|
|
|
|
|
|
singleBatteryStackOptions: [],
|
|
|
|
|
|
singleBatteryClusterOptions: [],
|
|
|
|
|
|
singleBatteryInitForm: {
|
|
|
|
|
|
stackDeviceId: '',
|
|
|
|
|
|
clusterDeviceId: '',
|
|
|
|
|
|
targetCount: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
singleBatteryInitResult: {
|
2026-04-12 15:18:00 +08:00
|
|
|
|
committed: false,
|
2026-04-15 22:44:21 +08:00
|
|
|
|
siteId: '',
|
|
|
|
|
|
scopeType: 'stack',
|
|
|
|
|
|
scopeDeviceId: '',
|
|
|
|
|
|
targetCount: 0,
|
|
|
|
|
|
existingBatteryCount: 0,
|
|
|
|
|
|
initializedBatteryCount: 0,
|
2026-04-12 15:18:00 +08:00
|
|
|
|
insertedBatteryCount: 0,
|
2026-04-15 22:44:21 +08:00
|
|
|
|
pointHitCount: 0,
|
|
|
|
|
|
fixedValueFallbackCount: 0,
|
2026-04-12 15:18:00 +08:00
|
|
|
|
insertedMappingCount: 0,
|
|
|
|
|
|
updatedMappingCount: 0,
|
2026-04-15 22:44:21 +08:00
|
|
|
|
message: ''
|
2026-04-12 15:18:00 +08:00
|
|
|
|
},
|
2026-02-15 16:24:29 +08:00
|
|
|
|
lastSavedPointSignature: '',
|
|
|
|
|
|
lastSavedEnumSignature: '',
|
|
|
|
|
|
workStatusEnumMappings: []
|
2026-02-12 21:19:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-13 21:46:12 +08:00
|
|
|
|
watch: {
|
|
|
|
|
|
'$route.query.siteId': {
|
|
|
|
|
|
immediate: false,
|
|
|
|
|
|
async handler(newSiteId) {
|
|
|
|
|
|
if (newSiteId === this.siteId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.siteId = newSiteId || ''
|
|
|
|
|
|
this.siteName = this.$route.query.siteName || this.siteId
|
|
|
|
|
|
await this.initMenuStructure()
|
|
|
|
|
|
}
|
2026-02-15 16:24:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
topMenuList: {
|
|
|
|
|
|
deep: true,
|
|
|
|
|
|
handler() {
|
|
|
|
|
|
this.scheduleAutoSave()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
deletedFieldCodes: {
|
|
|
|
|
|
deep: true,
|
|
|
|
|
|
handler() {
|
|
|
|
|
|
this.scheduleAutoSave()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
workStatusEnumMappings: {
|
|
|
|
|
|
deep: true,
|
|
|
|
|
|
handler() {
|
|
|
|
|
|
this.scheduleAutoSave()
|
|
|
|
|
|
}
|
2026-04-01 14:27:35 +08:00
|
|
|
|
},
|
|
|
|
|
|
'pointSelectorQuery.pointId'() {
|
|
|
|
|
|
this.schedulePointSelectorSearch()
|
|
|
|
|
|
},
|
|
|
|
|
|
'pointSelectorQuery.pointDesc'() {
|
|
|
|
|
|
this.schedulePointSelectorSearch()
|
2026-02-12 21:19:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-12 15:18:00 +08:00
|
|
|
|
computed: {
|
|
|
|
|
|
showSingleBatteryImportActions() {
|
|
|
|
|
|
const currentTopMenu = this.topMenuList.find(item => item.code === this.activeTopMenu)
|
|
|
|
|
|
if (!currentTopMenu) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentTopMenu.code === 'SBJK_DTDC') {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
const activeChildCode = this.activeChildMap[this.activeTopMenu] || ''
|
|
|
|
|
|
return activeChildCode === 'SBJK_DTDC'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-12 21:19:23 +08:00
|
|
|
|
methods: {
|
|
|
|
|
|
async initMenuStructure() {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
this.topMenuList = []
|
2026-02-13 21:46:12 +08:00
|
|
|
|
this.deletedFieldCodes = []
|
2026-02-15 16:24:29 +08:00
|
|
|
|
this.workStatusEnumMappings = this.buildDefaultWorkStatusEnumMappings()
|
|
|
|
|
|
this.activeEnumScope = this.enumScopeOptions[0]?.key || 'PCS|workStatus'
|
2026-02-12 21:19:23 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-15 16:24:29 +08:00
|
|
|
|
this.suppressAutoSave = true
|
|
|
|
|
|
const [mappingResp, enumResp] = await Promise.all([
|
|
|
|
|
|
getSingleMonitorProjectPointMapping(this.siteId),
|
|
|
|
|
|
getSingleMonitorWorkStatusEnumMappings(this.siteId)
|
|
|
|
|
|
])
|
|
|
|
|
|
const list = mappingResp?.data || []
|
|
|
|
|
|
const enumList = Array.isArray(enumResp?.data) ? enumResp.data : []
|
2026-02-12 21:19:23 +08:00
|
|
|
|
const topMap = new Map()
|
|
|
|
|
|
const childMap = new Map()
|
|
|
|
|
|
|
|
|
|
|
|
list.forEach((row, index) => {
|
|
|
|
|
|
const moduleCode = row.moduleCode || 'UNKNOWN'
|
|
|
|
|
|
const moduleName = row.moduleName || '未分组'
|
|
|
|
|
|
const menuCode = row.menuCode || moduleCode
|
|
|
|
|
|
const menuName = row.menuName || moduleName
|
|
|
|
|
|
const topKey = moduleCode
|
|
|
|
|
|
if (!topMap.has(topKey)) {
|
|
|
|
|
|
topMap.set(topKey, {
|
|
|
|
|
|
code: moduleCode,
|
|
|
|
|
|
name: moduleName,
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
items: []
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
const item = {
|
2026-02-15 16:24:29 +08:00
|
|
|
|
code: `${moduleCode}_${menuCode}_${row.fieldCode || index}_${row.deviceId || 'NA'}`,
|
2026-02-12 21:19:23 +08:00
|
|
|
|
section: row.sectionName || '-',
|
|
|
|
|
|
name: row.fieldName || '-',
|
|
|
|
|
|
field: row.fieldCode || '',
|
2026-02-15 16:24:29 +08:00
|
|
|
|
deviceId: row.deviceId || '',
|
|
|
|
|
|
deviceName: row.deviceName || '',
|
2026-02-13 21:46:12 +08:00
|
|
|
|
point: row.dataPoint || '',
|
|
|
|
|
|
fixedValue: row.fixedDataPoint || '',
|
|
|
|
|
|
useFixedDisplay: row.useFixedDisplay === 1 ? 1 : 0
|
2026-02-12 21:19:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
const topItem = topMap.get(topKey)
|
|
|
|
|
|
const isTopDirect = moduleCode === menuCode || moduleCode === 'HOME'
|
|
|
|
|
|
if (isTopDirect) {
|
|
|
|
|
|
topItem.items.push(item)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const childKey = `${moduleCode}_${menuCode}`
|
|
|
|
|
|
if (!childMap.has(childKey)) {
|
|
|
|
|
|
const child = { code: menuCode, name: menuName, items: [] }
|
|
|
|
|
|
childMap.set(childKey, child)
|
|
|
|
|
|
topItem.children.push(child)
|
|
|
|
|
|
}
|
|
|
|
|
|
childMap.get(childKey).items.push(item)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
this.topMenuList = Array.from(topMap.values())
|
|
|
|
|
|
const firstTop = this.topMenuList[0]
|
|
|
|
|
|
this.activeTopMenu = firstTop ? firstTop.code : 'HOME'
|
|
|
|
|
|
this.activeChildMap = {}
|
2026-02-15 16:24:29 +08:00
|
|
|
|
this.sectionActiveMap = {}
|
2026-02-13 21:46:12 +08:00
|
|
|
|
this.deletedFieldCodes = []
|
2026-02-15 16:24:29 +08:00
|
|
|
|
this.workStatusEnumMappings = this.normalizeWorkStatusEnumMappings(enumList)
|
|
|
|
|
|
if (!this.enumScopeOptions.find(item => item.key === this.activeEnumScope)) {
|
|
|
|
|
|
this.activeEnumScope = this.enumScopeOptions[0]?.key || 'PCS|workStatus'
|
|
|
|
|
|
}
|
2026-02-12 21:19:23 +08:00
|
|
|
|
this.topMenuList.forEach(top => {
|
|
|
|
|
|
if (top.children && top.children.length > 0) {
|
|
|
|
|
|
this.$set(this.activeChildMap, top.code, top.children[0].code)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-02-15 16:24:29 +08:00
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.lastSavedPointSignature = this.buildPointPayloadSignature()
|
|
|
|
|
|
this.lastSavedEnumSignature = this.buildEnumPayloadSignature()
|
|
|
|
|
|
this.suppressAutoSave = false
|
|
|
|
|
|
this.saveStatusText = '自动保存已开启'
|
|
|
|
|
|
})
|
2026-02-12 21:19:23 +08:00
|
|
|
|
},
|
|
|
|
|
|
getAllMappingRows() {
|
|
|
|
|
|
const rows = []
|
|
|
|
|
|
this.topMenuList.forEach(top => {
|
|
|
|
|
|
;(top.items || []).forEach(item => rows.push(item))
|
|
|
|
|
|
;(top.children || []).forEach(child => {
|
|
|
|
|
|
;(child.items || []).forEach(item => rows.push(item))
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
return rows
|
|
|
|
|
|
},
|
2026-02-15 16:24:29 +08:00
|
|
|
|
getDeviceCategoryByField(fieldCode) {
|
|
|
|
|
|
if (!fieldCode || fieldCode.indexOf('__') === -1) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const menuCode = fieldCode.split('__')[0]
|
|
|
|
|
|
const menuMap = {
|
|
|
|
|
|
SBJK_SSYX: 'PCS',
|
|
|
|
|
|
SBJK_EMS: 'EMS',
|
|
|
|
|
|
SBJK_PCS: 'PCS',
|
|
|
|
|
|
SBJK_BMSZL: 'STACK',
|
|
|
|
|
|
SBJK_BMSDCC: 'CLUSTER',
|
|
|
|
|
|
SBJK_DTDC: 'BATTERY',
|
|
|
|
|
|
SBJK_DB: 'AMMETER',
|
|
|
|
|
|
SBJK_YL: 'COOLING',
|
|
|
|
|
|
SBJK_DH: 'DH',
|
|
|
|
|
|
SBJK_XF: 'XF'
|
|
|
|
|
|
}
|
|
|
|
|
|
return menuMap[menuCode] || ''
|
|
|
|
|
|
},
|
|
|
|
|
|
handlePointInputFocus(row) {
|
|
|
|
|
|
if (!row) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.currentPointRow = row
|
|
|
|
|
|
this.pointSelectorQuery.pageNum = 1
|
|
|
|
|
|
this.pointSelectorQuery.siteId = this.siteId
|
|
|
|
|
|
this.pointSelectorQuery.deviceCategory = ''
|
|
|
|
|
|
this.pointSelectorQuery.deviceId = ''
|
|
|
|
|
|
this.pointSelectorVisible = true
|
|
|
|
|
|
this.loadPointSelectorList()
|
|
|
|
|
|
},
|
|
|
|
|
|
selectPointFromDialog(pointRow) {
|
|
|
|
|
|
if (!this.currentPointRow) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.currentPointRow.point = pointRow?.pointId || ''
|
|
|
|
|
|
this.currentPointRow.useFixedDisplay = 0
|
|
|
|
|
|
this.currentPointRow = null
|
|
|
|
|
|
this.pointSelectorVisible = false
|
|
|
|
|
|
},
|
|
|
|
|
|
searchPointSelector() {
|
|
|
|
|
|
this.pointSelectorQuery.pageNum = 1
|
|
|
|
|
|
this.loadPointSelectorList()
|
|
|
|
|
|
},
|
|
|
|
|
|
resetPointSelector() {
|
|
|
|
|
|
this.pointSelectorQuery.pageNum = 1
|
|
|
|
|
|
this.pointSelectorQuery.pointId = ''
|
|
|
|
|
|
this.pointSelectorQuery.pointDesc = ''
|
|
|
|
|
|
this.loadPointSelectorList()
|
|
|
|
|
|
},
|
|
|
|
|
|
handlePointSelectorPageChange(pageNum) {
|
|
|
|
|
|
this.pointSelectorQuery.pageNum = pageNum
|
|
|
|
|
|
this.loadPointSelectorList()
|
|
|
|
|
|
},
|
2026-04-01 14:27:35 +08:00
|
|
|
|
openImportConfigDialog() {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
this.$message.warning('请先选择站点')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.$refs.configInput) {
|
|
|
|
|
|
this.$refs.configInput.value = ''
|
|
|
|
|
|
this.$refs.configInput.click()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-15 22:44:21 +08:00
|
|
|
|
async openSingleBatteryInitDialog() {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
this.$message.warning('请先选择站点')
|
2026-04-12 15:18:00 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-15 22:44:21 +08:00
|
|
|
|
this.singleBatteryInitDialogVisible = true
|
|
|
|
|
|
this.singleBatteryInitForm.stackDeviceId = ''
|
|
|
|
|
|
this.singleBatteryInitForm.clusterDeviceId = ''
|
|
|
|
|
|
this.singleBatteryInitForm.targetCount = 1
|
|
|
|
|
|
await this.loadSingleBatteryStackOptions()
|
|
|
|
|
|
this.singleBatteryClusterOptions = []
|
2026-04-12 15:18:00 +08:00
|
|
|
|
},
|
2026-04-15 22:44:21 +08:00
|
|
|
|
async loadSingleBatteryStackOptions() {
|
|
|
|
|
|
const response = await getStackNameList(this.siteId)
|
|
|
|
|
|
this.singleBatteryStackOptions = Array.isArray(response?.data) ? response.data : []
|
|
|
|
|
|
},
|
|
|
|
|
|
async handleSingleBatteryStackChange(stackDeviceId) {
|
|
|
|
|
|
this.singleBatteryInitForm.clusterDeviceId = ''
|
|
|
|
|
|
if (!stackDeviceId) {
|
|
|
|
|
|
this.singleBatteryClusterOptions = []
|
2026-04-12 15:18:00 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-15 22:44:21 +08:00
|
|
|
|
const response = await getClusterNameList({ stackDeviceId, siteId: this.siteId })
|
|
|
|
|
|
this.singleBatteryClusterOptions = Array.isArray(response?.data) ? response.data : []
|
|
|
|
|
|
},
|
|
|
|
|
|
normalizeSingleBatteryInitResult(result) {
|
|
|
|
|
|
const source = result || {}
|
|
|
|
|
|
return {
|
|
|
|
|
|
committed: !!source.committed,
|
|
|
|
|
|
siteId: source.siteId || '',
|
|
|
|
|
|
scopeType: source.scopeType || 'stack',
|
|
|
|
|
|
scopeDeviceId: source.scopeDeviceId || '',
|
|
|
|
|
|
targetCount: Number(source.targetCount || 0),
|
|
|
|
|
|
existingBatteryCount: Number(source.existingBatteryCount || 0),
|
|
|
|
|
|
initializedBatteryCount: Number(source.initializedBatteryCount || 0),
|
|
|
|
|
|
insertedBatteryCount: Number(source.insertedBatteryCount || 0),
|
|
|
|
|
|
pointHitCount: Number(source.pointHitCount || 0),
|
|
|
|
|
|
fixedValueFallbackCount: Number(source.fixedValueFallbackCount || 0),
|
|
|
|
|
|
insertedMappingCount: Number(source.insertedMappingCount || 0),
|
|
|
|
|
|
updatedMappingCount: Number(source.updatedMappingCount || 0),
|
|
|
|
|
|
message: source.message || ''
|
2026-04-12 15:18:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-15 22:44:21 +08:00
|
|
|
|
async submitSingleBatteryInit() {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
this.$message.warning('请先选择站点')
|
2026-04-12 15:18:00 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-15 22:44:21 +08:00
|
|
|
|
if (!this.singleBatteryInitForm.stackDeviceId) {
|
|
|
|
|
|
this.$message.warning('请选择电池堆')
|
2026-04-12 15:18:00 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-15 22:44:21 +08:00
|
|
|
|
if (!this.singleBatteryInitForm.clusterDeviceId) {
|
|
|
|
|
|
this.$message.warning('请选择电池簇')
|
2026-04-12 15:18:00 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-15 22:44:21 +08:00
|
|
|
|
const scopeType = 'cluster'
|
|
|
|
|
|
const scopeDeviceId = this.singleBatteryInitForm.clusterDeviceId
|
|
|
|
|
|
this.singleBatteryInitLoading = true
|
2026-04-12 15:18:00 +08:00
|
|
|
|
try {
|
2026-04-15 22:44:21 +08:00
|
|
|
|
const response = await initializeSingleBatteryMonitorMappings({
|
|
|
|
|
|
siteId: this.siteId,
|
|
|
|
|
|
scopeType,
|
|
|
|
|
|
scopeDeviceId,
|
|
|
|
|
|
targetCount: this.singleBatteryInitForm.targetCount
|
|
|
|
|
|
})
|
|
|
|
|
|
this.singleBatteryInitResult = this.normalizeSingleBatteryInitResult(response?.data || {})
|
|
|
|
|
|
this.singleBatteryInitDialogVisible = false
|
|
|
|
|
|
this.singleBatteryInitResultVisible = true
|
|
|
|
|
|
if (this.singleBatteryInitResult.committed) {
|
2026-04-12 15:18:00 +08:00
|
|
|
|
await this.initMenuStructure()
|
2026-04-15 22:44:21 +08:00
|
|
|
|
this.$message.success('初始化单体电池配置成功')
|
2026-04-12 15:18:00 +08:00
|
|
|
|
} else {
|
2026-04-15 22:44:21 +08:00
|
|
|
|
this.$message.warning(this.singleBatteryInitResult.message || '初始化未完成')
|
2026-04-12 15:18:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-04-15 22:44:21 +08:00
|
|
|
|
this.$message.error(error?.message || '初始化失败,请稍后重试')
|
2026-04-12 15:18:00 +08:00
|
|
|
|
} finally {
|
2026-04-15 22:44:21 +08:00
|
|
|
|
this.singleBatteryInitLoading = false
|
2026-04-12 15:18:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-04-01 14:27:35 +08:00
|
|
|
|
handleConfigFileChange(event) {
|
|
|
|
|
|
const file = event && event.target && event.target.files && event.target.files[0]
|
|
|
|
|
|
if (!file) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const isCsv = /\.csv$/i.test(file.name || '')
|
|
|
|
|
|
if (!isCsv) {
|
|
|
|
|
|
this.$message.error('仅支持导入 CSV 文件')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const reader = new FileReader()
|
|
|
|
|
|
reader.onload = async () => {
|
|
|
|
|
|
const text = (reader.result || '').toString()
|
|
|
|
|
|
const parsed = this.parseCsvImportData(text)
|
|
|
|
|
|
if (!parsed) {
|
|
|
|
|
|
this.$message.error('CSV 格式不正确,导入失败')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.confirmAndApplyImport(parsed, file.name || '配置文件')
|
|
|
|
|
|
}
|
|
|
|
|
|
reader.onerror = () => {
|
|
|
|
|
|
this.$message.error('读取文件失败,请重试')
|
|
|
|
|
|
}
|
|
|
|
|
|
reader.readAsText(file, 'utf-8')
|
|
|
|
|
|
},
|
|
|
|
|
|
async confirmAndApplyImport(parsedData, fileName) {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
this.$message.warning('请先选择站点')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const importedSiteId = (((parsedData || {}).meta || {}).siteId || (parsedData || {}).siteId || '').toString().trim()
|
|
|
|
|
|
if (importedSiteId && importedSiteId !== this.siteId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.$confirm(
|
|
|
|
|
|
`文件站点(${importedSiteId})与当前站点(${this.siteId})不一致,仍要导入并覆盖当前配置吗?`,
|
|
|
|
|
|
'提示',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
showClose: false,
|
|
|
|
|
|
closeOnClickModal: false
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.$confirm(
|
|
|
|
|
|
`确认导入文件 ${fileName} 并覆盖当前页面配置吗?`,
|
|
|
|
|
|
'提示',
|
|
|
|
|
|
{
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning',
|
|
|
|
|
|
showClose: false,
|
|
|
|
|
|
closeOnClickModal: false
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.applyImportedConfig(parsedData)
|
|
|
|
|
|
},
|
|
|
|
|
|
normalizeImportMappings(parsedData) {
|
|
|
|
|
|
const data = parsedData || {}
|
|
|
|
|
|
const mappings = []
|
|
|
|
|
|
if (Array.isArray(data.mappings)) {
|
|
|
|
|
|
return data.mappings
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.pointMapping && Array.isArray(data.pointMapping.mappings)) {
|
|
|
|
|
|
return data.pointMapping.mappings
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.pointPayload && Array.isArray(data.pointPayload.mappings)) {
|
|
|
|
|
|
return data.pointPayload.mappings
|
|
|
|
|
|
}
|
|
|
|
|
|
return mappings
|
|
|
|
|
|
},
|
|
|
|
|
|
normalizeImportDeletedFieldCodes(parsedData) {
|
|
|
|
|
|
const data = parsedData || {}
|
|
|
|
|
|
const source = data.deletedFieldCodes || ((data.pointMapping || {}).deletedFieldCodes) || ((data.pointPayload || {}).deletedFieldCodes) || []
|
|
|
|
|
|
if (!Array.isArray(source)) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
return source
|
|
|
|
|
|
.map(item => (item === null || item === undefined ? '' : String(item).trim()))
|
|
|
|
|
|
.filter(item => !!item)
|
|
|
|
|
|
},
|
|
|
|
|
|
normalizeImportEnumMappings(parsedData) {
|
|
|
|
|
|
const data = parsedData || {}
|
|
|
|
|
|
if (Array.isArray(data.enumMappings)) {
|
|
|
|
|
|
return data.enumMappings
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.workStatusEnum && Array.isArray(data.workStatusEnum.mappings)) {
|
|
|
|
|
|
return data.workStatusEnum.mappings
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
},
|
|
|
|
|
|
escapeCsvCell(value) {
|
|
|
|
|
|
if (value === null || value === undefined) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const text = String(value)
|
|
|
|
|
|
if (/[",\n\r]/.test(text)) {
|
|
|
|
|
|
return `"${text.replace(/"/g, '""')}"`
|
|
|
|
|
|
}
|
|
|
|
|
|
return text
|
|
|
|
|
|
},
|
|
|
|
|
|
parseCsvText(text) {
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
const normalized = String(text).replace(/^\uFEFF/, '')
|
|
|
|
|
|
const rows = []
|
|
|
|
|
|
let row = []
|
|
|
|
|
|
let cell = ''
|
|
|
|
|
|
let inQuotes = false
|
|
|
|
|
|
for (let i = 0; i < normalized.length; i += 1) {
|
|
|
|
|
|
const ch = normalized[i]
|
|
|
|
|
|
if (inQuotes) {
|
|
|
|
|
|
if (ch === '"') {
|
|
|
|
|
|
if (normalized[i + 1] === '"') {
|
|
|
|
|
|
cell += '"'
|
|
|
|
|
|
i += 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
inQuotes = false
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cell += ch
|
|
|
|
|
|
}
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ch === '"') {
|
|
|
|
|
|
inQuotes = true
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ch === ',') {
|
|
|
|
|
|
row.push(cell)
|
|
|
|
|
|
cell = ''
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ch === '\n') {
|
|
|
|
|
|
row.push(cell)
|
|
|
|
|
|
rows.push(row)
|
|
|
|
|
|
row = []
|
|
|
|
|
|
cell = ''
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ch === '\r') {
|
|
|
|
|
|
if (normalized[i + 1] === '\n') {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
row.push(cell)
|
|
|
|
|
|
rows.push(row)
|
|
|
|
|
|
row = []
|
|
|
|
|
|
cell = ''
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
cell += ch
|
|
|
|
|
|
}
|
|
|
|
|
|
row.push(cell)
|
|
|
|
|
|
if (row.some(item => item !== '')) {
|
|
|
|
|
|
rows.push(row)
|
|
|
|
|
|
}
|
|
|
|
|
|
return rows
|
|
|
|
|
|
},
|
|
|
|
|
|
mapCsvHeaderIndexes(headers) {
|
|
|
|
|
|
const headerMap = {}
|
|
|
|
|
|
const aliasMap = {
|
|
|
|
|
|
type: ['type'],
|
|
|
|
|
|
siteId: ['siteId'],
|
|
|
|
|
|
siteName: ['siteName'],
|
|
|
|
|
|
exportedAt: ['exportedAt'],
|
|
|
|
|
|
fieldCode: ['fieldCode'],
|
|
|
|
|
|
deviceId: ['deviceId'],
|
|
|
|
|
|
dataPoint: ['dataPoint', 'pointId'],
|
|
|
|
|
|
fixedDataPoint: ['fixedDataPoint'],
|
|
|
|
|
|
useFixedDisplay: ['useFixedDisplay'],
|
|
|
|
|
|
deletedFieldCode: ['deletedFieldCode'],
|
|
|
|
|
|
deviceCategory: ['deviceCategory'],
|
|
|
|
|
|
matchField: ['matchField'],
|
|
|
|
|
|
enumCode: ['enumCode'],
|
|
|
|
|
|
enumName: ['enumName'],
|
|
|
|
|
|
dataEnumCode: ['dataEnumCode'],
|
|
|
|
|
|
enumDesc: ['enumDesc']
|
|
|
|
|
|
}
|
|
|
|
|
|
const normalized = headers.map(item => String(item || '').trim())
|
|
|
|
|
|
Object.keys(aliasMap).forEach(key => {
|
|
|
|
|
|
const aliases = aliasMap[key]
|
|
|
|
|
|
const index = normalized.findIndex(item => aliases.includes(item))
|
|
|
|
|
|
if (index >= 0) {
|
|
|
|
|
|
headerMap[key] = index
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return headerMap
|
|
|
|
|
|
},
|
|
|
|
|
|
parseCsvImportData(text) {
|
|
|
|
|
|
const rows = this.parseCsvText(text)
|
|
|
|
|
|
if (!rows.length) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const headerIndexes = this.mapCsvHeaderIndexes(rows[0] || [])
|
|
|
|
|
|
if (Object.keys(headerIndexes).length === 0) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const meta = {}
|
|
|
|
|
|
const mappings = []
|
|
|
|
|
|
const deletedFieldCodes = []
|
|
|
|
|
|
const parseUseFixedDisplay = value => {
|
|
|
|
|
|
const textValue = String(value || '').trim().toLowerCase()
|
|
|
|
|
|
if (['1', 'true', '是', 'y', 'yes'].includes(textValue)) {
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let i = 1; i < rows.length; i += 1) {
|
|
|
|
|
|
const row = rows[i]
|
|
|
|
|
|
if (!row || row.every(cell => String(cell || '').trim() === '')) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
const getValue = key => {
|
|
|
|
|
|
const index = headerIndexes[key]
|
|
|
|
|
|
if (index === undefined) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return row[index] === undefined || row[index] === null ? '' : String(row[index]).trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
const type = getValue('type').toLowerCase()
|
|
|
|
|
|
const siteId = getValue('siteId')
|
|
|
|
|
|
const siteName = getValue('siteName')
|
|
|
|
|
|
const exportedAt = getValue('exportedAt')
|
|
|
|
|
|
if (type === 'meta' || (!type && (siteId || siteName || exportedAt))) {
|
|
|
|
|
|
if (siteId) {
|
|
|
|
|
|
meta.siteId = siteId
|
|
|
|
|
|
}
|
|
|
|
|
|
if (siteName) {
|
|
|
|
|
|
meta.siteName = siteName
|
|
|
|
|
|
}
|
|
|
|
|
|
if (exportedAt) {
|
|
|
|
|
|
meta.exportedAt = exportedAt
|
|
|
|
|
|
}
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
const fieldCode = getValue('fieldCode')
|
|
|
|
|
|
const deviceId = getValue('deviceId')
|
|
|
|
|
|
const dataPoint = getValue('dataPoint')
|
|
|
|
|
|
const fixedDataPoint = getValue('fixedDataPoint')
|
|
|
|
|
|
const useFixedDisplay = parseUseFixedDisplay(getValue('useFixedDisplay'))
|
|
|
|
|
|
const deletedFieldCode = getValue('deletedFieldCode')
|
|
|
|
|
|
if (type === 'deleted' || (!type && deletedFieldCode && !fieldCode)) {
|
|
|
|
|
|
if (deletedFieldCode) {
|
|
|
|
|
|
deletedFieldCodes.push(deletedFieldCode)
|
|
|
|
|
|
}
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (type === 'point' || fieldCode) {
|
|
|
|
|
|
if (!fieldCode) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
mappings.push({
|
|
|
|
|
|
fieldCode,
|
|
|
|
|
|
deviceId,
|
|
|
|
|
|
dataPoint,
|
|
|
|
|
|
fixedDataPoint,
|
|
|
|
|
|
useFixedDisplay
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
meta,
|
|
|
|
|
|
mappings,
|
|
|
|
|
|
deletedFieldCodes
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
buildMappingKey(fieldCode, deviceId) {
|
|
|
|
|
|
return `${(fieldCode || '').toString().trim()}::${(deviceId || '').toString().trim()}`
|
|
|
|
|
|
},
|
|
|
|
|
|
applyImportedConfig(parsedData) {
|
|
|
|
|
|
const importedMappings = this.normalizeImportMappings(parsedData)
|
|
|
|
|
|
const importedDeletedFieldCodes = this.normalizeImportDeletedFieldCodes(parsedData)
|
|
|
|
|
|
if (!Array.isArray(importedMappings) || importedMappings.length === 0) {
|
|
|
|
|
|
this.$message.error('导入文件缺少 mappings 数据')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const mappingByKey = new Map()
|
|
|
|
|
|
importedMappings.forEach(item => {
|
|
|
|
|
|
const fieldCode = ((item && item.fieldCode) || '').toString().trim()
|
|
|
|
|
|
if (!fieldCode) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const deviceId = ((item && item.deviceId) || '').toString().trim()
|
|
|
|
|
|
const key = this.buildMappingKey(fieldCode, deviceId)
|
|
|
|
|
|
mappingByKey.set(key, {
|
|
|
|
|
|
dataPoint: ((item && item.dataPoint) || '').toString(),
|
|
|
|
|
|
fixedDataPoint: ((item && item.fixedDataPoint) || '').toString(),
|
|
|
|
|
|
useFixedDisplay: item && item.useFixedDisplay === 1 ? 1 : 0
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
const allRows = this.getAllMappingRows()
|
|
|
|
|
|
let hitCount = 0
|
|
|
|
|
|
allRows.forEach(row => {
|
|
|
|
|
|
const key = this.buildMappingKey(row && row.field, row && row.deviceId)
|
|
|
|
|
|
const mapped = mappingByKey.get(key)
|
|
|
|
|
|
if (mapped) {
|
|
|
|
|
|
row.point = mapped.dataPoint
|
|
|
|
|
|
row.fixedValue = mapped.fixedDataPoint
|
|
|
|
|
|
row.useFixedDisplay = mapped.useFixedDisplay
|
|
|
|
|
|
hitCount += 1
|
|
|
|
|
|
} else {
|
|
|
|
|
|
row.point = ''
|
|
|
|
|
|
row.fixedValue = ''
|
|
|
|
|
|
row.useFixedDisplay = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
this.suppressAutoSave = true
|
|
|
|
|
|
this.deletedFieldCodes = importedDeletedFieldCodes
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.suppressAutoSave = false
|
|
|
|
|
|
this.scheduleAutoSave()
|
|
|
|
|
|
})
|
|
|
|
|
|
const missCount = Math.max(mappingByKey.size - hitCount, 0)
|
|
|
|
|
|
if (missCount > 0) {
|
|
|
|
|
|
this.$message.warning(`导入完成:匹配 ${hitCount} 条,忽略 ${missCount} 条(当前页面无对应字段/设备)`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.success(`导入完成:匹配 ${hitCount} 条,系统将自动保存`)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
escapeJsonValue(value) {
|
|
|
|
|
|
if (value === undefined || value === null) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return String(value)
|
|
|
|
|
|
},
|
|
|
|
|
|
exportConfig() {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
this.$message.warning('请先选择站点')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const pointPayload = this.buildSavePayload()
|
|
|
|
|
|
const headers = [
|
|
|
|
|
|
'type',
|
|
|
|
|
|
'siteId',
|
|
|
|
|
|
'siteName',
|
|
|
|
|
|
'exportedAt',
|
|
|
|
|
|
'fieldCode',
|
|
|
|
|
|
'deviceId',
|
|
|
|
|
|
'dataPoint',
|
|
|
|
|
|
'fixedDataPoint',
|
|
|
|
|
|
'useFixedDisplay',
|
|
|
|
|
|
'deletedFieldCode'
|
|
|
|
|
|
]
|
|
|
|
|
|
const rows = []
|
|
|
|
|
|
rows.push([
|
|
|
|
|
|
'meta',
|
|
|
|
|
|
this.siteId,
|
|
|
|
|
|
this.siteName || '',
|
|
|
|
|
|
new Date().toISOString(),
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
''
|
|
|
|
|
|
])
|
|
|
|
|
|
;(pointPayload.mappings || []).forEach(item => {
|
|
|
|
|
|
rows.push([
|
|
|
|
|
|
'point',
|
|
|
|
|
|
this.siteId,
|
|
|
|
|
|
this.siteName || '',
|
|
|
|
|
|
'',
|
|
|
|
|
|
item.fieldCode || '',
|
|
|
|
|
|
item.deviceId || '',
|
|
|
|
|
|
item.dataPoint || '',
|
|
|
|
|
|
item.fixedDataPoint || '',
|
|
|
|
|
|
item.useFixedDisplay === 1 ? '1' : '0',
|
|
|
|
|
|
''
|
|
|
|
|
|
])
|
|
|
|
|
|
})
|
|
|
|
|
|
;(pointPayload.deletedFieldCodes || []).forEach(code => {
|
|
|
|
|
|
rows.push([
|
|
|
|
|
|
'deleted',
|
|
|
|
|
|
this.siteId,
|
|
|
|
|
|
this.siteName || '',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'',
|
|
|
|
|
|
code || ''
|
|
|
|
|
|
])
|
|
|
|
|
|
})
|
|
|
|
|
|
const csvText = [headers.map(cell => this.escapeCsvCell(cell)).join(','), ...rows.map(row => row.map(cell => this.escapeCsvCell(cell)).join(','))].join('\n')
|
|
|
|
|
|
const blob = new Blob([`\uFEFF${csvText}`], { type: 'text/csv;charset=utf-8;' })
|
|
|
|
|
|
const safeSiteId = this.escapeJsonValue(this.siteId).replace(/[^\w-]/g, '_')
|
|
|
|
|
|
const fileName = `single_monitor_mapping_${safeSiteId || 'site'}_${Date.now()}.csv`
|
|
|
|
|
|
const url = window.URL.createObjectURL(blob)
|
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
|
link.href = url
|
|
|
|
|
|
link.download = fileName
|
|
|
|
|
|
document.body.appendChild(link)
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
document.body.removeChild(link)
|
|
|
|
|
|
window.URL.revokeObjectURL(url)
|
|
|
|
|
|
this.$message.success('导出成功')
|
|
|
|
|
|
},
|
|
|
|
|
|
schedulePointSelectorSearch() {
|
|
|
|
|
|
if (!this.pointSelectorVisible) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.pointSelectorQueryTimer) {
|
|
|
|
|
|
clearTimeout(this.pointSelectorQueryTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pointSelectorQueryTimer = setTimeout(() => {
|
|
|
|
|
|
this.pointSelectorQuery.pageNum = 1
|
|
|
|
|
|
this.loadPointSelectorList()
|
|
|
|
|
|
}, this.pointSelectorQueryDelay)
|
|
|
|
|
|
},
|
|
|
|
|
|
loadPointSelectorList(options = {}) {
|
|
|
|
|
|
const { silent = false } = options
|
2026-02-15 16:24:29 +08:00
|
|
|
|
if (!this.pointSelectorQuery.siteId) {
|
|
|
|
|
|
this.pointSelectorList = []
|
|
|
|
|
|
this.pointSelectorTotal = 0
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-04-01 14:27:35 +08:00
|
|
|
|
if (!silent) {
|
|
|
|
|
|
this.pointSelectorLoading = true
|
|
|
|
|
|
}
|
2026-02-15 16:24:29 +08:00
|
|
|
|
const query = {
|
|
|
|
|
|
...this.pointSelectorQuery
|
|
|
|
|
|
}
|
|
|
|
|
|
delete query.deviceCategory
|
|
|
|
|
|
delete query.deviceId
|
|
|
|
|
|
getPointMatchList(query)
|
|
|
|
|
|
.then(response => {
|
|
|
|
|
|
this.pointSelectorList = response?.rows || []
|
|
|
|
|
|
this.pointSelectorTotal = response?.total || 0
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
2026-04-01 14:27:35 +08:00
|
|
|
|
if (!silent) {
|
|
|
|
|
|
this.pointSelectorLoading = false
|
|
|
|
|
|
}
|
2026-02-15 16:24:29 +08:00
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
getFilteredQuickGroups(list) {
|
|
|
|
|
|
const sourceList = Array.isArray(list) ? list : []
|
|
|
|
|
|
const groupMap = new Map()
|
|
|
|
|
|
sourceList.forEach(item => {
|
|
|
|
|
|
const groupKey = `${item.section || '-'}__${item.field || '-'}__${item.name || '-'}`
|
|
|
|
|
|
if (!groupMap.has(groupKey)) {
|
|
|
|
|
|
groupMap.set(groupKey, {
|
|
|
|
|
|
key: groupKey,
|
|
|
|
|
|
section: item.section || '-',
|
|
|
|
|
|
field: item.field || '-',
|
|
|
|
|
|
name: item.name || '-',
|
|
|
|
|
|
rows: []
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
groupMap.get(groupKey).rows.push(item)
|
|
|
|
|
|
})
|
|
|
|
|
|
const keyword = (this.quickFilter || '').toLowerCase()
|
|
|
|
|
|
const groups = Array.from(groupMap.values())
|
|
|
|
|
|
if (!keyword) {
|
|
|
|
|
|
return groups
|
|
|
|
|
|
}
|
|
|
|
|
|
return groups.filter(group => {
|
|
|
|
|
|
const groupMatch = [group.section, group.field, group.name].some(text => (text || '').toLowerCase().includes(keyword))
|
|
|
|
|
|
if (groupMatch) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return group.rows.some(row => (row.deviceName || '').toLowerCase().includes(keyword))
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
getFilteredSectionBlocks(list) {
|
|
|
|
|
|
const fieldGroups = this.getFilteredQuickGroups(list)
|
|
|
|
|
|
const sectionMap = new Map()
|
|
|
|
|
|
fieldGroups.forEach(group => {
|
|
|
|
|
|
const sectionName = group.section || '-'
|
|
|
|
|
|
if (!sectionMap.has(sectionName)) {
|
|
|
|
|
|
sectionMap.set(sectionName, {
|
|
|
|
|
|
key: sectionName,
|
|
|
|
|
|
section: sectionName,
|
|
|
|
|
|
fields: []
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
sectionMap.get(sectionName).fields.push(group)
|
|
|
|
|
|
})
|
|
|
|
|
|
return Array.from(sectionMap.values())
|
|
|
|
|
|
},
|
|
|
|
|
|
buildSectionPanelKey(topCode, childCode) {
|
|
|
|
|
|
return `${topCode || 'ROOT'}__${childCode || 'ROOT'}`
|
|
|
|
|
|
},
|
|
|
|
|
|
getSectionActiveValue(panelKey, blocks) {
|
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(this.sectionActiveMap, panelKey)) {
|
|
|
|
|
|
return this.sectionActiveMap[panelKey]
|
|
|
|
|
|
}
|
|
|
|
|
|
return Array.isArray(blocks) && blocks.length > 0 ? blocks[0].key : ''
|
|
|
|
|
|
},
|
|
|
|
|
|
setSectionActiveValue(panelKey, value) {
|
|
|
|
|
|
this.$set(this.sectionActiveMap, panelKey, value || '')
|
|
|
|
|
|
},
|
|
|
|
|
|
clearGroupMapping(group) {
|
|
|
|
|
|
if (!group || !Array.isArray(group.rows)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
group.rows.forEach(row => {
|
|
|
|
|
|
row.point = ''
|
|
|
|
|
|
row.fixedValue = ''
|
|
|
|
|
|
row.useFixedDisplay = 0
|
|
|
|
|
|
})
|
|
|
|
|
|
this.$message.success('已清空该字段映射,系统将自动保存')
|
|
|
|
|
|
},
|
|
|
|
|
|
buildDefaultWorkStatusEnumMappings() {
|
|
|
|
|
|
return [
|
|
|
|
|
|
this.buildEnumItem('PCS', 'workStatus', '0', '运行'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'workStatus', '1', '停机'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'workStatus', '2', '故障'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'workStatus', '3', '待机'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'workStatus', '4', '充电'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'workStatus', '5', '放电'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'gridStatus', '0', '并网'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'gridStatus', '1', '未并网'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'deviceStatus', '0', '离线'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'deviceStatus', '1', '在线'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'controlMode', '0', '远程'),
|
|
|
|
|
|
this.buildEnumItem('PCS', 'controlMode', '1', '本地'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'workStatus', '0', '静置'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'workStatus', '1', '充电'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'workStatus', '2', '放电'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'workStatus', '3', '浮充'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'workStatus', '4', '待机'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'workStatus', '5', '运行'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'workStatus', '9', '故障'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'pcsCommunicationStatus', '0', '正常'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'pcsCommunicationStatus', '1', '通讯中断'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'pcsCommunicationStatus', '2', '异常'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'emsCommunicationStatus', '0', '正常'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'emsCommunicationStatus', '1', '通讯中断'),
|
|
|
|
|
|
this.buildEnumItem('STACK', 'emsCommunicationStatus', '2', '异常'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'workStatus', '0', '静置'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'workStatus', '1', '充电'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'workStatus', '2', '放电'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'workStatus', '3', '待机'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'workStatus', '5', '运行'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'workStatus', '9', '故障'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'pcsCommunicationStatus', '0', '正常'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'pcsCommunicationStatus', '1', '通讯中断'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'pcsCommunicationStatus', '2', '异常'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'emsCommunicationStatus', '0', '正常'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'emsCommunicationStatus', '1', '通讯中断'),
|
|
|
|
|
|
this.buildEnumItem('CLUSTER', 'emsCommunicationStatus', '2', '异常')
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
buildEnumItem(deviceCategory, matchField, enumCode = '', enumName = '', enumDesc = '', dataEnumCode = '') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
deviceCategory: deviceCategory || '',
|
|
|
|
|
|
matchField: matchField || '',
|
|
|
|
|
|
enumCode: enumCode || '',
|
|
|
|
|
|
enumName: enumName || '',
|
|
|
|
|
|
enumDesc: enumDesc || '',
|
|
|
|
|
|
dataEnumCode: dataEnumCode || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
parseEnumScopeKey(scopeKey) {
|
|
|
|
|
|
const text = scopeKey || ''
|
|
|
|
|
|
const splitIndex = text.indexOf('|')
|
|
|
|
|
|
if (splitIndex < 0) {
|
|
|
|
|
|
return { deviceCategory: '', matchField: '' }
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
deviceCategory: text.slice(0, splitIndex),
|
|
|
|
|
|
matchField: text.slice(splitIndex + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
getEnumMappingsByScope(scopeKey) {
|
|
|
|
|
|
const scope = this.parseEnumScopeKey(scopeKey)
|
|
|
|
|
|
return (this.workStatusEnumMappings || []).filter(item => {
|
|
|
|
|
|
return (item.deviceCategory || '') === scope.deviceCategory && (item.matchField || '') === scope.matchField
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
normalizeWorkStatusEnumMappings(list) {
|
|
|
|
|
|
const source = Array.isArray(list) ? list : []
|
|
|
|
|
|
const normalized = source
|
|
|
|
|
|
.map(item => this.buildEnumItem(
|
|
|
|
|
|
(item && item.deviceCategory) || '',
|
|
|
|
|
|
(item && item.matchField) || '',
|
|
|
|
|
|
(item && item.enumCode) || '',
|
|
|
|
|
|
(item && item.enumName) || '',
|
|
|
|
|
|
(item && item.enumDesc) || '',
|
|
|
|
|
|
(item && item.dataEnumCode) || ''
|
|
|
|
|
|
))
|
|
|
|
|
|
.filter(item => (item.deviceCategory && item.matchField) && (item.enumCode || item.enumName || item.dataEnumCode))
|
|
|
|
|
|
if (normalized.length > 0) {
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.buildDefaultWorkStatusEnumMappings()
|
|
|
|
|
|
},
|
|
|
|
|
|
addWorkStatusEnumMapping() {
|
|
|
|
|
|
const scope = this.parseEnumScopeKey(this.activeEnumScope)
|
|
|
|
|
|
this.workStatusEnumMappings.push(this.buildEnumItem(scope.deviceCategory, scope.matchField))
|
|
|
|
|
|
},
|
|
|
|
|
|
removeWorkStatusEnumMapping(row) {
|
|
|
|
|
|
const index = this.workStatusEnumMappings.findIndex(item => item === row)
|
|
|
|
|
|
if (index < 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.workStatusEnumMappings.splice(index, 1)
|
|
|
|
|
|
},
|
|
|
|
|
|
resetWorkStatusEnumMappings() {
|
|
|
|
|
|
this.workStatusEnumMappings = this.buildDefaultWorkStatusEnumMappings()
|
|
|
|
|
|
this.$message.success('已恢复默认工作状态枚举')
|
|
|
|
|
|
},
|
|
|
|
|
|
buildWorkStatusEnumPayload() {
|
|
|
|
|
|
const mappings = (this.workStatusEnumMappings || [])
|
|
|
|
|
|
.map(item => ({
|
|
|
|
|
|
deviceCategory: (item && item.deviceCategory) || '',
|
|
|
|
|
|
matchField: (item && item.matchField) || '',
|
|
|
|
|
|
enumCode: (item && item.enumCode) || '',
|
|
|
|
|
|
enumName: (item && item.enumName) || '',
|
|
|
|
|
|
enumDesc: (item && item.enumDesc) || '',
|
|
|
|
|
|
dataEnumCode: (item && item.dataEnumCode) || ''
|
|
|
|
|
|
}))
|
|
|
|
|
|
.filter(item => (item.deviceCategory && item.matchField) && (item.enumCode || item.enumName || item.dataEnumCode))
|
|
|
|
|
|
return {
|
|
|
|
|
|
siteId: this.siteId,
|
|
|
|
|
|
mappings
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-13 21:46:12 +08:00
|
|
|
|
handleDeleteItem(index, list) {
|
|
|
|
|
|
if (!Array.isArray(list) || index < 0 || index >= list.length) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-15 16:24:29 +08:00
|
|
|
|
const currentRow = list[index]
|
|
|
|
|
|
if (currentRow && currentRow.deviceId) {
|
|
|
|
|
|
currentRow.point = ''
|
|
|
|
|
|
currentRow.fixedValue = ''
|
|
|
|
|
|
currentRow.useFixedDisplay = 0
|
|
|
|
|
|
this.$message.success('已清空该设备映射,系统将自动保存')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-13 21:46:12 +08:00
|
|
|
|
this.$confirm('是否确认删除该条明细记录?删除后需点击“保存”才会生效。', '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning'
|
|
|
|
|
|
}).then(() => {
|
|
|
|
|
|
const fieldCode = list[index] && list[index].field
|
|
|
|
|
|
if (fieldCode && !this.deletedFieldCodes.includes(fieldCode)) {
|
|
|
|
|
|
this.deletedFieldCodes.push(fieldCode)
|
|
|
|
|
|
}
|
|
|
|
|
|
list.splice(index, 1)
|
2026-02-15 16:24:29 +08:00
|
|
|
|
this.$message.success('已删除该条明细记录,系统将自动保存')
|
2026-02-13 21:46:12 +08:00
|
|
|
|
}).catch(() => {})
|
|
|
|
|
|
},
|
2026-02-15 16:24:29 +08:00
|
|
|
|
buildSavePayload() {
|
2026-02-12 21:19:23 +08:00
|
|
|
|
const mappings = this.getAllMappingRows()
|
|
|
|
|
|
.filter(item => item.field)
|
2026-02-13 21:46:12 +08:00
|
|
|
|
.map(item => ({
|
|
|
|
|
|
fieldCode: item.field,
|
2026-02-15 16:24:29 +08:00
|
|
|
|
deviceId: item.deviceId || '',
|
2026-02-13 21:46:12 +08:00
|
|
|
|
dataPoint: item.point || '',
|
|
|
|
|
|
fixedDataPoint: item.fixedValue || '',
|
|
|
|
|
|
useFixedDisplay: item.useFixedDisplay === 1 ? 1 : 0
|
|
|
|
|
|
}))
|
2026-02-15 16:24:29 +08:00
|
|
|
|
return {
|
2026-02-13 21:46:12 +08:00
|
|
|
|
siteId: this.siteId,
|
|
|
|
|
|
mappings,
|
|
|
|
|
|
deletedFieldCodes: this.deletedFieldCodes
|
|
|
|
|
|
}
|
2026-02-15 16:24:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
buildPointPayloadSignature() {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return JSON.stringify(this.buildSavePayload())
|
|
|
|
|
|
},
|
|
|
|
|
|
buildEnumPayloadSignature() {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return JSON.stringify(this.buildWorkStatusEnumPayload())
|
|
|
|
|
|
},
|
|
|
|
|
|
scheduleAutoSave() {
|
|
|
|
|
|
if (this.suppressAutoSave || !this.siteId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const pointChanged = this.buildPointPayloadSignature() !== this.lastSavedPointSignature
|
|
|
|
|
|
const enumChanged = this.buildEnumPayloadSignature() !== this.lastSavedEnumSignature
|
|
|
|
|
|
if (!pointChanged && !enumChanged) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.saveStatusText = '有未保存修改...'
|
|
|
|
|
|
if (this.autoSaveTimer) {
|
|
|
|
|
|
clearTimeout(this.autoSaveTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
this.autoSaveTimer = setTimeout(() => {
|
|
|
|
|
|
this.persistMappings()
|
|
|
|
|
|
}, this.autoSaveDelay)
|
|
|
|
|
|
},
|
|
|
|
|
|
async persistMappings() {
|
|
|
|
|
|
if (!this.siteId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.isSaving) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const pointSignature = this.buildPointPayloadSignature()
|
|
|
|
|
|
const enumSignature = this.buildEnumPayloadSignature()
|
|
|
|
|
|
const pointChanged = !!pointSignature && pointSignature !== this.lastSavedPointSignature
|
|
|
|
|
|
const enumChanged = !!enumSignature && enumSignature !== this.lastSavedEnumSignature
|
|
|
|
|
|
if (!pointChanged && !enumChanged) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.isSaving = true
|
|
|
|
|
|
let pointError = null
|
|
|
|
|
|
let enumError = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (pointChanged) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await saveSingleMonitorProjectPointMapping(this.buildSavePayload())
|
|
|
|
|
|
this.lastSavedPointSignature = pointSignature
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
pointError = error
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (enumChanged) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await saveSingleMonitorWorkStatusEnumMappings(this.buildWorkStatusEnumPayload())
|
|
|
|
|
|
this.lastSavedEnumSignature = enumSignature
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
enumError = error
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!pointError && !enumError) {
|
|
|
|
|
|
this.saveStatusText = '已自动保存'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pointError && !enumError) {
|
|
|
|
|
|
this.saveStatusText = '状态映射已保存,点位映射保存失败'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!pointError && enumError) {
|
|
|
|
|
|
this.saveStatusText = '点位映射已保存,状态映射保存失败'
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
this.saveStatusText = '自动保存失败,请稍后重试'
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.isSaving = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
|
if (this.autoSaveTimer) {
|
|
|
|
|
|
clearTimeout(this.autoSaveTimer)
|
2026-02-12 21:19:23 +08:00
|
|
|
|
}
|
2026-04-01 14:27:35 +08:00
|
|
|
|
if (this.pointSelectorQueryTimer) {
|
|
|
|
|
|
clearTimeout(this.pointSelectorQueryTimer)
|
|
|
|
|
|
this.pointSelectorQueryTimer = null
|
|
|
|
|
|
}
|
2026-02-12 21:19:23 +08:00
|
|
|
|
},
|
|
|
|
|
|
async mounted() {
|
2026-02-13 21:46:12 +08:00
|
|
|
|
this.siteId = this.$route.query.siteId || ''
|
|
|
|
|
|
this.siteName = this.$route.query.siteName || this.siteId
|
2026-02-12 21:19:23 +08:00
|
|
|
|
await this.initMenuStructure()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.header-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title-block {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-title {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-desc {
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #909399;
|
2026-02-13 21:46:12 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.site-label {
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 16:24:29 +08:00
|
|
|
|
.enum-mapping-card {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.enum-mapping-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.enum-scope-group {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.enum-mapping-tip {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 21:46:12 +08:00
|
|
|
|
.short-input {
|
|
|
|
|
|
width: 220px;
|
2026-02-12 21:19:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.child-menu-tabs {
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
2026-02-15 16:24:29 +08:00
|
|
|
|
|
|
|
|
|
|
.mode-switch-row {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
gap: 12px;
|
2026-04-01 14:27:35 +08:00
|
|
|
|
flex-wrap: wrap;
|
2026-02-15 16:24:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-status-text {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
min-width: 120px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-filter-input {
|
|
|
|
|
|
width: 280px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 14:27:35 +08:00
|
|
|
|
.filter-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-12 15:18:00 +08:00
|
|
|
|
.filter-actions:not(.single-battery-actions-visible) > .el-button:nth-of-type(3),
|
|
|
|
|
|
.filter-actions:not(.single-battery-actions-visible) > .el-button:nth-of-type(4) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.import-result-title {
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 16:24:29 +08:00
|
|
|
|
.quick-wrapper {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-card {
|
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-card-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-title-block {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-sub-title {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-row {
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-row + .quick-row {
|
|
|
|
|
|
border-top: 1px dashed #ebeef5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-device-name {
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-wrapper {
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-collapse {
|
|
|
|
|
|
border-top: 0;
|
|
|
|
|
|
border-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title-row {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-badge {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-field-list {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-field-card {
|
|
|
|
|
|
border: 1px solid #ebeef5;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 2200px) {
|
|
|
|
|
|
.quick-wrapper {
|
|
|
|
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-field-list {
|
|
|
|
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1680px) {
|
|
|
|
|
|
.quick-wrapper {
|
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-field-list {
|
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1280px) {
|
|
|
|
|
|
.quick-wrapper {
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-field-list {
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-12 21:19:23 +08:00
|
|
|
|
</style>
|