<!-- 昨日供水管网概况 -->
|
<template>
|
<div class="w-full flex-column">
|
<div class="flex mb-4 flex-wrap flex-0">
|
<!-- TimeRange v-model 跟 @change 中的值会不一样,以@change 中为准 -->
|
<template v-if="visibleParams && visibleParams.length > 0 && showFilter">
|
<component
|
class="flex-0 m-2"
|
v-model="visibleParams[index].value"
|
v-for="(item, index) in visibleParams as any"
|
:key="item.id"
|
:id="item.id"
|
:is="recordSetMapCom[item.type]"
|
:data="item"
|
:originData="originData"
|
@change="(val) => handleQueryChange(val, item)"
|
:disabled="chartLoading ||disabled"
|
></component>
|
</template>
|
<slot> </slot>
|
<YRange @input="yRangeInput" />
|
<el-tooltip
|
v-if="originChartType === ChartTypeEnum.Score"
|
:content="`${Object.keys(scoreMap)
|
.map((key) => `${key} 表示${scoreMap[key]}`)
|
.join(', ')}`"
|
placement="top-start"
|
>
|
<SvgIcon name="fa fa-question-circle-o" :size="15" class="ml-1 cursor-help flex-center" color="#909399" />
|
</el-tooltip>
|
|
<el-checkbox class="m-2" v-model="isMultiCompare" label="多日对比" @change="multiCompareChange"></el-checkbox>
|
|
<DisplayMode class="ml-auto" v-model="showMode" @change="displayModeChange" />
|
</div>
|
|
<RecordSetTable
|
:data="tableData"
|
v-if="tableIsShow"
|
:key="tableKey"
|
:tableLimitHeight="tableLimitHeight"
|
:class="{ 'flex-auto': chartHeight == undefined }"
|
/>
|
<div
|
v-show="!tableIsShow"
|
:style="{ height: chartHeight }"
|
:class="{ 'flex-auto': chartHeight == undefined }"
|
v-resize="chartContainerResize"
|
v-loading="chartLoading"
|
>
|
<div ref="chartRef"></div>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import type * as echarts from 'echarts';
|
import _ from 'lodash';
|
import moment from 'moment';
|
import type { PropType } from 'vue';
|
import { computed, ref, shallowRef, watch } from 'vue';
|
import { SCATTER_SYMBOL_SIZE, getChatChartOption } from '../../../common';
|
import { useDrawChatChart } from '../../../hooks/useDrawChatChart';
|
import { ChartTypeEnum } from '../../../types';
|
import RecordSetTable from '../recordSetTable/RecordSetTable.vue';
|
import DisplayMode from './components/DisplayMode.vue';
|
import YRange from './components/YRange.vue';
|
import { DisplayModeType } from './components/types';
|
import type { RecordSetParamsItem } from './types';
|
import { RecordSetParamsType, recordSetMapCom, scoreMap } from './types';
|
import { curveQuery } from '/@/api/ai/chat';
|
import { axisLabelFormatter } from '/@/utils/chart';
|
import { deepClone } from '/@/utils/other';
|
import { debounce } from '/@/utils/util';
|
import { IS_DAY_LIST } from './components/constants';
|
const chartRef = ref<HTMLDivElement>(null);
|
|
const showMode = ref(DisplayModeType.Chart);
|
// const props = defineProps({
|
// data: {
|
// type: Object as PropType<RecordSet>,
|
// },
|
// });
|
|
const emits = defineEmits<{
|
(event: 'updateQuery', res: any): void;
|
}>();
|
|
const props = defineProps({
|
data: {
|
type: Object as PropType<any>,
|
},
|
originData: {
|
type: Object as PropType<any>,
|
},
|
summaryIndex: {
|
type: Number,
|
},
|
chartHeight: {
|
type: String,
|
required: false,
|
},
|
showFilter: {
|
type: Boolean,
|
default: true,
|
},
|
disabled:{
|
type:Boolean,
|
default:false,
|
}
|
}) as {
|
data: any;
|
summaryIndex: number;
|
showFilter: Boolean;
|
};
|
|
const tableLimitHeight = props.chartHeight == undefined ? undefined : document.body.clientHeight * 0.7;
|
|
const chartLoading = ref(false);
|
|
const stepOptions = [
|
{ title: '5分钟', value: '5 minutes' },
|
{ title: '10分钟', value: '10 minutes' },
|
{ title: '半小时', value: '30 minutes' },
|
{ title: '1小时', value: '1 hours' },
|
{ title: '1天', value: '1 days' },
|
];
|
|
const getVisibleParams = (data) => {
|
// const visibleList = props.data?.params?.filter((item) => !item?.hide) ?? [];
|
// index 作为 id
|
const visibleList = (data?.filter ?? []).map((item, index) => {
|
// 不修改原始地址
|
item.id = index + '';
|
|
return item;
|
});
|
const newList: RecordSetParamsItem[] = [];
|
for (let index = 0; index < visibleList.length; index++) {
|
const current = visibleList[index];
|
switch (current.type) {
|
case RecordSetParamsType.TimeRange:
|
newList.push({
|
id: current.id,
|
type: RecordSetParamsType.TimeRange,
|
origin: current,
|
value: [current.start_value, current.end_value],
|
title: current.title,
|
});
|
break;
|
case RecordSetParamsType.Step:
|
newList.push({
|
id: current.id,
|
type: RecordSetParamsType.Step,
|
origin: current,
|
value: current.step_value,
|
list: stepOptions,
|
title: current.title,
|
});
|
break;
|
default:
|
break;
|
}
|
Reflect.deleteProperty(current, 'id');
|
}
|
|
return newList;
|
};
|
|
// 更改 value 值,完全覆盖会丢失响应性,只修改 value
|
const updateVisibleParams = (data) => {
|
const filter = data?.filter ?? [];
|
|
filter.map((newItem, index) => {
|
const currentItem = visibleParams.value.find((item) => item.id === index + '');
|
|
if (currentItem) {
|
if (newItem.type === RecordSetParamsType.TimeRange) {
|
currentItem.value = [newItem.start_value, newItem.end_value];
|
} else {
|
currentItem.value = newItem.step_value;
|
}
|
}
|
});
|
};
|
|
const visibleParams = ref(getVisibleParams(props.data));
|
|
const checkIsDayTime = () => {
|
if (!props.showFilter) return false;
|
const stepFilter = visibleParams.value.find((item) => item.type === RecordSetParamsType.Step);
|
if (!stepFilter.origin.step_value) return false;
|
|
return IS_DAY_LIST.includes(stepFilter.origin.step_value);
|
};
|
// 跨度是否是日期形式
|
// const isDayTime = checkIsDayTime();
|
let groupedValues = null;
|
let timeIndex = undefined;
|
let valueIndex = undefined;
|
let nameIndex = undefined;
|
|
let timeCol = null;
|
let valueCol = null;
|
|
let preData = null;
|
|
let activeChartType: ChartTypeEnum = props.data?.chart_type ?? ChartTypeEnum.Line;
|
let originChartType = activeChartType;
|
|
// 给表格用的 series
|
const currentSeries = shallowRef<any[]>(null);
|
|
const getChartTypeSeriesOption = (type: ChartTypeEnum) => {
|
let result = {};
|
switch (type) {
|
case ChartTypeEnum.Bar:
|
result = {
|
type: 'bar',
|
symbol: 'none',
|
};
|
|
break;
|
case ChartTypeEnum.Line:
|
result = {
|
type: 'line',
|
symbol: 'none',
|
smooth: true,
|
};
|
|
break;
|
|
case ChartTypeEnum.Scatter:
|
result = {
|
type: 'scatter',
|
symbol: 'circle',
|
symbolSize: SCATTER_SYMBOL_SIZE,
|
};
|
|
break;
|
case ChartTypeEnum.Score:
|
result = {
|
type: 'bar',
|
symbol: 'none',
|
};
|
|
break;
|
|
default:
|
break;
|
}
|
|
return result;
|
};
|
|
const setNewOption = (series?: any[], extraOption: echarts.EChartsOption = {}) => {
|
const isEmpty = !series || series.length === 0;
|
if (isEmpty) {
|
series = Object.keys(groupedValues).map((item) => {
|
const values = groupedValues[item];
|
return {
|
name: item === 'default' ? '' : item,
|
data: values.map((item) => [item[timeIndex], item[valueIndex]]),
|
...getChartTypeSeriesOption(activeChartType),
|
};
|
});
|
}
|
currentSeries.value = series;
|
const yAxisFormatter =
|
originChartType === ChartTypeEnum.Score
|
? (value) => {
|
return scoreMap[value];
|
}
|
: axisLabelFormatter;
|
|
const tooltipValueFormatter =
|
originChartType === ChartTypeEnum.Score
|
? (value) => {
|
return scoreMap[value];
|
}
|
: undefined;
|
|
const scoreYAxisOption: echarts.YAXisComponentOption = {
|
min: 0,
|
max: 4,
|
interval: 1,
|
axisLabel: {
|
formatter: yAxisFormatter,
|
},
|
};
|
const combineOption = _.defaultsDeep(
|
{
|
grid: {
|
bottom: 20,
|
},
|
legend: {
|
top: 19,
|
show: true,
|
type: 'scroll',
|
},
|
tooltip: {
|
valueFormatter: tooltipValueFormatter,
|
},
|
toolbox: {
|
show: true,
|
feature: {
|
myBar: {
|
onclick: () => {
|
activeChartType = originChartType === ChartTypeEnum.Score ? ChartTypeEnum.Score : ChartTypeEnum.Bar;
|
chartInstance.value.setOption({
|
series: series.map((item) => ({
|
...item,
|
...getChartTypeSeriesOption(activeChartType),
|
})),
|
});
|
},
|
},
|
|
myScatter: {
|
onclick: () => {
|
activeChartType = ChartTypeEnum.Scatter;
|
|
chartInstance.value.setOption({
|
series: series.map((item) => ({
|
...item,
|
...getChartTypeSeriesOption(activeChartType),
|
})),
|
});
|
},
|
},
|
myLine: {
|
onclick: () => {
|
activeChartType = ChartTypeEnum.Line;
|
chartInstance.value.setOption({
|
series: series.map((item) => ({
|
...item,
|
...getChartTypeSeriesOption(activeChartType),
|
})),
|
});
|
},
|
},
|
},
|
},
|
|
title: {
|
text: preData?.title,
|
},
|
xAxis: {
|
name: timeCol?.title,
|
},
|
yAxis: {
|
name: valueCol?.title,
|
/** @description 不强制保留 */
|
scale: true,
|
...(originChartType === ChartTypeEnum.Score ? scoreYAxisOption : {}),
|
},
|
series: series,
|
} as echarts.EChartsOption,
|
extraOption,
|
getChatChartOption()
|
);
|
chartInstance.value.setOption(combineOption, {
|
notMerge: true,
|
});
|
};
|
|
const handleData = (data = props.data) => {
|
if (!data || !data.cols || !data.values) {
|
return;
|
}
|
preData = data;
|
const xType = 'time';
|
timeIndex = data.cols.findIndex((item) => item.type === 'time');
|
if (timeIndex === -1) {
|
timeIndex = 0;
|
}
|
timeCol = data.cols[timeIndex];
|
|
valueIndex = data.cols.findIndex((item) => item.type === 'value');
|
if (valueIndex === -1) {
|
valueIndex = 2;
|
}
|
valueCol = data.cols[valueIndex];
|
|
let nameCol = null;
|
groupedValues = null;
|
if (data.chart === 'muli_line') {
|
nameIndex = data.cols.findIndex((item) => item.type === 'name');
|
if (nameIndex === -1) {
|
nameIndex = 1;
|
}
|
nameCol = data.cols[nameIndex];
|
groupedValues = _.groupBy(data.values, (item) => item[nameIndex]);
|
} else if (data.chart === 'single_line') {
|
groupedValues = {
|
default: data.values,
|
};
|
} else {
|
// 默认都当muli_line
|
let nameIndex = data.cols.findIndex((item) => item.type === 'name');
|
if (nameIndex === -1) {
|
nameIndex = 1;
|
}
|
nameCol = data.cols[nameIndex];
|
groupedValues = _.groupBy(data.values, (item) => item[nameIndex]);
|
}
|
};
|
|
const drawChart = (data = props.data) => {
|
if (!data || !data.cols || !data.values) {
|
return;
|
}
|
handleData();
|
setNewOption();
|
};
|
const { chartContainerResize, chartInstance } = useDrawChatChart({ chartRef, drawChart });
|
|
const updateCurrent = (res, isNew = false) => {
|
const title = res?.title;
|
const values = res?.values ?? [];
|
//#region ====================== 刷新当前 filter ======================
|
// 只更新 value,不直接覆盖,防止丢失响应性
|
// updateVisibleParams(res);
|
//#endregion
|
groupedValues = _.groupBy(values, (item) => item[nameIndex]);
|
if (isMultiCompare.value) {
|
handleMultiCompare();
|
} else {
|
if (isNew) {
|
setNewOption();
|
} else {
|
(currentSeries.value =
|
groupedValues &&
|
Object.keys(groupedValues).map((item, index) => {
|
const values = groupedValues[item];
|
return {
|
name: item === 'default' ? '' : item,
|
data: values.map((item) => [item[timeIndex], item[valueIndex]]),
|
};
|
})),
|
chartInstance.value?.setOption({
|
title: {
|
text: title,
|
},
|
series: currentSeries.value,
|
});
|
}
|
}
|
};
|
|
const handleQueryChange = async (val: any, item: RecordSetParamsItem) => {
|
if (!val) return;
|
const historyId = (props as any).originData.historyId;
|
const curAgentKey = props.data.agent_key;
|
let res = null;
|
|
// 改变原始值
|
if (item.type === RecordSetParamsType.TimeRange) {
|
item.origin.start_value = val[0];
|
item.origin.end_value = val[1];
|
} else {
|
item.origin.step_value = val;
|
}
|
try {
|
// 相同 agent_key 下所有 filter 请求参数
|
const filterList = ((props as any).originData?.content?.origin?.summary ?? []).reduce((preVal, curVal) => {
|
if (curVal.agent_key !== curAgentKey) return preVal;
|
|
const filter = (curVal.filter ?? []).reduce((subPreVal, subCurVal) => {
|
if (subCurVal.type === RecordSetParamsType.TimeRange) {
|
subPreVal.push(
|
...[
|
{
|
update: subCurVal.update,
|
value: subCurVal.start_value,
|
path: subCurVal.start_path,
|
},
|
{
|
update: subCurVal.update,
|
value: subCurVal.end_value,
|
path: subCurVal.end_path,
|
},
|
]
|
);
|
} else {
|
subPreVal.push({
|
update: subCurVal.update,
|
value: subCurVal.step_value,
|
path: subCurVal.step_path,
|
});
|
}
|
|
return subPreVal;
|
}, []);
|
|
preVal = preVal.concat(filter);
|
|
return preVal;
|
}, []);
|
const params = {
|
history_id: historyId,
|
// 查询前后 agent_key 不会变
|
agent_key: props.data.agent_key,
|
filter_json: JSON.stringify(filterList),
|
};
|
res = await curveQuery(params);
|
chartLoading.value = true;
|
} finally {
|
chartLoading.value = false;
|
}
|
|
emits('updateQuery', res);
|
|
return;
|
};
|
|
let realRange = {
|
min: null,
|
max: null,
|
};
|
|
const getSingleDayOption = (day = COMMON_DAY) =>
|
({
|
tooltip: {
|
show: true,
|
trigger: 'axis',
|
formatter(params) {
|
const itemList = params.map((item, index) => {
|
return `<div style="margin: ${index === 0 ? 0 : 10}px 0 0; line-height: 1">
|
<div style="margin: 0px 0 0; line-height: 1">
|
${item.marker}<span style="font-size: 14px; color: #666; font-weight: 400; margin-left: 2px"
|
>${item.seriesName}</span
|
><span style="float: right; margin-left: 20px; font-size: 14px; color: #666; font-weight: 900">${
|
originChartType === ChartTypeEnum.Score ? scoreMap[item.data[1]] : item.data[1]
|
}</span>
|
<div style="clear: both"></div>
|
</div>
|
<div style="clear: both"></div>
|
</div>`;
|
});
|
|
const result = `<div style="margin: 0px 0 0; line-height: 1">
|
<div style="margin: 0px 0 0; line-height: 1">
|
<div style="font-size: 14px; color: #666; font-weight: 400; line-height: 1">${params?.[0]?.data[0]?.slice(10, 16)}</div>
|
<div style="margin: 10px 0 0; line-height: 1">
|
${itemList.join('')}
|
<div style="clear: both"></div>
|
</div>
|
<div style="clear: both"></div>
|
</div>
|
<div style="clear: both"></div>
|
</div>`;
|
return result;
|
},
|
},
|
xAxis: {
|
min: day + ' 00:00:00',
|
max: day + ' 23:59:59',
|
splitNumber: 10,
|
axisLabel: {
|
formatter: (val) => {
|
const newVal = moment(val).format('HH:mm');
|
return newVal;
|
},
|
showMaxLabel: true,
|
},
|
},
|
} as echarts.EChartsOption);
|
//#region ====================== 设置Y范围 ======================
|
const debounceSetYRange = debounce((val) => {
|
(realRange.min = val.min), (realRange.max = val.max);
|
chartInstance.value.setOption({
|
yAxis: realRange,
|
});
|
|
currentSeries.value = currentSeries.value.concat([]);
|
});
|
|
const yRangeInput = (val) => {
|
debounceSetYRange(val);
|
};
|
|
//#endregion
|
|
//#region ====================== 多日对比 ======================
|
// 多日对比基准时间
|
const COMMON_DAY = '2024-07-26';
|
|
const isMultiCompare = ref(false);
|
const handleMultiCompare = () => {
|
if (!isMultiCompare.value) return;
|
const cloneData = deepClone(groupedValues);
|
const seriesData = Object.keys(cloneData).reduce((preVal, curVal, curIndex, arr) => {
|
const values = cloneData[curVal];
|
const isMulti = arr.length > 1;
|
const groupByDateValues = _.groupBy(values, (item) => moment(item[timeIndex]).format('YYYY-MM-DD'));
|
for (const key in groupByDateValues) {
|
if (Object.prototype.hasOwnProperty.call(groupByDateValues, key)) {
|
const val = groupByDateValues[key];
|
|
const newVal = val.map((item) => {
|
// 新名称
|
item[nameIndex] = isMulti ? `${curVal}_${key}` : `${key}`;
|
item[timeIndex] = COMMON_DAY + ' ' + moment(item[timeIndex]).format('HH:mm:ss');
|
return item;
|
});
|
|
preVal.push(newVal);
|
}
|
}
|
return preVal;
|
}, []);
|
const series = seriesData.map<echarts.SeriesOption>((item) => ({
|
name: item[0]?.[nameIndex],
|
data: item.map((item) => [item[timeIndex], item[valueIndex]]),
|
...getChartTypeSeriesOption(activeChartType),
|
}));
|
setNewOption(series, getSingleDayOption());
|
};
|
const multiCompareChange = (val) => {
|
if (!groupedValues) return;
|
if (val) {
|
handleMultiCompare();
|
} else {
|
setNewOption();
|
}
|
};
|
//#endregion
|
//#region ====================== 切换显示模式 ======================
|
|
const tableIsShow = ref(false);
|
const displayModeChange = (val: DisplayModeType) => {
|
if (val === DisplayModeType.List) {
|
tableIsShow.value = true;
|
} else {
|
tableIsShow.value = false;
|
}
|
};
|
|
const tableData = computed(() => {
|
if (!currentSeries.value) return [];
|
const min = realRange.min == null ? -Infinity : realRange.min;
|
const max = realRange.max == null ? Infinity : realRange.max;
|
const timeDataMap = currentSeries.value.reduce((preVal, curVal, index) => {
|
for (const item of curVal.data) {
|
let [time, value] = item;
|
// 多日对比,只显示时分秒
|
if (isMultiCompare.value) {
|
time = time.slice(11);
|
}
|
if (value < min || value > max) {
|
continue;
|
}
|
if (!preVal[time]) {
|
preVal[time] = [];
|
}
|
|
// score 类型,value 值,需要映射成另一个值
|
if (originChartType === ChartTypeEnum.Score) {
|
value = scoreMap[value];
|
}
|
preVal[time][index] = value;
|
}
|
|
return preVal;
|
}, {});
|
|
const data = Object.keys(timeDataMap).map((item) => [item, ...timeDataMap[item]]);
|
const cols = currentSeries.value.map((item, index) => ({
|
title: item.name ?? `值${index + 1}`,
|
type: 'text',
|
}));
|
cols.unshift({
|
title: '时间',
|
type: 'time',
|
});
|
const result = {
|
type: 'recordset',
|
chart: 'table',
|
title: props.data?.title,
|
max_cols: 5,
|
cols: cols,
|
values: data,
|
};
|
|
return result;
|
});
|
//#endregion
|
|
const tableKey = ref('-999');
|
watch(
|
() => currentSeries.value,
|
(val) => {
|
tableKey.value = _.random(0, 100000) + '';
|
}
|
);
|
|
const updateAll = (triggerIndex, res) => {
|
// 当前 agent_key
|
const curAgentKey = props.data.agent_key;
|
const triggerAgentKey = (props as any).originData?.content?.origin?.summary?.[triggerIndex]?.agent_key;
|
if (curAgentKey !== triggerAgentKey) {
|
return;
|
}
|
if (!curAgentKey) {
|
return;
|
}
|
|
// 当前项所在索引
|
let currentIndex = -1;
|
for (let index = 0; index < (props as any).originData.content.origin.summary.length; index++) {
|
const item = (props as any).originData.content.origin.summary[index];
|
if (item.agent_key === curAgentKey) {
|
currentIndex++;
|
if (index === props.summaryIndex) {
|
break;
|
}
|
}
|
}
|
|
const newSummary = res?.summary?.[currentIndex];
|
if (!newSummary) return;
|
|
updateCurrent(newSummary);
|
};
|
|
const updateIndexSummary = (summary) => {
|
updateCurrent(summary?.[props.summaryIndex], true);
|
};
|
|
defineExpose({
|
drawChart,
|
isMultiCompare,
|
handleMultiCompare,
|
handleData,
|
updateAll,
|
|
updateIndexSummary,
|
});
|
</script>
|
<style scoped lang="scss"></style>
|