Merge branch 'lichang' of http://192.168.2.166:3180/OMC/ems_frontend_vue3 into lichang
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue3-smooth-dnd": "^0.0.6",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"xlsx": "~0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
541
src/views/tool/test/index.vue
Normal file
541
src/views/tool/test/index.vue
Normal file
@@ -0,0 +1,541 @@
|
||||
<script setup lang="ts">
|
||||
import draggable from 'vuedraggable';
|
||||
|
||||
import * as echarts from 'echarts';
|
||||
import { PageContainer } from 'antdv-pro-layout';
|
||||
import { onMounted, reactive, ref, markRaw, nextTick, onUnmounted } from 'vue';
|
||||
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import { SizeType } from 'ant-design-vue/es/config-provider';
|
||||
import { listKPIData, getKPITitle } from '@/api/perfManage/goldTarget';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import useNeInfoStore from '@/store/modules/neinfo';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ColumnsType } from 'ant-design-vue/es/table';
|
||||
import { generateColorRGBA } from '@/utils/generate-utils';
|
||||
import { SeriesOption } from 'echarts/types/dist/shared';
|
||||
import { LineSeriesOption } from 'echarts/charts';
|
||||
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||
import { Switch } from 'ant-design-vue';
|
||||
|
||||
const { t, currentLocale } = useI18n();
|
||||
const route = useRoute();
|
||||
const neInfoStore = useNeInfoStore();
|
||||
//WebSocket连接
|
||||
//const ws = new WS();
|
||||
const ws = ref<WS | null>(null);
|
||||
|
||||
//实时数据开关
|
||||
const handleRealTimeSwitch = (checked: any, event: Event) => {
|
||||
console.log('Switch toggled:', checked);
|
||||
fnRealTimeSwitch(!!checked);
|
||||
};
|
||||
//添加实时数据开关状态
|
||||
const realTimeEnabled = ref(false);
|
||||
|
||||
// 定义图表类型
|
||||
type ChartType = 'udm' | 'upf' | 'amf' | 'smf';
|
||||
//构建响应式数组储存图表类型数据
|
||||
const chartOrder = ref(['udm', 'upf', 'amf', 'smf']);
|
||||
|
||||
// 定义表格状态类型
|
||||
type TableStateType = {
|
||||
loading: boolean;
|
||||
size: SizeType;
|
||||
seached: boolean;
|
||||
data: Record<string, any>[];
|
||||
selectedRowKeys: (string | number)[];
|
||||
};
|
||||
|
||||
|
||||
// 创建可复用的状态
|
||||
const createChartState = () => ({
|
||||
chartDom: ref<HTMLElement | null>(null),
|
||||
chart: ref<echarts.ECharts | null>(null),
|
||||
tableColumns: ref<ColumnsType>([]),
|
||||
tableState: reactive<TableStateType>({
|
||||
loading: false,
|
||||
size: 'small',
|
||||
seached: true,
|
||||
data: [],
|
||||
selectedRowKeys: [],
|
||||
}),
|
||||
chartLegendSelected: {} as Record<string, boolean>,
|
||||
chartDataXAxisData: [] as string[],
|
||||
chartDataYSeriesData: [] as CustomSeriesOption[],
|
||||
});
|
||||
|
||||
// 为每种图表类型创建状态
|
||||
const chartStates: Record<ChartType, ReturnType<typeof createChartState>> = {
|
||||
udm: createChartState(),
|
||||
upf: createChartState(),
|
||||
amf: createChartState(),
|
||||
smf: createChartState(),
|
||||
};
|
||||
|
||||
//日期选择器
|
||||
interface RangePicker {
|
||||
udm: [string, string];
|
||||
upf: [string, string];
|
||||
amf: [string, string];
|
||||
smf: [string, string];
|
||||
placeholder: [string, string];
|
||||
ranges: Record<string, [Dayjs, Dayjs]>;
|
||||
}
|
||||
|
||||
// 创建日期选择器状态
|
||||
const rangePicker = reactive<RangePicker>({
|
||||
udm: [
|
||||
dayjs('2024-09-20 00:00:00').valueOf().toString(),
|
||||
dayjs('2024-09-20 23:59:59').valueOf().toString()
|
||||
],
|
||||
upf: [
|
||||
dayjs('2024-09-20 00:00:00').valueOf().toString(),
|
||||
dayjs('2024-09-20 23:59:59').valueOf().toString()
|
||||
],
|
||||
amf: [
|
||||
dayjs('2024-09-20 00:00:00').valueOf().toString(),
|
||||
dayjs('2024-09-20 23:59:59').valueOf().toString()
|
||||
],
|
||||
smf: [
|
||||
dayjs('2024-09-20 00:00:00').valueOf().toString(),
|
||||
dayjs('2024-09-20 23:59:59').valueOf().toString()
|
||||
],
|
||||
placeholder: [t('views.monitor.monitor.startTime'), t('views.monitor.monitor.endTime')] as [string, string],
|
||||
ranges: {
|
||||
[t('views.monitor.monitor.yesterday')]: [dayjs().subtract(1, 'day').startOf('day'), dayjs().subtract(1, 'day').endOf('day')],
|
||||
[t('views.monitor.monitor.today')]: [dayjs().startOf('day'), dayjs()],
|
||||
[t('views.monitor.monitor.week')]: [dayjs().startOf('week'), dayjs().endOf('week')],
|
||||
[t('views.monitor.monitor.month')]: [dayjs().startOf('month'), dayjs().endOf('month')],
|
||||
},
|
||||
});
|
||||
|
||||
// 创建可复用的图表初始化函数
|
||||
const initChart = (type: ChartType) => {
|
||||
nextTick(()=> {
|
||||
const state = chartStates[type];
|
||||
const container = state.chartDom.value;
|
||||
if (!container) return;
|
||||
state.chart.value = markRaw(echarts.init(container, 'light'));
|
||||
const option: echarts.EChartsOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: function(pt: any) {
|
||||
return [pt[0], '10%'];
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: [0, '100%'],
|
||||
},
|
||||
legend: {
|
||||
type: 'scroll',
|
||||
orient: 'horizontal',
|
||||
top: -5,
|
||||
itemWidth: 20,
|
||||
textStyle: {
|
||||
color: '#646A73',
|
||||
},
|
||||
icon: 'circle',
|
||||
selected: {},
|
||||
},
|
||||
grid: {
|
||||
left: '10%',
|
||||
right: '10%',
|
||||
bottom: '15%',
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: [],
|
||||
};
|
||||
state.chart.value.setOption(option);
|
||||
});
|
||||
};
|
||||
//结束拖拽事件
|
||||
const onDragEnd = () => {
|
||||
nextTick(() => {
|
||||
chartOrder.value.forEach((type) => {
|
||||
const state = chartStates[type as ChartType];
|
||||
if (state.chart.value) {
|
||||
state.chart.value.dispose(); // 销毁旧的图表实例
|
||||
}
|
||||
initChart(type as ChartType);
|
||||
fetchData(type as ChartType); // 重新获取数据并渲染图表
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 创建可复用的数据获取函数
|
||||
const fetchData = async (type: ChartType) => {
|
||||
const state = chartStates[type];
|
||||
const neId = '001';
|
||||
state.tableState.loading = true;
|
||||
try {
|
||||
const [startTime, endTime] = rangePicker[type];
|
||||
const res = await listKPIData({
|
||||
neType: type.toUpperCase(),
|
||||
neId,
|
||||
startTime,
|
||||
endTime,
|
||||
sortField: 'timeGroup',
|
||||
sortOrder: 'desc',
|
||||
interval: 5,
|
||||
});
|
||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
state.tableState.data = res.data;
|
||||
nextTick(()=> {
|
||||
renderChart(type);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error(t('common.getInfoFail'));
|
||||
} finally {
|
||||
state.tableState.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
//建立实时数据连接
|
||||
function fnRealTimeSwitch(bool: boolean) {
|
||||
console.log('fnRealTimeSwitch called with:', bool);
|
||||
realTimeEnabled.value = bool;
|
||||
if (bool) {
|
||||
if(!ws.value){
|
||||
console.log('Creating new WS instance');
|
||||
ws.value = new WS();
|
||||
}
|
||||
Object.values(chartStates).forEach(state => {
|
||||
state.tableState.seached = false;
|
||||
});
|
||||
// 建立连接
|
||||
const options: OptionsType = {
|
||||
url: '/ws',
|
||||
params: {
|
||||
subGroupID: Object.keys(chartStates).map(type => `10_${type.toUpperCase()}_001`).join(','),
|
||||
},
|
||||
onmessage: wsMessage,
|
||||
onerror: wsError,
|
||||
onopen:()=>{
|
||||
console.log('WebSocket connection established')
|
||||
}
|
||||
};
|
||||
console.log('Attempting to connect with options:', options);
|
||||
ws.value.connect(options);
|
||||
console.log('Connection attempt initiated');
|
||||
} else if(ws.value){
|
||||
console.log('Closing WebSocket connection');
|
||||
Object.values(chartStates).forEach(state => {
|
||||
state.tableState.seached = true;
|
||||
});
|
||||
ws.value.close();
|
||||
ws.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 接收数据后错误回调
|
||||
function wsError(ev: any) {
|
||||
console.error('WebSocket error:', ev);
|
||||
message.error(t('common.websocketError'));
|
||||
}
|
||||
|
||||
// 接收数据后回调
|
||||
function wsMessage(res: Record<string, any>) {
|
||||
//const res = JSON.parse(event.data);
|
||||
const { code, data } = res;
|
||||
if (code === RESULT_CODE_ERROR) {
|
||||
console.warn(res.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// 订阅组信息
|
||||
if (!data?.groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理四个图表的数据
|
||||
(Object.keys(chartStates)as ChartType[]).forEach((type) => {
|
||||
const state = chartStates[type];
|
||||
const kpiEvent = data.data[type.toUpperCase()];
|
||||
|
||||
if (kpiEvent) {
|
||||
// 更新 X 轴数据
|
||||
if (kpiEvent.timeGroup) {
|
||||
state.chartDataXAxisData.push(parseDateToStr(+kpiEvent.timeGroup));
|
||||
if (state.chartDataXAxisData.length > 100) {
|
||||
state.chartDataXAxisData.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Y 轴数据
|
||||
state.chartDataYSeriesData.forEach(series => {
|
||||
if (kpiEvent[series.customKey as string] !== undefined) {
|
||||
series.data.push(+kpiEvent[series.customKey as string]);
|
||||
if (series.data.length > 100) {
|
||||
series.data.shift();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 更新图表
|
||||
if (state.chart.value) {
|
||||
state.chart.value.setOption({
|
||||
xAxis: { data: state.chartDataXAxisData },
|
||||
series: state.chartDataYSeriesData,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type LineDataItem = {
|
||||
value: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
interface CustomSeriesOption extends Omit<LineSeriesOption, 'data'> {
|
||||
customKey?: string;
|
||||
data: (number | LineDataItem)[];
|
||||
}
|
||||
// 创建可复用的图表渲染函数
|
||||
const renderChart = (type: ChartType) => {
|
||||
const state = chartStates[type];
|
||||
if (state.chart.value == null || state.tableState.data.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置数据
|
||||
state.chartLegendSelected = {};
|
||||
state.chartDataXAxisData = [];
|
||||
state.chartDataYSeriesData = [];
|
||||
|
||||
// 处理数据
|
||||
for (const columns of state.tableColumns.value) {
|
||||
if (['neName', 'startIndex', 'timeGroup'].includes(columns.key as string)) continue;
|
||||
const color = generateColorRGBA();
|
||||
state.chartDataYSeriesData.push({
|
||||
name: columns.title as string,
|
||||
customKey: columns.key as string,
|
||||
//key: columns.key as string,
|
||||
type: 'line',
|
||||
symbol: 'none',
|
||||
sampling: 'lttb',
|
||||
itemStyle: { color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: color.replace(')', ',0.8)') },
|
||||
{ offset: 1, color: color.replace(')', ',0.3)') },
|
||||
]),
|
||||
},
|
||||
data: [],
|
||||
} as CustomSeriesOption);
|
||||
state.chartLegendSelected[columns.title as string] = true;
|
||||
//});
|
||||
//state.chartLegendSelected[columns.title as string] = true;
|
||||
}
|
||||
|
||||
const orgData = [...state.tableState.data].reverse();
|
||||
for (const item of orgData) {
|
||||
state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup));
|
||||
for (const y of state.chartDataYSeriesData) {
|
||||
//const key = (y.emphasis as any).customKey;
|
||||
y.data.push(+item[y.customKey as string]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
state.chart.value.setOption(
|
||||
{
|
||||
legend: { selected: state.chartLegendSelected },
|
||||
xAxis: { data: state.chartDataXAxisData,
|
||||
},
|
||||
series: state.chartDataYSeriesData,
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ replaceMerge: ['xAxis', 'series'] }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// 获取表头数据
|
||||
const fetchKPITitle = async (type: ChartType) => {
|
||||
const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0];
|
||||
try {
|
||||
const res = await getKPITitle(type.toUpperCase());
|
||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
chartStates[type].tableColumns.value = res.data.map(item => ({
|
||||
title: item[`${language}Title`],
|
||||
dataIndex: item.kpiId,
|
||||
align: 'left',
|
||||
key: item.kpiId,
|
||||
resizable: true,
|
||||
width: 100,
|
||||
minWidth: 150,
|
||||
maxWidth: 300,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.warning(t('common.getInfoFail'));
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化所有图表
|
||||
onMounted(async () => {
|
||||
ws.value = new WS();
|
||||
await neInfoStore.fnNelist();
|
||||
for (const type of Object.keys(chartStates) as ChartType[]) {
|
||||
await fetchKPITitle(type);
|
||||
initChart(type);
|
||||
fetchData(type);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// 在组件卸载时销毁图表实例
|
||||
onUnmounted(() => {
|
||||
if(ws.value &&ws.value.state()===WebSocket.OPEN) {
|
||||
ws.value.close();
|
||||
}
|
||||
Object.values(chartStates).forEach((state) => {
|
||||
if (state.chart.value) {
|
||||
state.chart.value.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<div class="control-row">
|
||||
<div class="real-time-switch">
|
||||
<Switch
|
||||
v-model:checked="realTimeEnabled"
|
||||
@change="handleRealTimeSwitch as any"
|
||||
/>
|
||||
<span class="switch-label">{{ realTimeEnabled ? '实时数据已开启' : '实时数据已关闭' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
v-model="chartOrder"
|
||||
:animation="200"
|
||||
item-key="type"
|
||||
@end="onDragEnd"
|
||||
class="row"
|
||||
onscroll='false'
|
||||
>
|
||||
<template #item="{element:type}">
|
||||
|
||||
<div class="col-lg-6 col-md=-6 col-xs-12">
|
||||
<a-card :bordered="false" :body-style="{ marginBottom: '24px', padding: '24px'}">
|
||||
<template #title>{{ type.toUpperCase() }}</template>
|
||||
<template #extra>
|
||||
<a-range-picker
|
||||
v-model:value="(rangePicker[type as keyof RangePicker]as[string,string])"
|
||||
:allow-clear="false"
|
||||
bordered
|
||||
:show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="x"
|
||||
:placeholder="rangePicker.placeholder"
|
||||
:ranges="rangePicker.ranges"
|
||||
style="width: 100%"
|
||||
@change="() => fetchData(type)"
|
||||
></a-range-picker>
|
||||
</template>
|
||||
<div class='chart' style="padding: 12px">
|
||||
<div :ref="el => { if (el) chartStates[type as ChartType].chartDom.value = el as HTMLElement }" style="height:400px;width:100%"></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</draggable>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
}
|
||||
.sortable-ghost {
|
||||
opacity: 0.5;
|
||||
background: #c8ebfb;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
opacity: 0.8;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -12px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
.col-lg-6 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
.col-md-6 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575px) {
|
||||
.col-xs-12 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.control-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background-color: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.real-time-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user