wujingjing
2025-03-27 f5e66da24d11103d19986d88428e17d6d4dd8b0f
数据监测
已修改6个文件
已添加3个文件
648 ■■■■■ 文件已修改
customer_list/yw/static/config/route.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/attach/index.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login/UserMenuData.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/chatComponents/summaryCom/components/recordSet/components/types.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/tree/useLeftTree.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/yw/systemManage/DataMonitor/ChartDisplay.vue 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/project/yw/systemManage/DataMonitor/index.vue 253 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/yw/static/config/route.js
@@ -1,5 +1,12 @@
window.route = [
    {
        name: 'DataMonitor',
        isKeepAlive: true,
        isAffix: false,
        path: '/dataMonitor',
        component: '/project/yw/systemManage/DataMonitor/index.vue',
    },
    {
        name: 'AgentGraph',
        isKeepAlive: true,
        isAffix: false,
src/api/attach/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
import request from '/@/utils/request';
export const getAttachTableList = () => {
    return request({
        url: '/attach/get_attach_table_list',
        method: 'POST',
    });
};
export const queryAttachTableRecords = (params: any) => {
    return request({
        url: '/attach/query_attach_table_records',
        method: 'POST',
        data: params,
    });
};
/**
 * @description èŽ·å–é™„ä»¶æŒ‡æ ‡å®šä¹‰åˆ—è¡¨
 **/
export const getAttachMetricListByPost = () =>
    request({
        url: `/attach/get_attach_metric_list`,
        method: 'post',
        params: {},
        data: {},
    });
/**
 * @description æŸ¥è¯¢é™„件指标值
 * @param {FormData} params
 **/
export const queryAttachMetricValuesByPost = (params) =>
    request({
        url: `/attach/query_attach_metric_values`,
        method: 'post',
        params: {},
        data: params,
    });
/**
 * @description æŸ¥è¯¢é™„件指标名称
 * @param {FormData} params
 **/
export const queryAttachMetricNamesByPost = (params) =>
    request({
        url: `/attach/query_attach_metric_names`,
        method: 'post',
        params: {},
        data: params,
    });
src/api/login/UserMenuData.ts
@@ -32,6 +32,22 @@
            },
            {
                Children: [],
                ID: '1121',
                ParentID: '1742436890822447104',
                Type: 2,
                Name: '数据监测',
                Path: '/dataMonitor',
                Permission: '',
                Icon: 'ywifont ywicon-tubiao-zhexiantu',
                IsIframe: false,
                OutLink: '',
                IsHide: false,
                Weight: 0,
                SortCode: 2,
                Description: '',
            },
            {
                Children: [],
                ID: '1122',
                ParentID: '1742436890822447104',
                Type: 2,
src/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue
@@ -15,7 +15,7 @@
                :value-format="valueFormat"
                :format="DEFAULT_FORMATS_DATE"
                :disabled-date="disabledDate"
                :clearable="false"
                :clearable="true"
                :disabled="disabled"
                @change="datePickerChange"
            >
@@ -33,12 +33,19 @@
        <div class="ml-2 inline-flex items-center space-x-2 text-[14px]">
            <div
                @click="quickPickRangeClick(parseInt(item))"
                class="border border-solid rounded-md px-2 cursor-pointer"
                :class="{ 'bg-[#1677ff]': parseInt(item) === quickPickValue, 'text-white': parseInt(item) === quickPickValue }"
                v-for="item in Object.keys(timeRangeEnumMapTitle)"
                class="border border-solid rounded-md px-2 py-1 cursor-pointer"
                :class="{
                    'bg-[#1677ff]': parseInt(item) === quickPickValue,
                    'text-white': parseInt(item) === quickPickValue,
                    'bg-[#f5f7fa]': disabled,
                    'text-[#a9acb3]': disabled,
                    'border-[#dcdfe6]': disabled,
                    '!cursor-not-allowed': disabled,
                }"
                v-for="item in Object.keys(timeRangeMapTitle)"
                :key="item"
            >
                {{ timeRangeEnumMapTitle[item] }}
                {{ timeRangeMapTitle[item] }}
            </div>
        </div>
    </div>
@@ -47,21 +54,19 @@
<script setup lang="ts">
import { ElDatePicker } from 'element-plus';
import { definePropType } from 'element-plus/es/utils/vue/props/runtime';
import { ref, type PropType, computed, watch, nextTick, onMounted } from 'vue';
import moment from 'moment';
import { computed, nextTick, onMounted, ref, type PropType } from 'vue';
import type { TimeRangeParam } from '../types';
import type { TimeRangeEnum } from './types';
import { timeRangeEnumMapTitle, timeRangeEnumMapValue } from './types';
import { dayTimeRangeEnumMapTitle, monthTimeRangeEnumMapTitle, timeRangeEnumMapValue, TimeStepValue } from './types';
import {
    CURRENT_DAY,
    DEFAULT_FORMATS_DATE,
    DEFAULT_FORMATS_TIME,
    END_PLACEHOLDER,
    RANGE_SEPARATOR,
    START_PLACEHOLDER,
} from '/@/components/form/datepicker/constants';
import { formatDate } from '/@/utils/formatTime';
import moment from 'moment';
const valueFormat = DEFAULT_FORMATS_DATE + ' ' + DEFAULT_FORMATS_TIME;
const props = defineProps({
    data: {
@@ -71,12 +76,24 @@
        type: Boolean,
        default: false,
    },
    quickActive: {
        type: Number as PropType<TimeRangeEnum>,
        required: false,
    },
});
const getRangeMapTitle = (timeStep: TimeStepValue) => {
    switch (timeStep) {
        case TimeStepValue.Day:
            return dayTimeRangeEnumMapTitle;
        case TimeStepValue.Month:
            return monthTimeRangeEnumMapTitle;
        default:
            return dayTimeRangeEnumMapTitle;
    }
};
const timeRangeMapTitle = getRangeMapTitle(props.data?.origin?.time_step);
const dateValue = defineModel({
    type: definePropType<[string, string]>(Array),
});
@@ -84,6 +101,9 @@
const dateChange = () => {
    nextTick(() => {
        if (dateValue.value?.[1]) {
            dateValue.value[1] = dateValue.value[1].slice(0, 10) + ' 23:59:59';
        }
        emit('change', dateValue.value);
    });
};
@@ -98,19 +118,22 @@
const quickPickValue = ref<TimeRangeEnum>(null);
const pickQuickRange = (val: TimeRangeEnum) => {
    if(val==undefined) return;
    // if(val==undefined) return;
    quickPickValue.value = val;
    dateValue.value = timeRangeEnumMapValue[val]().map((item) => formatDate(item)) as [string, string];
};
const quickPickRangeClick = (val: TimeRangeEnum) => {
    if (props.disabled) return;
    if (quickPickValue.value === val) return;
    pickQuickRange(val);
    pickQuickRange(val);
    dateChange();
};
const offsetClickIsAllow = computed(() => !!dateValue.value && !props.disabled);
const preDayClick = () => {
    if (props.disabled) return;
    if (!dateValue.value) return;
    dateValue.value[0] = moment(dateValue.value[0]).subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss');
    dateChange();
@@ -119,6 +142,8 @@
};
const nextDayClick = () => {
    if (props.disabled) return;
    if (!dateValue.value) return;
    dateValue.value[1] = moment(dateValue.value[1]).add(1, 'day').format('YYYY-MM-DD HH:mm:ss');
    dateChange();
@@ -130,15 +155,14 @@
    resetQuickPickValue();
    dateChange();
};
onMounted(() => {
    if(props.quickActive !=null){
    if (props.quickActive != null) {
        pickQuickRange(props.quickActive);
    }
});
defineExpose({
    formatDateValue: dateValue,
    resetQuickPickValue: resetQuickPickValue,
});
</script>
<style scoped lang="scss">
src/components/chat/chatComponents/summaryCom/components/recordSet/components/types.ts
@@ -4,6 +4,9 @@
    CurrentDay,
    ThreeDay,
    SevenDay,
    ThreeMonth,
    HalfYear,
    OneYear,
}
export const timeRangeEnumMapTitle = {
@@ -15,6 +18,10 @@
    [TimeRangeEnum.CurrentDay]: () => getRecentDateRange(1),
    [TimeRangeEnum.ThreeDay]: () => getRecentDateRange(3),
    [TimeRangeEnum.SevenDay]: () => getRecentDateRange(7),
    [TimeRangeEnum.ThreeMonth]: () => getRecentDateRange(90),
    [TimeRangeEnum.HalfYear]: () => getRecentDateRange(180),
    [TimeRangeEnum.OneYear]: () => getRecentDateRange(365),
};
export const enum DisplayModeType {
@@ -26,3 +33,25 @@
    [DisplayModeType.Chart]: '图表',
    [DisplayModeType.List]: '列表',
};
export const enum TimeStepValue {
    Day = 'day',
    Month = 'month',
}
/**
 * æŒ‰æ—¥å¿«æ·è¾“å…¥
 */
export const dayTimeRangeEnumMapTitle = {
    [TimeRangeEnum.CurrentDay]: '当日',
    [TimeRangeEnum.ThreeDay]: '近三日',
    [TimeRangeEnum.SevenDay]: '近七日',
};
/**
 * æŒ‰æœˆå¿«æ·è¾“å…¥
 */
export const monthTimeRangeEnumMapTitle = {
    [TimeRangeEnum.ThreeMonth]: '近三月',
    [TimeRangeEnum.HalfYear]: '近半年',
    [TimeRangeEnum.OneYear]: '近一年',
};
src/components/tree/useLeftTree.ts
@@ -155,7 +155,6 @@
        }
    );
    //#region ====================== æœç´¢æ¡†æ“ä½œ ======================
    const handleCommand = async (command: string | number | object) => {
        if ('expandAll' == command) {
@@ -166,7 +165,7 @@
            for (let i = 0; i < listTreeRef.value!.store._getAllNodes().length; i++) {
                listTreeRef.value!.store._getAllNodes()[i].expanded = false;
            }
        }
        }
    };
    //#endregion
@@ -205,6 +204,6 @@
        //#region ====================== åˆ—表树节点后缀图标 ======================
        suffixIconFun,
        //#endregion
        handleCommand
        handleCommand,
    };
};
src/views/project/yw/systemManage/DataMonitor/ChartDisplay.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,231 @@
<template>
    <div>
        <div class="h-full" v-loading="chartLoading" v-resize="chartContainerResize">
            <div ref="chartRef"></div>
        </div>
    </div>
</template>
<script setup lang="ts" name="ChartDisplay">
import { defaultsDeep } from 'lodash-es';
import type { PropType } from 'vue';
import { computed, ref, watch } from 'vue';
import { getChatChartOption, SCATTER_SYMBOL_SIZE } from '/@/components/chat/chatComponents/common';
import { useDrawChatChart } from '/@/components/chat/chatComponents/hooks/useDrawChatChart';
import { ChartTypeEnum } from '/@/components/chat/chatComponents/types';
import type { SeriesOption } from 'echarts';
import { axisLabelFormatter } from '/@/utils/chart';
const props = defineProps({
    displayList: {
        type: Array as PropType<any[]>,
        default: () => [],
    },
});
const chartRef = ref<HTMLDivElement>();
const getYColumnIndexMap = () => {
    const displayList = props?.displayList ?? [];
    if (displayList.length === 0) return [{}, []];
    const yColumnList = displayList.map((item) => {
        return item.values?.columns[1];
    });
    const yColumnIndexMap = {};
    // Remove duplicates from yColumnList
    const uniqueYColumns = Array.from(new Set(yColumnList.filter(Boolean)));
    for (const item of displayList) {
        uniqueYColumns.forEach((yColumn, index) => {
            if (item.values?.columns[1] === yColumn) {
                yColumnIndexMap[item.logicalId] = { index, name: yColumn };
            }
        });
    }
    return [yColumnIndexMap, uniqueYColumns] as any;
};
const drawChart = () => {
    const displayList = props?.displayList ?? [];
    if (displayList.length === 0) {
        chartInstance.value.clear();
        return;
    }
    const xRowTitle = displayList[0]?.values?.columns?.[0] ?? '时间';
    const [yColumnIndexMap, uniqueYColumns] = getYColumnIndexMap();
    const chartOption = getChatChartOption();
    Reflect.deleteProperty(chartOption, 'yAxis');
    const combineOption = defaultsDeep(
        {
            grid: {
                bottom: 50,
                right: 65,
            },
            legend: {
                top: 19,
                show: true,
                type: 'scroll',
            },
            tooltip: {
                // valueFormatter: tooltipValueFormatter,
            },
            toolbox: {
                show: true,
                feature: {
                    myBar: {
                        onclick: () => {
                            activeChartType = ChartTypeEnum.Bar;
                            chartInstance.value.setOption({
                                series: seriesInfo.value.map((item) => ({
                                    ...item,
                                    ...getChartTypeSeriesOption(activeChartType),
                                })),
                            });
                        },
                    },
                    myScatter: {
                        onclick: () => {
                            activeChartType = ChartTypeEnum.Scatter;
                            chartInstance.value.setOption({
                                series: seriesInfo.value.map((item) => ({
                                    ...item,
                                    ...getChartTypeSeriesOption(activeChartType),
                                })),
                            });
                        },
                    },
                    myLine: {
                        onclick: () => {
                            activeChartType = ChartTypeEnum.Line;
                            chartInstance.value.setOption({
                                series: seriesInfo.value.map((item) => ({
                                    ...item,
                                    ...getChartTypeSeriesOption(activeChartType),
                                })),
                            });
                        },
                    },
                },
            },
            // title: {
            //     text: props.mapRow?.title,
            // },
            xAxis: {
                name: xRowTitle,
                nameLocation: 'middle',
                nameGap: 32,
            },
            // yAxis: displayList.map((item) => {
            //     return {
            //         name: item.values?.columns[1],
            //         /** @description ä¸å¼ºåˆ¶ä¿ç•™ */
            //         scale: true,
            //     };
            // }),
            yAxis: uniqueYColumns.map((item) => {
                return {
                    name: item,
                    /** @description ä¸å¼ºåˆ¶ä¿ç•™ */
                    scale: true,
                    // æ˜¾ç¤ºy轴线
                    axisLine: {
                        show: true,
                    },
                    // æ˜¾ç¤ºåˆ»åº¦çº¿
                    axisTick: {
                        show: true,
                    },
                    type: 'value',
                    axisLabel: {
                        formatter: axisLabelFormatter,
                    },
                };
            }),
            series: seriesInfo.value,
        } as echarts.EChartsOption,
        chartOption
    );
    chartInstance.value?.setOption(combineOption, {
        notMerge: true,
    });
};
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;
};
let activeChartType: ChartTypeEnum = ChartTypeEnum.Line;
const seriesInfo = computed(() => {
    const displayList = props.displayList ?? [];
    if (displayList.length === 0) return [];
    const xProp = 0;
    const yProp = 1;
    const [yColumnIndexMap, uniqueYColumns] = getYColumnIndexMap();
    const series = props.displayList.map((item) => {
        return {
            name: item.title,
            data: item.values?.records.toSorted((b, a) => {
                return b[xProp]?.localeCompare(a[xProp]);
            }),
            ...getChartTypeSeriesOption(activeChartType),
            yAxisIndex: yColumnIndexMap[item.logicalId]?.index,
        } as SeriesOption;
    });
    return series;
});
const chartLoading = ref(false);
const { chartContainerResize, chartInstance } = useDrawChatChart({ chartRef, drawChart });
watch(
    () => props.displayList,
    (val) => {
        drawChart();
    }
);
</script>
<style scoped lang="scss"></style>
src/views/project/yw/systemManage/DataMonitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,253 @@
<template>
    <AMContainer class="flex-auto" type="card">
        <template #aside>
            <!-- ç›®å½•æ ‘ -->
            <LeftTreeByMgr
                v-loading="treeLoading"
                class="h100"
                ref="leftTreeRef"
                :defaultProps="{
                    id: 'logicalId',
                    label: 'title',
                    children: 'children',
                }"
                :titleName="'指标列表'"
                @check="handleCheck"
                showCheckbox
                defaultExpandAll
                :treedata="listTreeData"
                :show-more-operate="false"
                :show-add="false"
                :current-node-key="currentListID"
                :folder-icon="(_, data) => data.type !== 'metric'"
                @click="handleClickNode"
            >
            </LeftTreeByMgr>
        </template>
        <template #main>
            <div class="w100 h100">
                <div class="h-full">
                    <div class="h-full flex-column gap-2">
                        <div class="flex-0 flex items-center">
                            <TimeRange
                                class="flex-0"
                                ref="timeRangeRef"
                                v-if="dateRange"
                                v-model="dateRange"
                                @change="handleSearchInput"
                                :data="{
                                    origin: {
                                        time_step: TimeStepValue.Month,
                                    },
                                }"
                                :quickActive="TimeRangeEnum.ThreeMonth"
                            />
                        </div>
                        <ChartDisplay v-loading="chartLoading" class="flex-auto" :displayList="checkedList" />
                    </div>
                </div>
            </div>
        </template>
    </AMContainer>
</template>
<script setup lang="ts" name="DataMonitor">
import type { TableInstance } from 'element-plus';
import { ElMessage } from 'element-plus';
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useCompRef } from '/@/utils/types';
import * as attachApi from '/@/api/attach';
import { convertListToTree, getRecentDateRange, travelTree } from '/@/utils/util';
import { formatDate } from '/@/utils/formatTime';
import TimeRange from '/@/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue';
import LeftTreeByMgr from '/@/components/tree/leftTreeByMgr.vue';
import ChartDisplay from './ChartDisplay.vue';
import AMContainer from '/@/components/layout/AMContainer.vue';
import { TimeStepValue } from '/@/components/chat/chatComponents/summaryCom/components/recordSet/components/types';
import { TimeRangeEnum } from '/@/components/chat/chatComponents/summaryCom/components/recordSet/components/types';
const timeRangeRef = useCompRef(TimeRange);
const checkedList = ref([]);
const handleCheck = async (currentNode, checkedInfo) => {
    const checkedNodes = checkedInfo.checkedNodes.filter((item) => item.type === 'metric');
    await getUnGetData(checkedNodes);
    checkedList.value = checkedNodes;
};
//#region ====================== å·¦ä¾§æ ‘数据,tree init ======================
const leftTreeRef = useCompRef(LeftTreeByMgr);
const treeLoading = ref(false);
const currentListID = computed(() => currentNode.value?.id);
const listTreeData = ref([]);
const currentNode = ref(null);
const requestListTreeData = async (listData: any[]) => {
    const listDataWithType = listData.map((item) => ({
        id: item.id,
        logicalId: `item-${item.id}`,
        logicalParentId: `group-${item.group}`,
        ...item,
        type: 'item',
    }));
    const groupList = Array.from(new Set(listDataWithType.filter((item) => item.group).map((item) => item.group))).map((item) => ({
        id: item,
        title: item,
        logicalId: `group-${item}`,
        type: 'group',
    }));
    const treeData = convertListToTree(listDataWithType.concat(groupList), {
        ID: 'logicalId',
        ParentID: 'logicalParentId',
        Children: 'children',
    });
    const requestParams = listData.map((item) => {
        return {
            id: item.id,
            name: item.title,
        };
    });
    const p = Promise.all(
        requestParams.map((item) => {
            return attachApi.queryAttachMetricNamesByPost(item);
        })
    );
    const res = await p;
    const idMapMetricName = new Map(
        requestParams.map((item, index) => {
            return [item.id, res[index].values ?? []];
        })
    );
    for (const metricItem of listDataWithType) {
        const metricId = metricItem.id;
        if (idMapMetricName.has(metricId)) {
            const metricNames = idMapMetricName.get(metricId);
            metricItem.children = metricNames.map((item) => {
                const id = `${item.OTYPE}-${item.ONAME}`;
                return {
                    metric: metricItem,
                    id,
                    logicalId: `metric-${id}`,
                    logicalParentId: metricItem.logicalId,
                    title: item.TITLE,
                    type: 'metric',
                    model: item,
                };
            });
        }
    }
    return treeData;
};
const handleClickNode = (data) => {
    return;
    if (data.type === 'group' || data.type === 'item') {
        ElMessage.warning('请选择指标');
        return;
    }
    nextTick(() => {
        leftTreeRef.value?.treeRef.setCurrentKey(data.logicalId);
    });
    currentNode.value = data;
    handleChartQuery(data);
};
const getListTreeData = async () => {
    treeLoading.value = true;
    const res = await attachApi.getAttachMetricListByPost().finally(() => {
        treeLoading.value = false;
    });
    const listData = res.metrics || [];
    listTreeData.value = await requestListTreeData(listData);
    let firstListTreeNode;
    travelTree(listTreeData.value, (value, index, array, parent) => {
        if (value.type === 'metric') {
            firstListTreeNode = value;
            return true;
        }
    });
    if (firstListTreeNode) {
        await handleChartQuery(firstListTreeNode);
        leftTreeRef.value?.treeRef.setCheckedKeys([firstListTreeNode.logicalId]);
        checkedList.value = [firstListTreeNode];
    } else {
        currentNode.value = null;
    }
};
//#endregion
const getUnGetData = async (checkedList: any[]) => {
    const getDataPromiseList = [];
    for (const item of checkedList) {
        if (!item.values) {
            getDataPromiseList.push(handleSearchItem(item));
        }
    }
    // ç­‰å¾…所有数据获取完成
    await Promise.all(getDataPromiseList);
};
//#region ====================== æŒ‡æ ‡ç®¡ç†è¡¨æ ¼æ•°æ®ï¼Œtable init ======================
const chartLoading = ref(false);
const handleChartQuery = async (row) => {
    if (!row.values) {
        await handleSearchItem(row);
    }
};
//#endregion
//#region ====================== æŸ¥è¯¢ ======================
const dateRange = ref(getRecentDateRange(90).map((item) => formatDate(item)));
const getSearchParams = (node) => {
    const metricsNameModel = JSON.stringify(node.model);
    const params = {
        id: node.metric?.id,
        start_time: dateRange.value[0],
        end_time: dateRange.value[1],
        quota_keys: metricsNameModel,
        limit: 100,
    } as any;
    return params;
};
const handleSearchItem = async (node) => {
    const params = getSearchParams(node);
    chartLoading.value = true;
    const res = await attachApi.queryAttachMetricValuesByPost(params).finally(() => {
        chartLoading.value = false;
    });
    node.values = res ?? {};
};
const handleSearchInput = async () => {
    const allPromiseList = [];
    for (const item of checkedList.value) {
        allPromiseList.push(handleSearchItem(item));
    }
    await Promise.all(allPromiseList);
    checkedList.value = checkedList.value.concat([])
};
//#endregion
onMounted(() => {
    getListTreeData();
});
</script>
<style lang="scss" scoped></style>
vite.config.ts
@@ -45,7 +45,7 @@
            host: '0.0.0.0',
            port: env.VITE_PORT as unknown as number,
            open: JSON.parse(env.VITE_OPEN),
            hmr: false,
            hmr: true,
            proxy: {
                '/gitee': {
                    target: 'https://gitee.com',