重构
This commit is contained in:
@ -33,6 +33,17 @@
|
||||
class="quick-filter-input"
|
||||
placeholder="按字段名/展示名/设备名筛选"
|
||||
/>
|
||||
<div class="filter-actions">
|
||||
<el-button size="small" :disabled="!siteId" @click="openImportConfigDialog">导入配置</el-button>
|
||||
<el-button size="small" type="primary" :disabled="!siteId" @click="exportConfig">导出配置</el-button>
|
||||
<input
|
||||
ref="configInput"
|
||||
type="file"
|
||||
accept=".csv"
|
||||
style="display: none"
|
||||
@change="handleConfigFileChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTopMenu" style="margin-top: 16px;">
|
||||
@ -407,7 +418,7 @@
|
||||
>
|
||||
<el-form :inline="true" class="select-container">
|
||||
<el-form-item label="点位ID">
|
||||
<el-input v-model.trim="pointSelectorQuery.pointId" clearable placeholder="请输入点位ID" style="width: 180px" />
|
||||
<el-input v-model.trim="pointSelectorQuery.pointId" clearable placeholder="请输入点位ID(支持模糊)" style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="点位描述">
|
||||
<el-input v-model.trim="pointSelectorQuery.pointDesc" clearable placeholder="请输入点位描述" style="width: 180px" />
|
||||
@ -503,6 +514,8 @@ export default {
|
||||
},
|
||||
autoSaveTimer: null,
|
||||
autoSaveDelay: 900,
|
||||
pointSelectorQueryTimer: null,
|
||||
pointSelectorQueryDelay: 280,
|
||||
suppressAutoSave: false,
|
||||
isSaving: false,
|
||||
saveStatusText: '自动保存已开启',
|
||||
@ -540,6 +553,12 @@ export default {
|
||||
handler() {
|
||||
this.scheduleAutoSave()
|
||||
}
|
||||
},
|
||||
'pointSelectorQuery.pointId'() {
|
||||
this.schedulePointSelectorSearch()
|
||||
},
|
||||
'pointSelectorQuery.pointDesc'() {
|
||||
this.schedulePointSelectorSearch()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -687,13 +706,446 @@ export default {
|
||||
this.pointSelectorQuery.pageNum = pageNum
|
||||
this.loadPointSelectorList()
|
||||
},
|
||||
loadPointSelectorList() {
|
||||
openImportConfigDialog() {
|
||||
if (!this.siteId) {
|
||||
this.$message.warning('请先选择站点')
|
||||
return
|
||||
}
|
||||
if (this.$refs.configInput) {
|
||||
this.$refs.configInput.value = ''
|
||||
this.$refs.configInput.click()
|
||||
}
|
||||
},
|
||||
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
|
||||
if (!this.pointSelectorQuery.siteId) {
|
||||
this.pointSelectorList = []
|
||||
this.pointSelectorTotal = 0
|
||||
return
|
||||
}
|
||||
this.pointSelectorLoading = true
|
||||
if (!silent) {
|
||||
this.pointSelectorLoading = true
|
||||
}
|
||||
const query = {
|
||||
...this.pointSelectorQuery
|
||||
}
|
||||
@ -705,7 +1157,9 @@ export default {
|
||||
this.pointSelectorTotal = response?.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
this.pointSelectorLoading = false
|
||||
if (!silent) {
|
||||
this.pointSelectorLoading = false
|
||||
}
|
||||
})
|
||||
},
|
||||
getFilteredQuickGroups(list) {
|
||||
@ -1018,6 +1472,10 @@ export default {
|
||||
if (this.autoSaveTimer) {
|
||||
clearTimeout(this.autoSaveTimer)
|
||||
}
|
||||
if (this.pointSelectorQueryTimer) {
|
||||
clearTimeout(this.pointSelectorQueryTimer)
|
||||
this.pointSelectorQueryTimer = null
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.siteId = this.$route.query.siteId || ''
|
||||
@ -1093,6 +1551,7 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.save-status-text {
|
||||
@ -1105,6 +1564,12 @@ export default {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user