重构
This commit is contained in:
@ -8,6 +8,14 @@ export function getDzjkHomeView(siteId) {
|
||||
})
|
||||
}
|
||||
|
||||
//获取单个站点总累计运行数据(基于日表)
|
||||
export function getDzjkHomeTotalView(siteId) {
|
||||
return request({
|
||||
url: `/ems/siteMonitor/homeTotalView?siteId=${siteId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 单站监控项目点位配置(供单站监控功能查询)
|
||||
export function getProjectPointMapping(siteId) {
|
||||
return request({
|
||||
@ -738,88 +746,25 @@ export function getLoadNameList(siteId) {
|
||||
|
||||
// 电表报表
|
||||
export function getAmmeterData({siteId, startTime, endTime, pageSize, pageNum}) {
|
||||
const kind = 'day'
|
||||
const aliasMap = {
|
||||
activePeakKwh: 'activePeakKwh',
|
||||
activeHighKwh: 'activeHighKwh',
|
||||
activeFlatKwh: 'activeFlatKwh',
|
||||
activeValleyKwh: 'activeValleyKwh',
|
||||
activeTotalKwh: 'activeTotalKwh',
|
||||
reActivePeakKwh: 'reActivePeakKwh',
|
||||
reActiveHighKwh: 'reActiveHighKwh',
|
||||
reActiveFlatKwh: 'reActiveFlatKwh',
|
||||
reActiveValleyKwh: 'reActiveValleyKwh',
|
||||
reActiveTotalKwh: 'reActiveTotalKwh',
|
||||
effect: 'effect',
|
||||
}
|
||||
return queryMenuPointCurves({
|
||||
return request({
|
||||
url: `/ems/statsReport/getAmmeterDataFromDaily`,
|
||||
method: 'get',
|
||||
params: {
|
||||
siteId,
|
||||
menuCode: 'TJBB_DBBB',
|
||||
startDate: startTime,
|
||||
endDate: endTime,
|
||||
}).then((records) => {
|
||||
const rowMap = new Map()
|
||||
records.forEach((record) => {
|
||||
const alias = aliasMap[record.fieldName]
|
||||
if (!alias) return
|
||||
;(record.curve || []).forEach((point) => {
|
||||
const time = point?.dataTime ? new Date(point.dataTime) : null
|
||||
if (!time || isNaN(time.getTime())) return
|
||||
const label = formatTimeLabelByKind(time, kind)
|
||||
if (!rowMap.has(label)) rowMap.set(label, {dataTime: label})
|
||||
rowMap.get(label)[alias] = normalizePointValue(point?.pointValue)
|
||||
})
|
||||
})
|
||||
const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind)
|
||||
const fullRows = labels.map(label => rowMap.get(label))
|
||||
return {
|
||||
rows: paginateRows(fullRows, pageNum, pageSize),
|
||||
total: fullRows.length,
|
||||
startTime,
|
||||
endTime,
|
||||
pageSize,
|
||||
pageNum,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 电价报表
|
||||
export function getAmmeterRevenueData(data) {
|
||||
const {siteId, startTime, endTime, pageNum, pageSize} = data || {}
|
||||
const kind = 'day'
|
||||
const aliasMap = {
|
||||
activePeakPrice: 'activePeakPrice',
|
||||
activeHighPrice: 'activeHighPrice',
|
||||
activeFlatPrice: 'activeFlatPrice',
|
||||
activeValleyPrice: 'activeValleyPrice',
|
||||
activeTotalPrice: 'activeTotalPrice',
|
||||
reActivePeakPrice: 'reActivePeakPrice',
|
||||
reActiveHighPrice: 'reActiveHighPrice',
|
||||
reActiveFlatPrice: 'reActiveFlatPrice',
|
||||
reActiveValleyPrice: 'reActiveValleyPrice',
|
||||
reActiveTotalPrice: 'reActiveTotalPrice',
|
||||
actualRevenue: 'actualRevenue',
|
||||
}
|
||||
return queryMenuPointCurves({
|
||||
siteId,
|
||||
menuCode: 'TJBB_SYBB',
|
||||
startDate: startTime,
|
||||
endDate: endTime,
|
||||
}).then((records) => {
|
||||
const rowMap = new Map()
|
||||
records.forEach((record) => {
|
||||
const alias = aliasMap[record.fieldName]
|
||||
if (!alias) return
|
||||
;(record.curve || []).forEach((point) => {
|
||||
const time = point?.dataTime ? new Date(point.dataTime) : null
|
||||
if (!time || isNaN(time.getTime())) return
|
||||
const label = formatTimeLabelByKind(time, kind)
|
||||
if (!rowMap.has(label)) rowMap.set(label, {dataTime: label})
|
||||
rowMap.get(label)[alias] = normalizePointValue(point?.pointValue)
|
||||
})
|
||||
})
|
||||
const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind)
|
||||
const fullRows = labels.map(label => rowMap.get(label))
|
||||
return {
|
||||
rows: paginateRows(fullRows, pageNum, pageSize),
|
||||
total: fullRows.length,
|
||||
}
|
||||
return request({
|
||||
url: `/ems/statsReport/getAmmeterRevenueData`,
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,15 @@ export function getSiteInfoList({siteName, startTime, endTime, pageSize, pageNum
|
||||
})
|
||||
}
|
||||
|
||||
// 手动同步站点天气(收益报表)
|
||||
export function syncSiteWeatherByDateRange({siteId, startTime, endTime}) {
|
||||
return request({
|
||||
url: `/ems/statsReport/syncWeatherByDateRange`,
|
||||
method: 'post',
|
||||
params: {siteId, startTime, endTime}
|
||||
})
|
||||
}
|
||||
|
||||
// 新增站点
|
||||
export function addSite(data) {
|
||||
return request({
|
||||
@ -309,6 +318,18 @@ export function getPointConfigCurve(data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 点位配置-生成最近7天数据
|
||||
export function generatePointConfigRecent7Days(data) {
|
||||
return request({
|
||||
url: `/ems/pointConfig/generateRecent7Days`,
|
||||
method: 'post',
|
||||
data,
|
||||
headers: {
|
||||
repeatSubmit: false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算点配置列表
|
||||
export function getPointCalcConfigList(params) {
|
||||
return request({
|
||||
@ -352,6 +373,92 @@ export function deletePointCalcConfig(ids) {
|
||||
})
|
||||
}
|
||||
|
||||
// 数据修正列表(ems_daily_energy_data)
|
||||
export function getDailyEnergyDataList(params) {
|
||||
return request({
|
||||
url: `/ems/dailyEnergyData/list`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 数据修正详情
|
||||
export function getDailyEnergyDataDetail(id) {
|
||||
return request({
|
||||
url: `/ems/dailyEnergyData/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 新增数据修正
|
||||
export function addDailyEnergyData(data) {
|
||||
return request({
|
||||
url: `/ems/dailyEnergyData`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑数据修正
|
||||
export function updateDailyEnergyData(data) {
|
||||
return request({
|
||||
url: `/ems/dailyEnergyData`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除数据修正
|
||||
export function deleteDailyEnergyData(ids) {
|
||||
return request({
|
||||
url: `/ems/dailyEnergyData/${ids}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
// 充放电修正列表(ems_daily_charge_data)
|
||||
export function getDailyChargeDataList(params) {
|
||||
return request({
|
||||
url: `/ems/dailyChargeData/list`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 充放电修正详情
|
||||
export function getDailyChargeDataDetail(id) {
|
||||
return request({
|
||||
url: `/ems/dailyChargeData/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 新增充放电修正
|
||||
export function addDailyChargeData(data) {
|
||||
return request({
|
||||
url: `/ems/dailyChargeData`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑充放电修正
|
||||
export function updateDailyChargeData(data) {
|
||||
return request({
|
||||
url: `/ems/dailyChargeData`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除充放电修正
|
||||
export function deleteDailyChargeData(ids) {
|
||||
return request({
|
||||
url: `/ems/dailyChargeData/${ids}`,
|
||||
method: 'delete',
|
||||
})
|
||||
}
|
||||
|
||||
//mqtt
|
||||
export function getMqttList({pageSize, pageNum, mqttTopic, topicName, siteId}) {
|
||||
return request({
|
||||
|
||||
@ -17,13 +17,13 @@
|
||||
color:#666666;
|
||||
text-align: left;
|
||||
.single-square-box-title{
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
padding-bottom: 12px;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.single-square-box-value{
|
||||
font-size: 26px;
|
||||
line-height: 26px;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.point-loading-icon{
|
||||
@ -33,7 +33,7 @@
|
||||
animation: pointLoadingSpinPulse 1.1s linear infinite;
|
||||
}
|
||||
::v-deep .el-card__body{
|
||||
padding: 12px 10px;
|
||||
padding: 8px 7px;
|
||||
}
|
||||
}
|
||||
@keyframes pointLoadingSpinPulse {
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
class="sjgl-col power-col"
|
||||
>
|
||||
<div class="sjgl-wrapper">
|
||||
<div class="sjgl-title">装机功率(MW)</div>
|
||||
<div class="sjgl-title">装机功率(MWh)</div>
|
||||
<div class="sjgl-value">
|
||||
<i v-if="isBaseInfoLoading" class="el-icon-loading"></i>
|
||||
<span v-else>{{ info.installPower | formatNumber }}</span>
|
||||
@ -54,7 +54,7 @@
|
||||
class="sjgl-col power-col"
|
||||
>
|
||||
<div class="sjgl-wrapper">
|
||||
<div class="sjgl-title">装机容量(MW)</div>
|
||||
<div class="sjgl-title">装机容量(MWh)</div>
|
||||
<div class="sjgl-value">
|
||||
<i v-if="isBaseInfoLoading" class="el-icon-loading"></i>
|
||||
<span v-else>{{ info.installCapacity | formatNumber }}</span>
|
||||
@ -151,7 +151,7 @@
|
||||
<script>
|
||||
import * as echarts from "echarts";
|
||||
import {getSingleSiteBaseInfo} from "@/api/ems/zddt";
|
||||
import {getDzjkHomeView, getProjectDisplayData} from "@/api/ems/dzjk";
|
||||
import {getDzjkHomeTotalView, getProjectDisplayData} from "@/api/ems/dzjk";
|
||||
import {getPointConfigCurve} from "@/api/ems/site";
|
||||
import WeekChart from "./WeekChart.vue";
|
||||
import ActiveChart from "./ActiveChart.vue";
|
||||
@ -482,7 +482,7 @@ export default {
|
||||
this.setRunningInfoLoading(true);
|
||||
}
|
||||
return Promise.all([
|
||||
getDzjkHomeView(this.siteId),
|
||||
getDzjkHomeTotalView(this.siteId),
|
||||
getProjectDisplayData(this.siteId),
|
||||
]).then(([homeResponse, displayResponse]) => {
|
||||
const nextRunningInfo = homeResponse?.data || {};
|
||||
|
||||
@ -149,7 +149,7 @@ export default {
|
||||
exportTable() {
|
||||
if (!this.dateRange?.length) return
|
||||
const [startTime, endTime] = this.dateRange
|
||||
this.download('ems/statsReport/exportAmmeterData', {
|
||||
this.download('ems/statsReport/exportAmmeterDataFromDaily', {
|
||||
siteId: this.siteId,
|
||||
startTime,
|
||||
endTime,
|
||||
|
||||
@ -39,6 +39,17 @@
|
||||
label="日期"
|
||||
min-width="100px" align="center">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="dayType"
|
||||
label="日期类型"
|
||||
min-width="100px" align="center">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="weatherDesc"
|
||||
label="天气情况"
|
||||
min-width="180px"
|
||||
align="center">
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<!--充电量列-->
|
||||
<el-table-column label="充电价格" align="center">
|
||||
@ -187,8 +198,9 @@ export default {
|
||||
const {siteId, pageNum, pageSize} = this
|
||||
const [startTime = '', endTime = ''] = (this.dateRange || [])
|
||||
getAmmeterRevenueData({siteId: siteId, startTime, endTime, pageSize, pageNum}).then(response => {
|
||||
this.tableData = response?.rows || [];
|
||||
const rows = response?.rows || [];
|
||||
this.totalSize = response?.total || 0
|
||||
this.tableData = rows
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
@ -71,6 +71,7 @@
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm">生成图表</el-button>
|
||||
<el-button style="margin-left: 8px" @click="handleExportData">导出数据</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-card
|
||||
@ -102,7 +103,6 @@ import * as echarts from "echarts";
|
||||
import resize from "@/mixins/ems/resize";
|
||||
import {getPointValueList, pointFuzzyQuery} from "@/api/ems/search";
|
||||
import DateTimeSelect from "./DateTimeSelect.vue";
|
||||
import {debounce} from "@/utils";
|
||||
|
||||
export default {
|
||||
name: "Search",
|
||||
@ -132,8 +132,10 @@ export default {
|
||||
queryGroups: [],
|
||||
dataUnit: 1,
|
||||
},
|
||||
debouncedPointSearchMap: {},
|
||||
queryGroupSeed: 0,
|
||||
lastQueryResult: [],
|
||||
sitePointOptionsCache: {},
|
||||
sitePointRequestId: 0,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@ -143,6 +145,103 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getChartColor(index = 0) {
|
||||
const palette = [
|
||||
"#5470C6",
|
||||
"#91CC75",
|
||||
"#FAC858",
|
||||
"#EE6666",
|
||||
"#73C0DE",
|
||||
"#3BA272",
|
||||
"#FC8452",
|
||||
"#9A60B4",
|
||||
"#EA7CCC",
|
||||
];
|
||||
return palette[index % palette.length];
|
||||
},
|
||||
getSelectedPointAxisKeys() {
|
||||
const keys = [];
|
||||
const seen = new Set();
|
||||
(this.form.queryGroups || []).forEach((group) => {
|
||||
const key = String(this.resolveSelectedPointName(group) || group?.pointId || "").trim();
|
||||
if (!key || seen.has(key)) return;
|
||||
seen.add(key);
|
||||
keys.push(key);
|
||||
});
|
||||
return keys;
|
||||
},
|
||||
buildDynamicYAxisConfig(data = []) {
|
||||
const axisIndexMap = {};
|
||||
const yAxis = [];
|
||||
const orderedAxisKeys = [];
|
||||
const seenAxisKeys = new Set();
|
||||
|
||||
const pushAxisKey = (key) => {
|
||||
const normalized = String(key || "").trim();
|
||||
if (!normalized || seenAxisKeys.has(normalized)) return;
|
||||
seenAxisKeys.add(normalized);
|
||||
orderedAxisKeys.push(normalized);
|
||||
};
|
||||
|
||||
this.getSelectedPointAxisKeys().forEach((key) => pushAxisKey(key));
|
||||
|
||||
data.forEach((item) => {
|
||||
const pointId = String(item?.pointId || "").trim();
|
||||
const pointName = String(item?.pointName || "").trim();
|
||||
pushAxisKey(pointName || pointId);
|
||||
|
||||
(item?.deviceList || []).forEach((inner) => {
|
||||
pushAxisKey(inner?.deviceId);
|
||||
});
|
||||
});
|
||||
|
||||
orderedAxisKeys.forEach((pointKey) => {
|
||||
const axisIndex = yAxis.length;
|
||||
const sideIndex = Math.floor(axisIndex / 2);
|
||||
const position = axisIndex % 2 === 0 ? "left" : "right";
|
||||
const color = this.getChartColor(axisIndex);
|
||||
axisIndexMap[pointKey] = axisIndex;
|
||||
|
||||
yAxis.push({
|
||||
type: "value",
|
||||
name: pointKey || `点位${axisIndex + 1}`,
|
||||
position,
|
||||
offset: sideIndex * 55,
|
||||
alignTicks: true,
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {color},
|
||||
},
|
||||
axisLabel: {
|
||||
color,
|
||||
},
|
||||
nameTextStyle: {
|
||||
color,
|
||||
},
|
||||
splitLine: {
|
||||
show: axisIndex === 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (yAxis.length === 0) {
|
||||
yAxis.push({type: "value"});
|
||||
}
|
||||
|
||||
const axisCount = yAxis.length;
|
||||
const leftCount = Math.ceil(axisCount / 2);
|
||||
const rightCount = Math.floor(axisCount / 2);
|
||||
|
||||
return {
|
||||
yAxis,
|
||||
axisIndexMap,
|
||||
grid: {
|
||||
containLabel: true,
|
||||
left: 40 + Math.max(0, leftCount - 1) * 55,
|
||||
right: 40 + Math.max(0, rightCount - 1) * 55,
|
||||
},
|
||||
};
|
||||
},
|
||||
createEmptyQueryGroup(key) {
|
||||
return {
|
||||
key,
|
||||
@ -164,7 +263,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.form.queryGroups.splice(groupIndex, 1);
|
||||
this.debouncedPointSearchMap = {};
|
||||
},
|
||||
getQueryGroup(index) {
|
||||
return this.form.queryGroups[index];
|
||||
@ -209,11 +307,25 @@ export default {
|
||||
},
|
||||
setLineOption(data) {
|
||||
let dataset = [];
|
||||
const axisConfig = this.buildDynamicYAxisConfig(data);
|
||||
data.forEach((item) => {
|
||||
const pointId = String(item?.pointId || "").trim();
|
||||
const pointName = String(item?.pointName || "").trim();
|
||||
const pointKey = pointId || pointName;
|
||||
item.deviceList.forEach((inner) => {
|
||||
const seriesKey = String(inner?.deviceId || "").trim() || pointKey;
|
||||
const yAxisIndex = axisConfig.axisIndexMap[seriesKey] ?? 0;
|
||||
const axisColor = this.getChartColor(yAxisIndex);
|
||||
dataset.push({
|
||||
name: inner.deviceId,
|
||||
type: "line",
|
||||
yAxisIndex,
|
||||
itemStyle: {
|
||||
color: axisColor,
|
||||
},
|
||||
lineStyle: {
|
||||
color: axisColor,
|
||||
},
|
||||
markPoint: {
|
||||
symbolSize: 30,
|
||||
emphasis: {
|
||||
@ -263,9 +375,7 @@ export default {
|
||||
|
||||
this.chart.setOption({
|
||||
legend: {},
|
||||
grid: {
|
||||
containLabel: true,
|
||||
},
|
||||
grid: axisConfig.grid,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
@ -276,9 +386,7 @@ export default {
|
||||
color: "#333333",
|
||||
},
|
||||
xAxis: {type: "category", data: dataset?.[0]?.xdata || []},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
},
|
||||
yAxis: axisConfig.yAxis,
|
||||
dataZoom: [
|
||||
{
|
||||
type: "inside",
|
||||
@ -295,11 +403,22 @@ export default {
|
||||
},
|
||||
setBoxOption(data) {
|
||||
let dataset = [];
|
||||
const axisConfig = this.buildDynamicYAxisConfig(data);
|
||||
data.forEach((item) => {
|
||||
const pointId = String(item?.pointId || "").trim();
|
||||
const pointName = String(item?.pointName || "").trim();
|
||||
const pointKey = pointId || pointName;
|
||||
item.deviceList.forEach((inner) => {
|
||||
const seriesKey = String(inner?.deviceId || "").trim() || pointKey;
|
||||
const yAxisIndex = axisConfig.axisIndexMap[seriesKey] ?? 0;
|
||||
const axisColor = this.getChartColor(yAxisIndex);
|
||||
dataset.push({
|
||||
name: inner.deviceId,
|
||||
type: "boxplot",
|
||||
yAxisIndex,
|
||||
itemStyle: {
|
||||
color: axisColor,
|
||||
},
|
||||
xdata: [],
|
||||
data: [],
|
||||
});
|
||||
@ -314,9 +433,7 @@ export default {
|
||||
|
||||
this.chart.setOption({
|
||||
legend: {},
|
||||
grid: {
|
||||
containLabel: true,
|
||||
},
|
||||
grid: axisConfig.grid,
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: function (params) {
|
||||
@ -332,9 +449,7 @@ export default {
|
||||
color: "#333333",
|
||||
},
|
||||
xAxis: {type: "category", data: dataset?.[0]?.xdata || []},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
},
|
||||
yAxis: axisConfig.yAxis,
|
||||
dataZoom: [
|
||||
{
|
||||
type: "inside",
|
||||
@ -352,9 +467,218 @@ export default {
|
||||
submitForm() {
|
||||
this.getDate();
|
||||
},
|
||||
buildQueryPayload() {
|
||||
const activeGroups = this.form.queryGroups
|
||||
.map((group) => ({group}))
|
||||
.filter(({group}) => !!group.pointId);
|
||||
|
||||
if (activeGroups.length === 0) {
|
||||
this.$message.error("请至少选择1组点位");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.form.siteIds || this.form.siteIds.length === 0) {
|
||||
this.$message.error("请先在顶部选择站点");
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
siteIds,
|
||||
dataUnit,
|
||||
dataRange: [start = "", end = ""],
|
||||
} = this.form;
|
||||
|
||||
if (!start || !end) {
|
||||
this.$message.error("请选择时间");
|
||||
return null;
|
||||
}
|
||||
|
||||
let startDate = start;
|
||||
let endDate = end;
|
||||
if (start && dataUnit === 3) {
|
||||
startDate = `${start} 00:00:00`;
|
||||
}
|
||||
if (end && dataUnit === 3) {
|
||||
endDate = `${end} 00:00:00`;
|
||||
}
|
||||
|
||||
const selectedPoints = [];
|
||||
const pointIdSet = new Set();
|
||||
activeGroups.forEach(({group}) => {
|
||||
const pointId = String(group.pointId || "").trim();
|
||||
if (!pointId || pointIdSet.has(pointId)) return;
|
||||
pointIdSet.add(pointId);
|
||||
selectedPoints.push({
|
||||
pointId,
|
||||
pointName: this.resolveSelectedPointName(group) || pointId,
|
||||
});
|
||||
});
|
||||
|
||||
if (selectedPoints.length === 0) {
|
||||
this.$message.error("请至少选择1组点位");
|
||||
return null;
|
||||
}
|
||||
|
||||
const pointIds = selectedPoints.map((item) => item.pointId);
|
||||
const pointNames = selectedPoints.map((item) => item.pointName);
|
||||
return {
|
||||
siteIds,
|
||||
dataUnit,
|
||||
pointIds,
|
||||
pointNames,
|
||||
pointId: pointIds.join(","),
|
||||
startDate,
|
||||
endDate,
|
||||
};
|
||||
},
|
||||
requestPointData(payload) {
|
||||
return getPointValueList(payload).then((response) => response?.data || []);
|
||||
},
|
||||
resolveExportPointValue(value = {}) {
|
||||
if (value?.pointValue !== undefined && value?.pointValue !== null && value?.pointValue !== "") {
|
||||
return value.pointValue;
|
||||
}
|
||||
if (value?.median !== undefined && value?.median !== null && value?.median !== "") {
|
||||
return value.median;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
normalizeExportRows(data = [], payload = {}) {
|
||||
const pointIds = payload?.pointIds || [];
|
||||
const pointNames = payload?.pointNames || [];
|
||||
const selectedPoints = pointIds
|
||||
.map((pointId, index) => {
|
||||
const normalizedId = String(pointId || "").trim();
|
||||
if (!normalizedId) return null;
|
||||
return {
|
||||
pointId: normalizedId,
|
||||
pointName: String(pointNames[index] || normalizedId).trim(),
|
||||
pointOrder: index,
|
||||
};
|
||||
})
|
||||
.filter((item) => !!item);
|
||||
const selectedPointById = {};
|
||||
const selectedPointByName = {};
|
||||
selectedPoints.forEach((point) => {
|
||||
selectedPointById[point.pointId] = point;
|
||||
if (point.pointName) {
|
||||
selectedPointByName[point.pointName] = point;
|
||||
}
|
||||
});
|
||||
|
||||
const groups = [];
|
||||
const groupMap = {};
|
||||
const rowMap = {};
|
||||
const rowDates = [];
|
||||
data.forEach((item, itemIndex) => {
|
||||
const itemPointId = String(item?.pointId || "").trim();
|
||||
const itemPointName = String(item?.pointName || "").trim();
|
||||
const pointByItemId = itemPointId ? selectedPointById[itemPointId] : null;
|
||||
const pointByItemName = itemPointName ? selectedPointByName[itemPointName] : null;
|
||||
|
||||
(item?.deviceList || []).forEach((device) => {
|
||||
const deviceId = String(device?.deviceId || "").trim();
|
||||
const pointByDeviceId = deviceId ? selectedPointById[deviceId] : null;
|
||||
const pointByDeviceName = deviceId ? selectedPointByName[deviceId] : null;
|
||||
const pointByOrder = selectedPoints[itemIndex] || null;
|
||||
const matchedPoint = pointByItemId || pointByItemName || pointByDeviceId || pointByDeviceName || pointByOrder || null;
|
||||
|
||||
const pointId = itemPointId || matchedPoint?.pointId || "";
|
||||
const pointName = itemPointName || matchedPoint?.pointName || pointId;
|
||||
const pointOrder = matchedPoint?.pointOrder ?? Number.MAX_SAFE_INTEGER;
|
||||
const pointKey = pointId || pointName || `point_${itemIndex + 1}`;
|
||||
const deviceKey = deviceId || "unknown";
|
||||
const groupKey = `${pointKey}__${deviceKey}`;
|
||||
|
||||
if (!groupMap[groupKey]) {
|
||||
groupMap[groupKey] = true;
|
||||
groups.push({
|
||||
groupKey,
|
||||
pointId,
|
||||
pointName,
|
||||
deviceId,
|
||||
pointOrder,
|
||||
});
|
||||
}
|
||||
(device?.pointValueList || []).forEach((value) => {
|
||||
const valueDate = String(value?.valueDate || "").trim();
|
||||
if (!valueDate) return;
|
||||
if (!rowMap[valueDate]) {
|
||||
rowMap[valueDate] = {valueDate};
|
||||
rowDates.push(valueDate);
|
||||
}
|
||||
rowMap[valueDate][groupKey] = this.resolveExportPointValue(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
groups.sort((a, b) => {
|
||||
const ao = a.pointOrder === -1 ? Number.MAX_SAFE_INTEGER : a.pointOrder;
|
||||
const bo = b.pointOrder === -1 ? Number.MAX_SAFE_INTEGER : b.pointOrder;
|
||||
if (ao !== bo) return ao - bo;
|
||||
if (a.pointId !== b.pointId) return a.pointId.localeCompare(b.pointId);
|
||||
return a.deviceId.localeCompare(b.deviceId);
|
||||
});
|
||||
|
||||
return {
|
||||
groups,
|
||||
rows: rowDates.sort().map((valueDate) => rowMap[valueDate]),
|
||||
};
|
||||
},
|
||||
csvCell(value) {
|
||||
const text = value === undefined || value === null ? "" : String(value);
|
||||
return `"${text.replace(/"/g, '""')}"`;
|
||||
},
|
||||
exportRowsToCsv({groups = [], rows = []} = {}) {
|
||||
const headers = ["时间"];
|
||||
groups.forEach((_, index) => {
|
||||
const order = index + 1;
|
||||
headers.push(`点位ID${order}`, `点位名称${order}`, `设备ID${order}`, `点位值${order}`);
|
||||
});
|
||||
const lines = [headers.map((item) => this.csvCell(item)).join(",")];
|
||||
rows.forEach((row) => {
|
||||
const line = [row.valueDate || ""];
|
||||
groups.forEach((group) => {
|
||||
line.push(group.pointId || "", group.pointName || "", group.deviceId || "", row[group.groupKey] ?? "");
|
||||
});
|
||||
lines.push(line.map((item) => this.csvCell(item)).join(","));
|
||||
});
|
||||
const csv = `\uFEFF${lines.join("\n")}`;
|
||||
this.$download.saveAs(new Blob([csv], {type: "text/csv;charset=utf-8;"}), `综合查询数据_${new Date().getTime()}.csv`);
|
||||
},
|
||||
handleExportData() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) return;
|
||||
const payload = this.buildQueryPayload();
|
||||
if (!payload) return;
|
||||
|
||||
this.requestPointData(payload).then((data) => {
|
||||
if (!data || data.length === 0) {
|
||||
this.$message.warning("暂无可导出数据");
|
||||
return;
|
||||
}
|
||||
const exportData = this.normalizeExportRows(data, payload);
|
||||
if (!exportData?.rows?.length || !exportData?.groups?.length) {
|
||||
this.$message.warning("暂无可导出数据");
|
||||
return;
|
||||
}
|
||||
this.exportRowsToCsv(exportData);
|
||||
this.$message.success("导出成功");
|
||||
}).catch((error) => {
|
||||
if (error?.code === "ECONNABORTED") {
|
||||
this.$message.error("查询超时,请缩短时间范围后重试");
|
||||
return;
|
||||
}
|
||||
this.$message.error("导出失败,请稍后重试");
|
||||
});
|
||||
});
|
||||
},
|
||||
getPointCacheKey(query = "") {
|
||||
return `${this.form.siteIds.join(",")}_${query.trim()}`;
|
||||
},
|
||||
getSitePointCacheKey() {
|
||||
return this.form.siteIds.join(",");
|
||||
},
|
||||
formatPointLabel({pointId = "", pointName = "", dataKey = ""} = {}) {
|
||||
return `${pointId || "-"}-${pointName || "-"}(${dataKey || "-"})`;
|
||||
},
|
||||
@ -378,79 +702,93 @@ export default {
|
||||
};
|
||||
}).filter((item) => item.value);
|
||||
},
|
||||
fuzzyFilterPointOptions(options = [], query = "") {
|
||||
const keyword = String(query || "").trim().toLowerCase();
|
||||
if (!keyword) return options;
|
||||
return (options || []).filter((item) => {
|
||||
const marker = `${item?.pointId || ""} ${item?.pointName || ""} ${item?.dataKey || ""} ${item?.pointDesc || ""}`.toLowerCase();
|
||||
return marker.includes(keyword);
|
||||
});
|
||||
},
|
||||
setPointOptions(group, data = []) {
|
||||
const normalized = this.normalizePointOptions(data);
|
||||
const selected = group.pointId
|
||||
? [{
|
||||
? (group.pointOptions || []).find((item) => item?.value === group.pointId)
|
||||
|| {
|
||||
value: group.pointId,
|
||||
label: this.formatPointLabel({pointId: group.pointId}),
|
||||
pointId: group.pointId,
|
||||
pointName: "",
|
||||
pointName: group.selectedPointName || "",
|
||||
dataKey: "",
|
||||
pointDesc: "",
|
||||
}]
|
||||
: [];
|
||||
const merged = [...normalized, ...group.pointOptions, ...selected];
|
||||
const seen = {};
|
||||
group.pointOptions = merged.filter((item) => {
|
||||
if (!item?.value || seen[item.value]) {
|
||||
return false;
|
||||
}
|
||||
: null;
|
||||
const nextOptions = selected ? [...normalized, selected] : normalized;
|
||||
const seen = {};
|
||||
group.pointOptions = nextOptions.filter((item) => {
|
||||
if (!item?.value || seen[item.value]) return false;
|
||||
seen[item.value] = true;
|
||||
return true;
|
||||
});
|
||||
},
|
||||
fetchPointOptions(groupIndex, query = "", {force = false} = {}) {
|
||||
const group = this.getQueryGroup(groupIndex);
|
||||
if (!group || !this.canSelectPoint(group)) return Promise.resolve([]);
|
||||
const normalizedQuery = (query || "").trim();
|
||||
const cacheKey = this.getPointCacheKey(normalizedQuery);
|
||||
if (!force && group.pointOptionsCache[cacheKey]) {
|
||||
this.setPointOptions(group, group.pointOptionsCache[cacheKey]);
|
||||
return Promise.resolve(group.pointOptionsCache[cacheKey]);
|
||||
applyPointOptionsToGroups(pointOptions = []) {
|
||||
(this.form.queryGroups || []).forEach((group) => {
|
||||
if (!group) return;
|
||||
const baseCacheKey = this.getPointCacheKey("");
|
||||
group.pointOptionsCache[baseCacheKey] = pointOptions;
|
||||
this.setPointOptions(group, pointOptions);
|
||||
});
|
||||
},
|
||||
loadSitePointOptions({force = false} = {}) {
|
||||
const siteCacheKey = this.getSitePointCacheKey();
|
||||
if (!siteCacheKey) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const requestId = ++group.pointRequestId;
|
||||
if (!force && this.sitePointOptionsCache[siteCacheKey]) {
|
||||
const cached = this.sitePointOptionsCache[siteCacheKey];
|
||||
this.applyPointOptionsToGroups(cached);
|
||||
return Promise.resolve(cached);
|
||||
}
|
||||
const requestId = ++this.sitePointRequestId;
|
||||
(this.form.queryGroups || []).forEach((group) => {
|
||||
group.pointLoading = true;
|
||||
});
|
||||
return pointFuzzyQuery({
|
||||
siteIds: this.form.siteIds,
|
||||
pointName: normalizedQuery,
|
||||
pointName: "",
|
||||
})
|
||||
.then((response) => {
|
||||
if (requestId !== group.pointRequestId) return [];
|
||||
if (requestId !== this.sitePointRequestId) return [];
|
||||
const data = this.normalizePointOptions(response?.data || []);
|
||||
group.pointOptionsCache[cacheKey] = data;
|
||||
this.setPointOptions(group, data);
|
||||
this.sitePointOptionsCache[siteCacheKey] = data;
|
||||
this.applyPointOptionsToGroups(data);
|
||||
return data;
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === group.pointRequestId) {
|
||||
if (requestId !== this.sitePointRequestId) return;
|
||||
(this.form.queryGroups || []).forEach((group) => {
|
||||
group.pointLoading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
getDebouncedPointSearch(groupIndex) {
|
||||
if (!this.debouncedPointSearchMap[groupIndex]) {
|
||||
this.debouncedPointSearchMap[groupIndex] = debounce((query) => {
|
||||
this.fetchPointOptions(groupIndex, query);
|
||||
}, 260);
|
||||
}
|
||||
return this.debouncedPointSearchMap[groupIndex];
|
||||
});
|
||||
},
|
||||
remotePointSearch(groupIndex, query) {
|
||||
const group = this.getQueryGroup(groupIndex);
|
||||
if (!group || !this.canSelectPoint(group)) return;
|
||||
this.getDebouncedPointSearch(groupIndex)(query);
|
||||
const baseCacheKey = this.getPointCacheKey("");
|
||||
const baseOptions = group.pointOptionsCache?.[baseCacheKey] || group.pointOptions || [];
|
||||
const localFiltered = this.fuzzyFilterPointOptions(baseOptions, query);
|
||||
this.setPointOptions(group, localFiltered);
|
||||
},
|
||||
handlePointDropdownVisible(groupIndex, visible) {
|
||||
const group = this.getQueryGroup(groupIndex);
|
||||
if (visible && group && this.canSelectPoint(group)) {
|
||||
this.fetchPointOptions(groupIndex, "");
|
||||
this.loadSitePointOptions();
|
||||
}
|
||||
},
|
||||
refreshPointOptions(groupIndex) {
|
||||
const group = this.getQueryGroup(groupIndex);
|
||||
if (!group || !this.canSelectPoint(group)) return;
|
||||
this.fetchPointOptions(groupIndex, "", {force: true});
|
||||
this.loadSitePointOptions({force: true});
|
||||
},
|
||||
clearPointSelection(groupIndex) {
|
||||
const group = this.getQueryGroup(groupIndex);
|
||||
@ -480,66 +818,31 @@ export default {
|
||||
syncQuerySiteIds(routeSiteId) {
|
||||
const siteId = routeSiteId || this.$route?.query?.siteId;
|
||||
const normalizedSiteId = siteId === undefined || siteId === null ? "" : String(siteId).trim();
|
||||
const prevSiteIds = (this.form.siteIds || []).join(",");
|
||||
this.form.siteIds = normalizedSiteId ? [normalizedSiteId] : [];
|
||||
const nextSiteIds = (this.form.siteIds || []).join(",");
|
||||
if (prevSiteIds !== nextSiteIds) {
|
||||
(this.form.queryGroups || []).forEach((group) => {
|
||||
group.pointOptions = [];
|
||||
group.pointOptionsCache = {};
|
||||
group.pointLoading = false;
|
||||
});
|
||||
}
|
||||
if (nextSiteIds) {
|
||||
this.loadSitePointOptions();
|
||||
}
|
||||
},
|
||||
getDate() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const payload = this.buildQueryPayload();
|
||||
if (!payload) return;
|
||||
|
||||
const activeGroups = this.form.queryGroups
|
||||
.map((group) => ({group}))
|
||||
.filter(({group}) => !!group.pointId);
|
||||
|
||||
if (activeGroups.length === 0) {
|
||||
return this.$message.error("请至少选择1组点位");
|
||||
}
|
||||
|
||||
if (!this.form.siteIds || this.form.siteIds.length === 0) {
|
||||
return this.$message.error("请先在顶部选择站点");
|
||||
}
|
||||
|
||||
const {
|
||||
siteIds,
|
||||
dataUnit,
|
||||
dataRange: [start = "", end = ""],
|
||||
} = this.form;
|
||||
|
||||
if (!start || !end) return this.$message.error("请选择时间");
|
||||
|
||||
let startDate = start;
|
||||
let endDate = end;
|
||||
if (start && dataUnit === 3) {
|
||||
startDate = start + " 00:00:00";
|
||||
}
|
||||
if (end && dataUnit === 3) {
|
||||
endDate = end + " 00:00:00";
|
||||
}
|
||||
|
||||
const selectedPoints = [];
|
||||
const pointIdSet = new Set();
|
||||
activeGroups.forEach(({group}) => {
|
||||
const pointId = String(group.pointId || "").trim();
|
||||
if (!pointId || pointIdSet.has(pointId)) return;
|
||||
pointIdSet.add(pointId);
|
||||
selectedPoints.push({
|
||||
pointId,
|
||||
pointName: this.resolveSelectedPointName(group) || pointId,
|
||||
});
|
||||
});
|
||||
const pointIds = selectedPoints.map((item) => item.pointId);
|
||||
const pointNames = selectedPoints.map((item) => item.pointName);
|
||||
getPointValueList({
|
||||
siteIds,
|
||||
dataUnit,
|
||||
pointIds,
|
||||
pointNames,
|
||||
pointId: pointIds.join(","),
|
||||
startDate,
|
||||
endDate,
|
||||
}).then((response) => {
|
||||
this.setOption(response?.data || []);
|
||||
this.requestPointData(payload).then((data) => {
|
||||
this.lastQueryResult = data || [];
|
||||
this.setOption(this.lastQueryResult);
|
||||
}).catch((error) => {
|
||||
if (error?.code === "ECONNABORTED") {
|
||||
this.$message.error("查询超时,请缩短时间范围后重试");
|
||||
|
||||
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,9 +997,8 @@ 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()
|
||||
@ -1025,6 +1012,71 @@ export default {
|
||||
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 }
|
||||
}
|
||||
if (matchType('(')) {
|
||||
@ -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,12 +373,26 @@ 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 || "";
|
||||
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,
|
||||
value: pointId || pointName,
|
||||
label: this.formatPointLabel({ pointId, pointName, pointDesc }),
|
||||
pointId,
|
||||
pointName,
|
||||
@ -385,7 +402,8 @@ export default {
|
||||
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
|
||||
}
|
||||
if (!silent) {
|
||||
this.pointSelectorLoading = true
|
||||
}
|
||||
const query = {
|
||||
...this.pointSelectorQuery
|
||||
}
|
||||
@ -705,7 +1157,9 @@ export default {
|
||||
this.pointSelectorTotal = response?.total || 0
|
||||
})
|
||||
.finally(() => {
|
||||
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
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="map-wrapper">
|
||||
<div ref="mapRef" class="map-canvas"></div>
|
||||
<div v-if="selectedAddress" class="map-center-address">{{ selectedAddress }}</div>
|
||||
<div v-if="!hasPoint" class="map-empty">暂无站点坐标</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -37,11 +38,13 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasPoint: false,
|
||||
selectedAddress: '',
|
||||
map: null,
|
||||
overlays: [],
|
||||
pendingPayload: null,
|
||||
mapConfig: {
|
||||
zoom: 12,
|
||||
selectedZoom: 15,
|
||||
tk: '01e99ab4472430e1c7dbfe4b5db99787'
|
||||
}
|
||||
}
|
||||
@ -71,13 +74,14 @@ export default {
|
||||
},
|
||||
normalizePoint(site = {}) {
|
||||
const name = site.siteName || site.name || ''
|
||||
const address = site.siteAddress || site.address || ''
|
||||
const value = site.value || site.siteLocation || []
|
||||
const lonSource = site.longitude !== undefined && site.longitude !== null ? site.longitude : value[0]
|
||||
const latSource = site.latitude !== undefined && site.latitude !== null ? site.latitude : value[1]
|
||||
const lon = Number(lonSource)
|
||||
const lat = Number(latSource)
|
||||
if (!lon || !lat) return null
|
||||
return { name, lon, lat }
|
||||
return { name, address, lon, lat }
|
||||
},
|
||||
clearOverlays() {
|
||||
if (!this.map || !this.overlays.length) return
|
||||
@ -92,6 +96,7 @@ export default {
|
||||
const points = (Array.isArray(sitesRaw) ? sitesRaw : [])
|
||||
.map(item => this.normalizePoint(item))
|
||||
.filter(Boolean)
|
||||
this.selectedAddress = selected?.address || ''
|
||||
if (selected && !points.find(item => item.lon === selected.lon && item.lat === selected.lat)) {
|
||||
points.push(selected)
|
||||
}
|
||||
@ -118,7 +123,9 @@ export default {
|
||||
this.overlays.push(label)
|
||||
}
|
||||
|
||||
if (viewPoints.length === 1) {
|
||||
if (selected) {
|
||||
this.map.centerAndZoom(new window.T.LngLat(selected.lon, selected.lat), this.mapConfig.selectedZoom)
|
||||
} else if (viewPoints.length === 1) {
|
||||
this.map.centerAndZoom(viewPoints[0], this.mapConfig.zoom)
|
||||
} else {
|
||||
this.map.setViewport(viewPoints)
|
||||
@ -160,4 +167,22 @@ export default {
|
||||
background: rgba(245, 247, 250, 0.9);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.map-center-address {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, calc(-50% - 26px));
|
||||
max-width: min(70%, 520px);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #ffffff;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -30,11 +30,11 @@
|
||||
<span class="site-card-value">{{ formatSiteCardDate(item.runningTime) }}</span>
|
||||
</div>
|
||||
<div class="site-card-info-row">
|
||||
<span class="site-card-label">装机功率(MW)</span>
|
||||
<span class="site-card-label">装机功率(MWh)</span>
|
||||
<span class="site-card-value">{{ formatSiteCardField(item.installPower) }}</span>
|
||||
</div>
|
||||
<div class="site-card-info-row">
|
||||
<span class="site-card-label">装机容量(MW)</span>
|
||||
<span class="site-card-label">装机容量(MWh)</span>
|
||||
<span class="site-card-value">{{ formatSiteCardField(item.installCapacity) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,6 +67,7 @@ export default {
|
||||
loading: false,
|
||||
singleSiteId: '',
|
||||
singleSiteName: '',
|
||||
singleSiteAddress: '',
|
||||
singleSiteLocation: [],
|
||||
allSites: [],
|
||||
canScrollLeft: false,
|
||||
@ -138,7 +139,7 @@ export default {
|
||||
},
|
||||
updateMapMarkers(){
|
||||
this.$refs.mapChart && this.$refs.mapChart.setOption({
|
||||
selected: {name:this.singleSiteName,value:this.singleSiteLocation},
|
||||
selected: {name:this.singleSiteName,address:this.singleSiteAddress,value:this.singleSiteLocation},
|
||||
sites:this.allSites
|
||||
})
|
||||
},
|
||||
@ -147,6 +148,7 @@ export default {
|
||||
this.singleSiteId = id
|
||||
const currentSite = this.allSites.find(item => this.isSameSite(item.siteId, id)) || {}
|
||||
this.singleSiteName = currentSite.siteName || ''
|
||||
this.singleSiteAddress = currentSite.siteAddress || ''
|
||||
this.singleSiteLocation = currentSite.siteLocation || []
|
||||
if (!this.singleSiteLocation.length) {
|
||||
this.singleSiteLocation = [currentSite.longitude, currentSite.latitude].filter(item => item !== undefined && item !== null)
|
||||
|
||||
Reference in New Issue
Block a user