重构
This commit is contained in:
183
src/views/ems/site/dataCorrection/AddChargeDataCorrection.vue
Normal file
183
src/views/ems/site/dataCorrection/AddChargeDataCorrection.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible.sync="dialogVisible"
|
||||
:close-on-press-escape="false"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
append-to-body
|
||||
width="760px"
|
||||
class="ems-dialog"
|
||||
:title="mode === 'add' ? '新增充放电收益修正' : '编辑充放电收益修正'"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="loading"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
size="medium"
|
||||
class="form-grid"
|
||||
>
|
||||
<el-form-item label="站点" prop="siteId">
|
||||
<el-input v-model="formData.siteId" placeholder="请先在顶部选择站点" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据日期" prop="dateTime">
|
||||
<el-date-picker
|
||||
v-model="formData.dateTime"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="请选择数据日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="总充电量" prop="totalChargeData">
|
||||
<el-input-number v-model="formData.totalChargeData" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="总放电量" prop="totalDischargeData">
|
||||
<el-input-number v-model="formData.totalDischargeData" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当日充电量" prop="chargeData">
|
||||
<el-input-number v-model="formData.chargeData" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="当日放电量" prop="dischargeData">
|
||||
<el-input-number v-model="formData.dischargeData" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="累计收益" prop="totalRevenue">
|
||||
<el-input-number v-model="formData.totalRevenue" :controls="false" :min="-999999999" :max="999999999" :step="0.0001" :precision="4" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="当日收益" prop="dayRevenue">
|
||||
<el-input-number v-model="formData.dayRevenue" :controls="false" :min="-999999999" :max="999999999" :step="0.0001" :precision="4" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark" class="full-row">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" maxlength="300" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveDialog">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
addDailyChargeData,
|
||||
getDailyChargeDataDetail,
|
||||
updateDailyChargeData,
|
||||
} from '@/api/ems/site'
|
||||
|
||||
const buildEmptyForm = () => ({
|
||||
id: '',
|
||||
siteId: '',
|
||||
dateTime: '',
|
||||
totalChargeData: null,
|
||||
totalDischargeData: null,
|
||||
chargeData: null,
|
||||
dischargeData: null,
|
||||
totalRevenue: null,
|
||||
dayRevenue: null,
|
||||
remark: '',
|
||||
})
|
||||
|
||||
export default {
|
||||
name: 'AddChargeDataCorrection',
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
mode: 'add',
|
||||
loading: false,
|
||||
saving: false,
|
||||
formData: buildEmptyForm(),
|
||||
rules: {
|
||||
siteId: [{ required: true, message: '请先在顶部选择站点', trigger: 'blur' }],
|
||||
dateTime: [{ required: true, message: '请选择数据日期', trigger: 'change' }],
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRouteSiteId() {
|
||||
const siteId = this.$route?.query?.siteId
|
||||
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
|
||||
},
|
||||
showDialog(id, siteId = '') {
|
||||
this.dialogVisible = true
|
||||
if (id) {
|
||||
this.mode = 'edit'
|
||||
this.formData.id = id
|
||||
this.fetchDetail(id)
|
||||
} else {
|
||||
this.mode = 'add'
|
||||
this.formData = buildEmptyForm()
|
||||
this.formData.siteId = siteId || this.getRouteSiteId()
|
||||
}
|
||||
},
|
||||
fetchDetail(id) {
|
||||
this.loading = true
|
||||
getDailyChargeDataDetail(id)
|
||||
.then((res) => {
|
||||
this.formData = Object.assign(buildEmptyForm(), res?.data || {})
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
saveDialog() {
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.saving = true
|
||||
const request = this.mode === 'add' ? addDailyChargeData : updateDailyChargeData
|
||||
const payload = {
|
||||
id: this.formData.id,
|
||||
siteId: this.formData.siteId,
|
||||
dateTime: this.formData.dateTime,
|
||||
totalChargeData: this.formData.totalChargeData,
|
||||
totalDischargeData: this.formData.totalDischargeData,
|
||||
chargeData: this.formData.chargeData,
|
||||
dischargeData: this.formData.dischargeData,
|
||||
totalRevenue: this.formData.totalRevenue,
|
||||
dayRevenue: this.formData.dayRevenue,
|
||||
remark: this.formData.remark,
|
||||
}
|
||||
request(payload)
|
||||
.then((res) => {
|
||||
if (res?.code === 200) {
|
||||
this.$emit('update')
|
||||
this.closeDialog()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false
|
||||
})
|
||||
})
|
||||
},
|
||||
closeDialog() {
|
||||
this.dialogVisible = false
|
||||
this.formData = buildEmptyForm()
|
||||
this.$nextTick(() => {
|
||||
this.$refs.formRef && this.$refs.formRef.resetFields()
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 16px;
|
||||
|
||||
.full-row {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
218
src/views/ems/site/dataCorrection/AddDataCorrection.vue
Normal file
218
src/views/ems/site/dataCorrection/AddDataCorrection.vue
Normal file
@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible.sync="dialogVisible"
|
||||
:close-on-press-escape="false"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
lock-scroll
|
||||
append-to-body
|
||||
width="760px"
|
||||
class="ems-dialog"
|
||||
:title="mode === 'add' ? '新增数据修正' : '编辑数据修正'"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="loading"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="120px"
|
||||
size="medium"
|
||||
class="form-grid"
|
||||
>
|
||||
<el-form-item label="站点" prop="siteId">
|
||||
<el-input v-model="formData.siteId" placeholder="请先在顶部选择站点" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据日期" prop="dataDate">
|
||||
<el-date-picker
|
||||
v-model="formData.dataDate"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="请选择数据日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据小时" prop="dataHour">
|
||||
<el-input-number
|
||||
v-model="formData.dataHour"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:max="23"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
placeholder="0-23"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="尖充电差值" prop="peakChargeDiff">
|
||||
<el-input-number v-model="formData.peakChargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="尖放电差值" prop="peakDischargeDiff">
|
||||
<el-input-number v-model="formData.peakDischargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="峰充电差值" prop="highChargeDiff">
|
||||
<el-input-number v-model="formData.highChargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="峰放电差值" prop="highDischargeDiff">
|
||||
<el-input-number v-model="formData.highDischargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="平充电差值" prop="flatChargeDiff">
|
||||
<el-input-number v-model="formData.flatChargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="平放电差值" prop="flatDischargeDiff">
|
||||
<el-input-number v-model="formData.flatDischargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="谷充电差值" prop="valleyChargeDiff">
|
||||
<el-input-number v-model="formData.valleyChargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="谷放电差值" prop="valleyDischargeDiff">
|
||||
<el-input-number v-model="formData.valleyDischargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="计算时间" prop="calcTime" class="full-row">
|
||||
<el-date-picker
|
||||
v-model="formData.calcTime"
|
||||
type="datetime"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="请选择计算时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark" class="full-row">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" maxlength="300" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveDialog">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
addDailyEnergyData,
|
||||
getDailyEnergyDataDetail,
|
||||
updateDailyEnergyData,
|
||||
} from '@/api/ems/site'
|
||||
|
||||
const buildEmptyForm = () => ({
|
||||
id: '',
|
||||
siteId: '',
|
||||
dataDate: '',
|
||||
dataHour: null,
|
||||
peakChargeDiff: null,
|
||||
peakDischargeDiff: null,
|
||||
highChargeDiff: null,
|
||||
highDischargeDiff: null,
|
||||
flatChargeDiff: null,
|
||||
flatDischargeDiff: null,
|
||||
valleyChargeDiff: null,
|
||||
valleyDischargeDiff: null,
|
||||
calcTime: '',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
export default {
|
||||
name: 'AddDataCorrection',
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
mode: 'add',
|
||||
loading: false,
|
||||
saving: false,
|
||||
formData: buildEmptyForm(),
|
||||
rules: {
|
||||
siteId: [{ required: true, message: '请先在顶部选择站点', trigger: 'blur' }],
|
||||
dataDate: [{ required: true, message: '请选择数据日期', trigger: 'change' }],
|
||||
dataHour: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (Number.isInteger(value) && value >= 0 && value <= 23) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
callback(new Error('数据小时需为 0-23 的整数'))
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRouteSiteId() {
|
||||
const siteId = this.$route?.query?.siteId
|
||||
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
|
||||
},
|
||||
showDialog(id, siteId = '') {
|
||||
this.dialogVisible = true
|
||||
if (id) {
|
||||
this.mode = 'edit'
|
||||
this.formData.id = id
|
||||
this.fetchDetail(id)
|
||||
} else {
|
||||
this.mode = 'add'
|
||||
this.formData = buildEmptyForm()
|
||||
this.formData.siteId = siteId || this.getRouteSiteId()
|
||||
}
|
||||
},
|
||||
fetchDetail(id) {
|
||||
this.loading = true
|
||||
getDailyEnergyDataDetail(id)
|
||||
.then((res) => {
|
||||
this.formData = Object.assign(buildEmptyForm(), res?.data || {})
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
saveDialog() {
|
||||
this.$refs.formRef.validate((valid) => {
|
||||
if (!valid) return
|
||||
this.saving = true
|
||||
const request = this.mode === 'add' ? addDailyEnergyData : updateDailyEnergyData
|
||||
request(this.formData)
|
||||
.then((res) => {
|
||||
if (res?.code === 200) {
|
||||
this.$emit('update')
|
||||
this.closeDialog()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false
|
||||
})
|
||||
})
|
||||
},
|
||||
closeDialog() {
|
||||
this.dialogVisible = false
|
||||
this.formData = buildEmptyForm()
|
||||
this.$nextTick(() => {
|
||||
this.$refs.formRef && this.$refs.formRef.resetFields()
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 16px;
|
||||
|
||||
.full-row {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
187
src/views/ems/site/dataCorrection/DailyChargeDataTab.vue
Normal file
187
src/views/ems/site/dataCorrection/DailyChargeDataTab.vue
Normal file
@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div style="background-color: #ffffff" v-loading="loading">
|
||||
<el-form :inline="true" class="select-container" @submit.native.prevent>
|
||||
<el-form-item label="数据日期">
|
||||
<el-date-picker
|
||||
v-model="form.dateTime"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
clearable
|
||||
placeholder="请选择日期"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button native-type="button" type="primary" @click="onSearch">搜索</el-button>
|
||||
<el-button native-type="button" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-button type="primary" @click="openDialog('')">新增</el-button>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
class="common-table"
|
||||
max-height="620px"
|
||||
stripe
|
||||
style="width: 100%; margin-top: 20px"
|
||||
>
|
||||
<el-table-column label="站点" prop="siteId" min-width="120" />
|
||||
<el-table-column label="数据日期" prop="dateTime" width="120" />
|
||||
<el-table-column label="总充电量" prop="totalChargeData" min-width="110" />
|
||||
<el-table-column label="总放电量" prop="totalDischargeData" min-width="110" />
|
||||
<el-table-column label="当日充电量" prop="chargeData" min-width="110" />
|
||||
<el-table-column label="当日放电量" prop="dischargeData" min-width="110" />
|
||||
<el-table-column label="累计收益" prop="totalRevenue" min-width="110" />
|
||||
<el-table-column label="当日收益" prop="dayRevenue" min-width="110" />
|
||||
<el-table-column label="备注" prop="remark" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column fixed="right" label="操作" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="warning" @click="openDialog(scope.row.id)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-show="tableData.length > 0"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="pageNum"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 30, 40]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="totalSize"
|
||||
style="margin-top: 15px; text-align: center"
|
||||
/>
|
||||
|
||||
<add-charge-data-correction ref="addChargeDataCorrection" @update="getData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
deleteDailyChargeData,
|
||||
getDailyChargeDataList,
|
||||
} from '@/api/ems/site'
|
||||
import AddChargeDataCorrection from './AddChargeDataCorrection.vue'
|
||||
|
||||
export default {
|
||||
name: 'DailyChargeDataTab',
|
||||
components: { AddChargeDataCorrection },
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: {
|
||||
siteId: '',
|
||||
dateTime: '',
|
||||
},
|
||||
tableData: [],
|
||||
pageSize: 10,
|
||||
pageNum: 1,
|
||||
totalSize: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.query.siteId'(newSiteId) {
|
||||
const normalizedSiteId = this.normalizeSiteId(newSiteId)
|
||||
if (normalizedSiteId === this.form.siteId) {
|
||||
return
|
||||
}
|
||||
this.form.siteId = normalizedSiteId
|
||||
this.onSearch()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.form.siteId = this.normalizeSiteId(this.$route.query.siteId)
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
normalizeSiteId(siteId) {
|
||||
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val
|
||||
this.pageNum = 1
|
||||
this.getData()
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.pageNum = val
|
||||
this.getData()
|
||||
},
|
||||
onSearch() {
|
||||
this.pageNum = 1
|
||||
this.getData()
|
||||
},
|
||||
onReset() {
|
||||
this.form = {
|
||||
siteId: this.form.siteId,
|
||||
dateTime: '',
|
||||
}
|
||||
this.pageNum = 1
|
||||
this.getData()
|
||||
},
|
||||
getData() {
|
||||
if (!this.form.siteId) {
|
||||
this.tableData = []
|
||||
this.totalSize = 0
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
const params = {
|
||||
pageNum: this.pageNum,
|
||||
pageSize: this.pageSize,
|
||||
siteId: this.form.siteId,
|
||||
}
|
||||
if (this.form.dateTime) {
|
||||
params.dateTime = this.form.dateTime
|
||||
}
|
||||
getDailyChargeDataList(params)
|
||||
.then((res) => {
|
||||
this.tableData = res?.rows || []
|
||||
this.totalSize = res?.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
openDialog(id) {
|
||||
this.$refs.addChargeDataCorrection.showDialog(id, this.form.siteId)
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$confirm('确认要删除该条数据吗?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
showClose: false,
|
||||
closeOnClickModal: false,
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true
|
||||
deleteDailyChargeData(row.id)
|
||||
.then((res) => {
|
||||
if (res?.code === 200) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
instance.confirmButtonLoading = false
|
||||
})
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success('删除成功!')
|
||||
this.getData()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
191
src/views/ems/site/dataCorrection/DailyEnergyDataTab.vue
Normal file
191
src/views/ems/site/dataCorrection/DailyEnergyDataTab.vue
Normal file
@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div style="background-color: #ffffff" v-loading="loading">
|
||||
<el-form :inline="true" class="select-container" @submit.native.prevent>
|
||||
<el-form-item label="数据日期">
|
||||
<el-date-picker
|
||||
v-model="form.dataDate"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
clearable
|
||||
placeholder="请选择日期"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button native-type="button" type="primary" @click="onSearch">搜索</el-button>
|
||||
<el-button native-type="button" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-button type="primary" @click="openDialog('')">新增</el-button>
|
||||
|
||||
<el-table
|
||||
:data="tableData"
|
||||
class="common-table"
|
||||
max-height="620px"
|
||||
stripe
|
||||
style="width: 100%; margin-top: 20px"
|
||||
>
|
||||
<el-table-column label="站点" prop="siteId" width="130" />
|
||||
<el-table-column label="数据日期" prop="dataDate" width="120" />
|
||||
<el-table-column label="data_hour" prop="dataHour" width="100" />
|
||||
<el-table-column label="尖充" prop="peakChargeDiff" min-width="90" />
|
||||
<el-table-column label="尖放" prop="peakDischargeDiff" min-width="90" />
|
||||
<el-table-column label="峰充" prop="highChargeDiff" min-width="90" />
|
||||
<el-table-column label="峰放" prop="highDischargeDiff" min-width="90" />
|
||||
<el-table-column label="平充" prop="flatChargeDiff" min-width="90" />
|
||||
<el-table-column label="平放" prop="flatDischargeDiff" min-width="90" />
|
||||
<el-table-column label="谷充" prop="valleyChargeDiff" min-width="90" />
|
||||
<el-table-column label="谷放" prop="valleyDischargeDiff" min-width="90" />
|
||||
<el-table-column label="计算时间" prop="calcTime" min-width="170" />
|
||||
<el-table-column label="备注" prop="remark" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column fixed="right" label="操作" width="150">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="warning" @click="openDialog(scope.row.id)">编辑</el-button>
|
||||
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-show="tableData.length > 0"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="pageNum"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 30, 40]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="totalSize"
|
||||
style="margin-top: 15px; text-align: center"
|
||||
/>
|
||||
|
||||
<add-data-correction ref="addDataCorrection" @update="getData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
deleteDailyEnergyData,
|
||||
getDailyEnergyDataList,
|
||||
} from '@/api/ems/site'
|
||||
import AddDataCorrection from './AddDataCorrection.vue'
|
||||
|
||||
export default {
|
||||
name: 'DailyEnergyDataTab',
|
||||
components: { AddDataCorrection },
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: {
|
||||
siteId: '',
|
||||
dataDate: '',
|
||||
},
|
||||
tableData: [],
|
||||
pageSize: 10,
|
||||
pageNum: 1,
|
||||
totalSize: 0,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.query.siteId'(newSiteId) {
|
||||
const normalizedSiteId = this.normalizeSiteId(newSiteId)
|
||||
if (normalizedSiteId === this.form.siteId) {
|
||||
return
|
||||
}
|
||||
this.form.siteId = normalizedSiteId
|
||||
this.onSearch()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.form.siteId = this.normalizeSiteId(this.$route.query.siteId)
|
||||
this.getData()
|
||||
},
|
||||
methods: {
|
||||
normalizeSiteId(siteId) {
|
||||
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.pageSize = val
|
||||
this.pageNum = 1
|
||||
this.getData()
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.pageNum = val
|
||||
this.getData()
|
||||
},
|
||||
onSearch() {
|
||||
this.pageNum = 1
|
||||
this.getData()
|
||||
},
|
||||
onReset() {
|
||||
this.form = {
|
||||
siteId: this.form.siteId,
|
||||
dataDate: '',
|
||||
}
|
||||
this.pageNum = 1
|
||||
this.getData()
|
||||
},
|
||||
getData() {
|
||||
if (!this.form.siteId) {
|
||||
this.tableData = []
|
||||
this.totalSize = 0
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
const params = {
|
||||
pageNum: this.pageNum,
|
||||
pageSize: this.pageSize,
|
||||
siteId: this.form.siteId,
|
||||
}
|
||||
if (this.form.dataDate) {
|
||||
params.dataDate = this.form.dataDate
|
||||
}
|
||||
getDailyEnergyDataList(params)
|
||||
.then((res) => {
|
||||
this.tableData = res?.rows || []
|
||||
this.totalSize = res?.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
openDialog(id) {
|
||||
this.$refs.addDataCorrection.showDialog(id, this.form.siteId)
|
||||
},
|
||||
handleDelete(row) {
|
||||
this.$confirm('确认要删除该条数据吗?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
showClose: false,
|
||||
closeOnClickModal: false,
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true
|
||||
deleteDailyEnergyData(row.id)
|
||||
.then((res) => {
|
||||
if (res?.code === 200) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
instance.confirmButtonLoading = false
|
||||
})
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success('删除成功!')
|
||||
this.getData()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
30
src/views/ems/site/dataCorrection/index.vue
Normal file
30
src/views/ems/site/dataCorrection/index.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="ems-dashboard-editor-container" style="background-color: #ffffff">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="充放电量修正" name="energy" lazy>
|
||||
<daily-energy-data-tab />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="充放电收益修正" name="charge" lazy>
|
||||
<daily-charge-data-tab />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DailyChargeDataTab from './DailyChargeDataTab.vue'
|
||||
import DailyEnergyDataTab from './DailyEnergyDataTab.vue'
|
||||
|
||||
export default {
|
||||
name: 'DataCorrection',
|
||||
components: { DailyEnergyDataTab, DailyChargeDataTab },
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'energy',
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@ -49,7 +49,7 @@
|
||||
新增{{ activePointTab === 'calc' ? '计算点' : '数据点' }}
|
||||
</el-button>
|
||||
<el-button type="warning" :disabled="!hasValidSiteId(queryParams.siteId)" @click="openImportPointDialog">导入点位</el-button>
|
||||
<el-checkbox v-model="overwrite" style="margin-left: 12px;">覆盖已存在点位数据</el-checkbox>
|
||||
<el-button :disabled="!tableData.length" @click="handleExport">导出点位</el-button>
|
||||
<input
|
||||
ref="csvInput"
|
||||
type="file"
|
||||
@ -203,7 +203,7 @@
|
||||
placeholder="例如:voltageA * currentA + powerLoss"
|
||||
/>
|
||||
<div class="calc-expression-tips">
|
||||
示例:A + B * 2;(A + B) / C;voltageA * currentA + powerLoss
|
||||
示例:A + B * 2;IF(A > B, A, B);DAY_DIFF(POINT4092);MONTH_DIFF(POINT4092);HOUR_DIFF(POINT4092)
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@ -271,6 +271,13 @@
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button
|
||||
v-if="form.pointType === 'calc'"
|
||||
:loading="generateRecentLoading"
|
||||
@click="handleGenerateRecent7Days"
|
||||
>
|
||||
生成最近7天数据
|
||||
</el-button>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
@ -343,7 +350,8 @@ import {
|
||||
deletePointMatch,
|
||||
getDeviceListBySiteAndCategory,
|
||||
getPointConfigLatestValues,
|
||||
getPointConfigCurve
|
||||
getPointConfigCurve,
|
||||
generatePointConfigRecent7Days
|
||||
} from '@/api/ems/site'
|
||||
|
||||
export default {
|
||||
@ -354,7 +362,6 @@ export default {
|
||||
deviceCategoryList: [],
|
||||
tableData: [],
|
||||
total: 0,
|
||||
overwrite: false,
|
||||
activePointTab: 'data',
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
@ -371,6 +378,7 @@ export default {
|
||||
curveDialogVisible: false,
|
||||
curveDialogTitle: '曲线',
|
||||
curveLoading: false,
|
||||
generateRecentLoading: false,
|
||||
curveChart: null,
|
||||
curveCustomRange: [],
|
||||
curveQuery: {
|
||||
@ -523,80 +531,29 @@ export default {
|
||||
return
|
||||
}
|
||||
const points = this.tableData
|
||||
.filter(item => item.pointType === 'data' && item.siteId && item.deviceId && item.dataKey)
|
||||
.filter(item => item.siteId && item.pointId)
|
||||
.map(item => ({
|
||||
siteId: item.siteId,
|
||||
deviceId: item.deviceId,
|
||||
dataKey: item.dataKey
|
||||
pointId: item.pointId
|
||||
}))
|
||||
if (!points.length) {
|
||||
const calcRows = this.tableData.filter(item => item.pointType !== 'data')
|
||||
if (!calcRows.length) {
|
||||
this.tableData = this.applyCalcLatestValues(this.tableData)
|
||||
return
|
||||
}
|
||||
this.loadCalcDependencyRows(calcRows).then(depRows => {
|
||||
if (!depRows.length) {
|
||||
this.tableData = this.applyCalcLatestValues(this.tableData)
|
||||
return
|
||||
}
|
||||
const depPoints = depRows.map(item => ({
|
||||
siteId: item.siteId,
|
||||
deviceId: item.deviceId,
|
||||
dataKey: item.dataKey
|
||||
}))
|
||||
return getPointConfigLatestValues({ points: depPoints }).then(response => {
|
||||
const latestList = response?.data || []
|
||||
const latestMap = latestList.reduce((acc, item) => {
|
||||
const key = `${item.siteId || ''}__${item.deviceId || ''}__${item.dataKey || ''}`
|
||||
acc[key] = item.pointValue
|
||||
return acc
|
||||
}, {})
|
||||
const depRowsWithLatest = depRows.map(row => {
|
||||
const key = `${row.siteId || ''}__${row.deviceId || ''}__${row.dataKey || ''}`
|
||||
const latestValue = latestMap[key]
|
||||
return {
|
||||
...row,
|
||||
latestValue: (latestValue === null || latestValue === undefined || latestValue === '') ? '-' : latestValue
|
||||
}
|
||||
})
|
||||
const mergedRows = this.applyCalcLatestValues([...depRowsWithLatest, ...this.tableData])
|
||||
const calcLatestMap = mergedRows
|
||||
.filter(item => item.pointType !== 'data')
|
||||
.reduce((acc, item) => {
|
||||
acc[this.getCalcRowKey(item)] = item.latestValue
|
||||
return acc
|
||||
}, {})
|
||||
this.tableData = this.tableData.map(row => {
|
||||
if (row.pointType === 'data') return row
|
||||
const nextLatest = calcLatestMap[this.getCalcRowKey(row)]
|
||||
return {
|
||||
...row,
|
||||
latestValue: (nextLatest === null || nextLatest === undefined || nextLatest === '') ? '-' : nextLatest
|
||||
}
|
||||
})
|
||||
})
|
||||
}).catch(() => {
|
||||
this.tableData = this.applyCalcLatestValues(this.tableData)
|
||||
})
|
||||
return
|
||||
}
|
||||
getPointConfigLatestValues({ points }).then(response => {
|
||||
const latestList = response?.data || []
|
||||
const latestMap = latestList.reduce((acc, item) => {
|
||||
const key = `${item.siteId || ''}__${item.deviceId || ''}__${item.dataKey || ''}`
|
||||
const key = `${item.siteId || ''}__${item.pointId || ''}`
|
||||
acc[key] = item.pointValue
|
||||
return acc
|
||||
}, {})
|
||||
const withDataLatestValue = this.tableData.map(row => {
|
||||
const key = `${row.siteId || ''}__${row.deviceId || ''}__${row.dataKey || ''}`
|
||||
this.tableData = this.tableData.map(row => {
|
||||
const key = `${row.siteId || ''}__${row.pointId || ''}`
|
||||
const latestValue = latestMap[key]
|
||||
return {
|
||||
...row,
|
||||
latestValue: (latestValue === null || latestValue === undefined || latestValue === '') ? '-' : latestValue
|
||||
}
|
||||
})
|
||||
this.tableData = this.applyCalcLatestValues(withDataLatestValue)
|
||||
}).catch(() => {})
|
||||
},
|
||||
getCalcRowKey(row) {
|
||||
@ -607,8 +564,8 @@ export default {
|
||||
if (!expr) {
|
||||
return []
|
||||
}
|
||||
if (!/^[0-9A-Za-z_+\-*/().\s]+$/.test(expr)) {
|
||||
throw new Error('计算表达式仅支持四则运算和括号')
|
||||
if (!/^[0-9A-Za-z_+\-*/().,?:<>=!&|\s]+$/.test(expr)) {
|
||||
throw new Error('计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法')
|
||||
}
|
||||
const tokens = []
|
||||
let index = 0
|
||||
@ -657,7 +614,20 @@ export default {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
if (['+', '-', '*', '/'].includes(ch)) {
|
||||
if (ch === ',' || ch === '?' || ch === ':') {
|
||||
tokens.push({ type: ch, value: ch })
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
if (index + 1 < expr.length) {
|
||||
const twoChars = expr.slice(index, index + 2)
|
||||
if (['&&', '||', '>=', '<=', '==', '!='].includes(twoChars)) {
|
||||
tokens.push({ type: 'operator', value: twoChars })
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (['+', '-', '*', '/', '>', '<', '!'].includes(ch)) {
|
||||
tokens.push({ type: 'operator', value: ch })
|
||||
index += 1
|
||||
continue
|
||||
@ -668,7 +638,7 @@ export default {
|
||||
return tokens
|
||||
},
|
||||
extractExpressionTokens(expression) {
|
||||
const reserved = new Set(['IF'])
|
||||
const reserved = new Set(['IF', 'DAY_DIFF', 'MONTH_DIFF', 'HOUR_DIFF'])
|
||||
try {
|
||||
const tokens = this.tokenizeCalcExpression(expression)
|
||||
const identifiers = tokens
|
||||
@ -874,6 +844,24 @@ export default {
|
||||
? evaluateNode(node.trueNode)
|
||||
: evaluateNode(node.falseNode)
|
||||
}
|
||||
if (node.type === 'function') {
|
||||
const fnName = String(node.name || '').toUpperCase()
|
||||
if (fnName === 'DAY_DIFF') {
|
||||
if (!Array.isArray(node.args) || node.args.length !== 1) {
|
||||
throw new Error('DAY_DIFF函数参数数量错误,需1个参数')
|
||||
}
|
||||
// 单参数模式依赖后端历史基线,前端仅做语法校验
|
||||
throw new Error('DAY_DIFF单参数仅支持后端计算')
|
||||
}
|
||||
if (fnName === 'MONTH_DIFF' || fnName === 'HOUR_DIFF') {
|
||||
if (!Array.isArray(node.args) || node.args.length !== 1) {
|
||||
throw new Error(`${fnName}函数参数数量错误,需1个参数`)
|
||||
}
|
||||
// 单参数模式依赖后端历史基线,前端仅做语法校验
|
||||
throw new Error(`${fnName}单参数仅支持后端计算`)
|
||||
}
|
||||
throw new Error(`不支持的函数: ${node.name}`)
|
||||
}
|
||||
throw new Error(`不支持的节点类型: ${node.type}`)
|
||||
}
|
||||
|
||||
@ -1009,21 +997,85 @@ export default {
|
||||
if (matchType('identifier')) {
|
||||
const identifier = String(token.value || '')
|
||||
if (matchType('(')) {
|
||||
if (identifier.toUpperCase() !== 'IF') {
|
||||
throw new Error(`不支持的函数: ${identifier}`)
|
||||
const functionName = identifier.toUpperCase()
|
||||
if (functionName === 'IF') {
|
||||
const condition = parseExpression()
|
||||
expectType(',', 'IF函数缺少第1个逗号')
|
||||
const trueValue = parseExpression()
|
||||
expectType(',', 'IF函数缺少第2个逗号')
|
||||
const falseValue = parseExpression()
|
||||
expectType(')', 'IF函数缺少右括号')
|
||||
return {
|
||||
type: 'ternary',
|
||||
condition,
|
||||
trueNode: trueValue,
|
||||
falseNode: falseValue
|
||||
}
|
||||
}
|
||||
const condition = parseExpression()
|
||||
expectType(',', 'IF函数缺少第1个逗号')
|
||||
const trueValue = parseExpression()
|
||||
expectType(',', 'IF函数缺少第2个逗号')
|
||||
const falseValue = parseExpression()
|
||||
expectType(')', 'IF函数缺少右括号')
|
||||
return {
|
||||
type: 'ternary',
|
||||
condition,
|
||||
trueNode: trueValue,
|
||||
falseNode: falseValue
|
||||
if (functionName === 'DAY_DIFF') {
|
||||
const args = []
|
||||
if (!matchType(')')) {
|
||||
while (true) {
|
||||
args.push(parseExpression())
|
||||
if (matchType(',')) {
|
||||
continue
|
||||
}
|
||||
expectType(')', 'DAY_DIFF函数缺少右括号')
|
||||
break
|
||||
}
|
||||
}
|
||||
if (args.length !== 1) {
|
||||
throw new Error('DAY_DIFF函数参数数量错误,需1个参数')
|
||||
}
|
||||
return {
|
||||
type: 'function',
|
||||
name: functionName,
|
||||
args
|
||||
}
|
||||
}
|
||||
if (functionName === 'MONTH_DIFF') {
|
||||
const args = []
|
||||
if (!matchType(')')) {
|
||||
while (true) {
|
||||
args.push(parseExpression())
|
||||
if (matchType(',')) {
|
||||
continue
|
||||
}
|
||||
expectType(')', 'MONTH_DIFF函数缺少右括号')
|
||||
break
|
||||
}
|
||||
}
|
||||
if (args.length !== 1) {
|
||||
throw new Error('MONTH_DIFF函数参数数量错误,需1个参数')
|
||||
}
|
||||
return {
|
||||
type: 'function',
|
||||
name: functionName,
|
||||
args
|
||||
}
|
||||
}
|
||||
if (functionName === 'HOUR_DIFF') {
|
||||
const args = []
|
||||
if (!matchType(')')) {
|
||||
while (true) {
|
||||
args.push(parseExpression())
|
||||
if (matchType(',')) {
|
||||
continue
|
||||
}
|
||||
expectType(')', 'HOUR_DIFF函数缺少右括号')
|
||||
break
|
||||
}
|
||||
}
|
||||
if (args.length !== 1) {
|
||||
throw new Error('HOUR_DIFF函数参数数量错误,需1个参数')
|
||||
}
|
||||
return {
|
||||
type: 'function',
|
||||
name: functionName,
|
||||
args
|
||||
}
|
||||
}
|
||||
throw new Error(`不支持的函数: ${identifier}`)
|
||||
}
|
||||
return { type: 'variable', name: identifier }
|
||||
}
|
||||
@ -1115,7 +1167,6 @@ export default {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('siteId', siteId)
|
||||
formData.append('overwrite', String(!!this.overwrite))
|
||||
this.loading = true
|
||||
return importPointConfigCsv(formData)
|
||||
}).then(response => {
|
||||
@ -1127,6 +1178,72 @@ export default {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
escapeCsvCell(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
const text = String(value)
|
||||
if (/[",\n\r]/.test(text)) {
|
||||
return `"${text.replace(/"/g, '""')}"`
|
||||
}
|
||||
return text
|
||||
},
|
||||
handleExport() {
|
||||
if (!this.tableData.length) {
|
||||
this.$message.warning('暂无可导出数据')
|
||||
return
|
||||
}
|
||||
const headers = [
|
||||
'站点ID',
|
||||
'点位ID',
|
||||
'点位名',
|
||||
'设备类型',
|
||||
'设备ID',
|
||||
'数据键',
|
||||
'点位描述',
|
||||
'寄存器地址',
|
||||
'A系数',
|
||||
'K系数',
|
||||
'B系数',
|
||||
'位偏移',
|
||||
'类型',
|
||||
'计算表达式',
|
||||
'最新值',
|
||||
'单位'
|
||||
]
|
||||
const rows = this.tableData.map(item => {
|
||||
return [
|
||||
item.siteId || '',
|
||||
item.pointId || '',
|
||||
item.pointName || '',
|
||||
item.deviceCategory || '',
|
||||
item.deviceId || '',
|
||||
item.dataKey || '',
|
||||
item.pointDesc || '',
|
||||
item.registerAddress || '',
|
||||
item.dataA === undefined || item.dataA === null ? '' : item.dataA,
|
||||
item.dataK === undefined || item.dataK === null ? '' : item.dataK,
|
||||
item.dataB === undefined || item.dataB === null ? '' : item.dataB,
|
||||
item.dataBit === undefined || item.dataBit === null ? '' : item.dataBit,
|
||||
item.pointType === 'calc' ? '计算点' : '数据点',
|
||||
item.pointType === 'calc' ? (item.calcExpression || '') : '',
|
||||
item.latestValue === undefined || item.latestValue === null ? '' : item.latestValue,
|
||||
item.dataUnit || ''
|
||||
].map(cell => this.escapeCsvCell(cell))
|
||||
})
|
||||
const csvText = [headers.map(cell => this.escapeCsvCell(cell)).join(','), ...rows.map(row => row.join(','))].join('\n')
|
||||
const blob = new Blob([`\uFEFF${csvText}`], { type: 'text/csv;charset=utf-8;' })
|
||||
const fileName = `point_list_${new Date().getTime()}.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('导出成功')
|
||||
},
|
||||
resetForm() {
|
||||
const querySiteId = this.hasValidSiteId(this.queryParams.siteId) ? String(this.queryParams.siteId).trim() : ''
|
||||
this.form = {
|
||||
@ -1444,6 +1561,33 @@ export default {
|
||||
}
|
||||
this.curveLoading = false
|
||||
},
|
||||
handleGenerateRecent7Days() {
|
||||
if (this.form.pointType !== 'calc') {
|
||||
return
|
||||
}
|
||||
const siteId = String(this.form.siteId || '').trim()
|
||||
const pointId = String(this.form.pointId || '').trim()
|
||||
if (!siteId) {
|
||||
this.$message.warning('站点ID不能为空')
|
||||
return
|
||||
}
|
||||
if (!pointId) {
|
||||
this.$message.warning('请先输入点位ID')
|
||||
return
|
||||
}
|
||||
this.generateRecentLoading = true
|
||||
generatePointConfigRecent7Days({
|
||||
siteId,
|
||||
pointId,
|
||||
deviceId: ''
|
||||
}).then(response => {
|
||||
this.$message.success(response?.msg || '最近7天数据生成成功')
|
||||
}).catch(err => {
|
||||
this.$message.error(err?.message || '最近7天数据生成失败')
|
||||
}).finally(() => {
|
||||
this.generateRecentLoading = false
|
||||
})
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.pointForm.validate(valid => {
|
||||
if (!valid) return
|
||||
|
||||
@ -262,7 +262,8 @@
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import { validText } from "@/utils/validate";
|
||||
import { addProtectPlan, getPointMatchList, getProtectPlan, updateProtectPlan } from "@/api/ems/site";
|
||||
import { addProtectPlan, getProtectPlan, updateProtectPlan } from "@/api/ems/site";
|
||||
import { pointFuzzyQuery } from "@/api/ems/search";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@ -308,6 +309,8 @@ export default {
|
||||
{ validator: validateText, trigger: "blur" },
|
||||
],
|
||||
},
|
||||
sitePointOptionsCache: {},
|
||||
sitePointRequestId: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -370,22 +373,37 @@ export default {
|
||||
return `${pointId || "-"}-${pointName || "-"}(${pointDesc || "-"})`;
|
||||
},
|
||||
normalizePointOptions(data = []) {
|
||||
return (data || []).map((item) => {
|
||||
const pointId = item?.pointId || item?.point || item?.value || "";
|
||||
const pointName = item?.pointName || pointId || "";
|
||||
const pointDesc = item?.pointDesc || "";
|
||||
return {
|
||||
value: pointId,
|
||||
label: this.formatPointLabel({ pointId, pointName, pointDesc }),
|
||||
pointId,
|
||||
pointName,
|
||||
pointDesc,
|
||||
deviceId: item?.deviceId || "",
|
||||
deviceName: item?.deviceName || "",
|
||||
deviceCategory: item?.deviceCategory || "",
|
||||
categoryName: item?.categoryName || item?.deviceCategory || "",
|
||||
};
|
||||
});
|
||||
return (data || [])
|
||||
.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return {
|
||||
value: item,
|
||||
label: this.formatPointLabel({ pointName: item }),
|
||||
pointId: "",
|
||||
pointName: item,
|
||||
pointDesc: "",
|
||||
deviceId: "",
|
||||
deviceName: "",
|
||||
deviceCategory: "",
|
||||
categoryName: "",
|
||||
};
|
||||
}
|
||||
const pointId = item?.pointId || "";
|
||||
const pointName = item?.pointName || item?.value || "";
|
||||
const pointDesc = item?.pointDesc || "";
|
||||
return {
|
||||
value: pointId || pointName,
|
||||
label: this.formatPointLabel({ pointId, pointName, pointDesc }),
|
||||
pointId,
|
||||
pointName,
|
||||
pointDesc,
|
||||
deviceId: item?.deviceId || "",
|
||||
deviceName: item?.deviceName || "",
|
||||
deviceCategory: item?.deviceCategory || "",
|
||||
categoryName: item?.categoryName || item?.deviceCategory || "",
|
||||
};
|
||||
})
|
||||
.filter((item) => item.value);
|
||||
},
|
||||
enhancePointRow(row = {}) {
|
||||
const nextRow = Object.assign(
|
||||
@ -398,7 +416,6 @@ export default {
|
||||
pointName: "",
|
||||
pointOptions: [],
|
||||
pointOptionsCache: {},
|
||||
pointRequestId: 0,
|
||||
pointLoading: false,
|
||||
},
|
||||
row || {}
|
||||
@ -523,67 +540,119 @@ export default {
|
||||
const row = this.getRow(type, index);
|
||||
if (!row) return;
|
||||
const normalized = this.normalizePointOptions(data);
|
||||
const selectedPoint = row.point
|
||||
? this.normalizePointOptions([
|
||||
{
|
||||
const selected =
|
||||
row.point
|
||||
? (row.pointOptions || []).find((item) => item?.value === row.point) || {
|
||||
value: row.point,
|
||||
label: this.formatPointLabel({ pointId: row.point }),
|
||||
pointId: row.point,
|
||||
pointName: row.pointName || row.point,
|
||||
pointName: row.pointName || "",
|
||||
pointDesc: "",
|
||||
deviceId: row.deviceId || "",
|
||||
deviceName: row.deviceName || "",
|
||||
deviceCategory: row.deviceCategory || "",
|
||||
categoryName: row.categoryName || "",
|
||||
},
|
||||
])
|
||||
: [];
|
||||
const merged = [...normalized, ...(row.pointOptions || []), ...selectedPoint];
|
||||
const optionMap = {};
|
||||
const uniqueOptions = merged.filter((item) => {
|
||||
if (!item?.value || optionMap[item.value]) return false;
|
||||
optionMap[item.value] = true;
|
||||
}
|
||||
: null;
|
||||
const nextOptions = selected ? [...normalized, selected] : normalized;
|
||||
const seen = {};
|
||||
const uniqueOptions = nextOptions.filter((item) => {
|
||||
if (!item?.value || seen[item.value]) return false;
|
||||
seen[item.value] = true;
|
||||
return true;
|
||||
});
|
||||
this.setRow(type, index, Object.assign({}, row, { pointOptions: uniqueOptions }));
|
||||
},
|
||||
fetchPointOptions(index, type, query = "", { force = false } = {}) {
|
||||
const row = this.getRow(type, index);
|
||||
if (!row || !this.formData.siteId) return Promise.resolve([]);
|
||||
const normalizedQuery = String(query || "").trim();
|
||||
const cacheKey = `${this.formData.siteId}_${normalizedQuery}`;
|
||||
if (!force && row.pointOptionsCache?.[cacheKey]) {
|
||||
this.setPointOptions(index, type, row.pointOptionsCache[cacheKey]);
|
||||
return Promise.resolve(row.pointOptionsCache[cacheKey]);
|
||||
getPointCacheKey(query = "") {
|
||||
return `${this.formData.siteId || ""}_${String(query || "").trim()}`;
|
||||
},
|
||||
getSitePointCacheKey() {
|
||||
return String(this.formData.siteId || "").trim();
|
||||
},
|
||||
fuzzyFilterPointOptions(options = [], query = "") {
|
||||
const keyword = String(query || "").trim().toLowerCase();
|
||||
if (!keyword) return options;
|
||||
return (options || []).filter((item) => {
|
||||
const marker = `${item?.pointId || ""} ${item?.pointName || ""} ${item?.pointDesc || ""} ${
|
||||
item?.deviceId || ""
|
||||
} ${item?.deviceName || ""} ${item?.categoryName || ""}`.toLowerCase();
|
||||
return marker.includes(keyword);
|
||||
});
|
||||
},
|
||||
applyPointOptionsToType(type, pointOptions = []) {
|
||||
const rows = this[type] || [];
|
||||
const cacheKey = this.getPointCacheKey("");
|
||||
rows.forEach((row, index) => {
|
||||
const latest = this.getRow(type, index);
|
||||
if (!latest) return;
|
||||
const nextCache = Object.assign({}, latest.pointOptionsCache || {}, { [cacheKey]: pointOptions });
|
||||
this.setRow(type, index, Object.assign({}, latest, { pointOptionsCache: nextCache }));
|
||||
this.setPointOptions(index, type, pointOptions);
|
||||
});
|
||||
},
|
||||
applyPointOptionsToGroups(pointOptions = []) {
|
||||
this.applyPointOptionsToType("faultProtectionSettings", pointOptions);
|
||||
this.applyPointOptionsToType("releaseProtectionSettings", pointOptions);
|
||||
},
|
||||
loadSitePointOptions({ force = false } = {}) {
|
||||
const siteCacheKey = this.getSitePointCacheKey();
|
||||
if (!siteCacheKey) return Promise.resolve([]);
|
||||
if (!force && this.sitePointOptionsCache[siteCacheKey]) {
|
||||
const cached = this.sitePointOptionsCache[siteCacheKey];
|
||||
this.applyPointOptionsToGroups(cached);
|
||||
return Promise.resolve(cached);
|
||||
}
|
||||
const requestId = Number(row.pointRequestId || 0) + 1;
|
||||
this.setRow(type, index, Object.assign({}, row, { pointRequestId: requestId, pointLoading: true }));
|
||||
return getPointMatchList({
|
||||
siteId: this.formData.siteId,
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
pointId: normalizedQuery,
|
||||
pointDesc: normalizedQuery,
|
||||
|
||||
const requestId = ++this.sitePointRequestId;
|
||||
["faultProtectionSettings", "releaseProtectionSettings"].forEach((type) => {
|
||||
(this[type] || []).forEach((row, index) => {
|
||||
this.setRow(type, index, Object.assign({}, row, { pointLoading: true }));
|
||||
});
|
||||
});
|
||||
|
||||
return pointFuzzyQuery({
|
||||
siteIds: [this.formData.siteId],
|
||||
pointName: "",
|
||||
})
|
||||
.then((response) => {
|
||||
const latestRow = this.getRow(type, index);
|
||||
if (!latestRow || latestRow.pointRequestId !== requestId) return [];
|
||||
const result = response?.rows || [];
|
||||
const cache = Object.assign({}, latestRow.pointOptionsCache || {}, { [cacheKey]: result });
|
||||
this.setRow(type, index, Object.assign({}, latestRow, { pointOptionsCache: cache }));
|
||||
this.setPointOptions(index, type, result);
|
||||
return result;
|
||||
if (requestId !== this.sitePointRequestId) return [];
|
||||
const data = this.normalizePointOptions(response?.data || []);
|
||||
this.sitePointOptionsCache[siteCacheKey] = data;
|
||||
this.applyPointOptionsToGroups(data);
|
||||
return data;
|
||||
})
|
||||
.finally(() => {
|
||||
const latestRow = this.getRow(type, index);
|
||||
if (!latestRow || latestRow.pointRequestId !== requestId) return;
|
||||
this.setRow(type, index, Object.assign({}, latestRow, { pointLoading: false }));
|
||||
if (requestId !== this.sitePointRequestId) return;
|
||||
["faultProtectionSettings", "releaseProtectionSettings"].forEach((type) => {
|
||||
(this[type] || []).forEach((row, index) => {
|
||||
const latest = this.getRow(type, index);
|
||||
if (!latest) return;
|
||||
this.setRow(type, index, Object.assign({}, latest, { pointLoading: false }));
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
remotePointSearch(index, type, query) {
|
||||
this.fetchPointOptions(index, type, query);
|
||||
const row = this.getRow(type, index);
|
||||
if (!row || !this.formData.siteId) return;
|
||||
const baseCacheKey = this.getPointCacheKey("");
|
||||
const baseOptions = row.pointOptionsCache?.[baseCacheKey];
|
||||
if (!baseOptions) {
|
||||
this.loadSitePointOptions().then(() => {
|
||||
const latestRow = this.getRow(type, index);
|
||||
if (!latestRow) return;
|
||||
const options = latestRow.pointOptionsCache?.[baseCacheKey] || [];
|
||||
const localFiltered = this.fuzzyFilterPointOptions(options, query);
|
||||
this.setPointOptions(index, type, localFiltered);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const localFiltered = this.fuzzyFilterPointOptions(baseOptions, query);
|
||||
this.setPointOptions(index, type, localFiltered);
|
||||
},
|
||||
handlePointDropdownVisible(index, type, visible) {
|
||||
if (!visible) return;
|
||||
this.fetchPointOptions(index, type, "");
|
||||
this.loadSitePointOptions();
|
||||
},
|
||||
handlePointChange(index, type, value) {
|
||||
const row = this.getRow(type, index);
|
||||
@ -679,11 +748,6 @@ export default {
|
||||
}
|
||||
}
|
||||
if (!faultSettingsValidateStatus) break;
|
||||
if (!row.deviceId) {
|
||||
this.$message.error(`请选择故障保护第${i + 1}行的点位(需从点位列表中选择)`);
|
||||
faultSettingsValidateStatus = false;
|
||||
break;
|
||||
}
|
||||
if (faultProtectionSettings[i + 1] && !row.relationNext) {
|
||||
this.$message.error(getToastMsg("relationNext", "faultProtectionSettings", i + 1));
|
||||
faultSettingsValidateStatus = false;
|
||||
@ -703,11 +767,6 @@ export default {
|
||||
}
|
||||
}
|
||||
if (!releaseSettingsValidateStatus) break;
|
||||
if (!row.deviceId) {
|
||||
this.$message.error(`请选择释放保护第${i + 1}行的点位(需从点位列表中选择)`);
|
||||
releaseSettingsValidateStatus = false;
|
||||
break;
|
||||
}
|
||||
if (releaseProtectionSettings[i + 1] && !row.relationNext) {
|
||||
this.$message.error(getToastMsg("relationNext", "releaseProtectionSettings", i + 1));
|
||||
releaseSettingsValidateStatus = false;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -63,10 +63,17 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="120"
|
||||
width="220"
|
||||
fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="small" @click="openEditDialog(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
:loading="syncingSiteId === scope.row.siteId"
|
||||
:disabled="syncingSiteId === scope.row.siteId"
|
||||
@click="syncWeather(scope.row)"
|
||||
>同步天气</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -164,7 +171,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {addSite, getSiteInfoList, updateSite} from '@/api/ems/site'
|
||||
import {addSite, getSiteInfoList, syncSiteWeatherByDateRange, updateSite} from '@/api/ems/site'
|
||||
import { formatDate } from '@/filters/ems'
|
||||
|
||||
const emptySiteForm = () => ({
|
||||
@ -199,6 +206,7 @@ export default {
|
||||
pageSize: 10,
|
||||
pageNum: 1,
|
||||
totalSize: 0,
|
||||
syncingSiteId: '',
|
||||
dialogVisible: false,
|
||||
isEdit: false,
|
||||
siteForm: emptySiteForm(),
|
||||
@ -239,6 +247,38 @@ export default {
|
||||
this.pageNum = 1
|
||||
this.getData()
|
||||
},
|
||||
getSyncDateRange() {
|
||||
const now = new Date()
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
return [formatDate(monthStart), formatDate(now)]
|
||||
},
|
||||
syncWeather(row) {
|
||||
const siteId = row?.siteId
|
||||
if (!siteId) {
|
||||
this.$message.warning('站点ID为空,无法同步天气')
|
||||
return
|
||||
}
|
||||
const [startTime, endTime] = this.getSyncDateRange()
|
||||
this.$confirm(`将同步站点 ${siteId} 在 ${startTime} 至 ${endTime} 的天气数据,是否继续?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.syncingSiteId = siteId
|
||||
return syncSiteWeatherByDateRange({siteId, startTime, endTime})
|
||||
}).then((response) => {
|
||||
const result = response?.data || {}
|
||||
const successDays = result.successDays ?? 0
|
||||
const totalDays = result.totalDays ?? 0
|
||||
this.$message.success(`天气同步完成(${successDays}/${totalDays}天)`)
|
||||
}).catch((err) => {
|
||||
if (err !== 'cancel') {
|
||||
this.$message.error('天气同步失败')
|
||||
}
|
||||
}).finally(() => {
|
||||
this.syncingSiteId = ''
|
||||
})
|
||||
},
|
||||
getData() {
|
||||
this.loading = true
|
||||
const {siteName, pageNum, pageSize} = this
|
||||
|
||||
Reference in New Issue
Block a user