wujingjing
2024-07-26 608be7b1d461138717c4bb34639d9976a605fe0f
yRange; timeRange; 多日对比
已修改9个文件
已添加4个文件
574 ■■■■ 文件已修改
customer_list/ch/static/fonts/ywiconfont/iconfont.css 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/ch/static/fonts/ywiconfont/iconfont.ttf 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/ch/static/fonts/ywiconfont/iconfont.woff 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/ch/static/fonts/ywiconfont/iconfont.woff2 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/chatComponents/common.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSet.vue 311 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/chatComponents/summaryCom/components/recordSet/components/YRange.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/chat/chatComponents/summaryCom/components/recordSet/components/types.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/form/datepicker/DatePicker.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/formatTime.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/util.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
customer_list/ch/static/fonts/ywiconfont/iconfont.css
@@ -1,8 +1,8 @@
@font-face {
  font-family: "ywicon"; /* Project id 4499025 */
  src: url('iconfont.woff2?t=1721375418093') format('woff2'),
       url('iconfont.woff?t=1721375418093') format('woff'),
       url('iconfont.ttf?t=1721375418093') format('truetype');
  src: url('iconfont.woff2?t=1721966908249') format('woff2'),
       url('iconfont.woff?t=1721966908249') format('woff'),
       url('iconfont.ttf?t=1721966908249') format('truetype');
}
.ywicon {
@@ -13,6 +13,14 @@
  -moz-osx-font-smoothing: grayscale;
}
.icon-pre:before {
  content: "\e61b";
}
.icon-next:before {
  content: "\e61c";
}
.icon-maikefeng-filled:before {
  content: "\e621";
}
customer_list/ch/static/fonts/ywiconfont/iconfont.ttf
Binary files differ
customer_list/ch/static/fonts/ywiconfont/iconfont.woff
Binary files differ
customer_list/ch/static/fonts/ywiconfont/iconfont.woff2
Binary files differ
src/components/chat/chatComponents/common.ts
@@ -54,7 +54,6 @@
    originData: {
        type: Object as PropType<any>,
    },
} as const);
export type ChatComPropsType = ExtractPropTypes<typeof chatComProps>;
@@ -63,12 +62,14 @@
        grid: {
            // bottom: 120,
            // right: '15%',
            top: 65,
            left: 35,
            right: 45,
        },
        tooltip: {
            show: true,
            trigger: 'axis',
        },
        toolbox: {
            show: true,
src/components/chat/chatComponents/summaryCom/components/recordSet/RecordSet.vue
@@ -13,6 +13,8 @@
                @change="(val) => handleQueryChange(val, item)"
                :disabled="chartLoading"
            ></component>
            <YRange v-model="yRange" @input="yRangeInput" />
            <el-checkbox class="m-1" v-model="isMultiCompare" label="多日对比" @change="multiCompareChange"></el-checkbox>
        </div>
        <div class="h-[20rem]" v-resize="chartContainerResize" v-loading="chartLoading">
            <div ref="chartRef"></div>
@@ -23,18 +25,24 @@
<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, watch } from 'vue';
import { SCATTER_SYMBOL_SIZE, chatComProps, getChatChartOption } from '../../../common';
import { computed, ref } from 'vue';
import { SCATTER_SYMBOL_SIZE, getChatChartOption } from '../../../common';
import { useDrawChatChart } from '../../../hooks/useDrawChatChart';
import YRange from './components/YRange.vue';
import type { RecordSet, RecordSetParamsItem } from './types';
import { recordSetMapCom } from './types';
import { filterQuery } from '/@/api/ai/chat';
import { deepClone } from '/@/utils/other';
import { debounce } from '/@/utils/util';
const chartRef = ref<HTMLDivElement>(null);
const defaultDisplayType = 'line';
const yRange = ref({
    min: null as number,
    max: null as number,
});
// const props = defineProps({
//     data: {
//         type: Object as PropType<RecordSet>,
@@ -61,25 +69,116 @@
let groupedValues = null;
let timeIndex = undefined;
let valueIndex = undefined;
let nameIndex = undefined;
let timeCol = null;
let valueCol = null;
let preData = null;
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]]),
                type: defaultDisplayType,
                symbol: 'none',
                smooth: true,
            };
        });
    }
    const combineOption = _.defaultsDeep(extraOption, getChatChartOption(), {
        grid: {
            bottom: 20,
        },
        legend: {
            top: 19,
            show: series?.length > 1,
            type: 'scroll',
        },
        toolbox: {
            show: true,
            feature: {
                myBar: {
                    onclick: () => {
                        chartInstance.value.setOption({
                            series: series.map((item) => ({
                                ...item,
                                type: 'bar',
                                symbol: 'none',
                            })),
                        });
                    },
                },
                myScatter: {
                    onclick: () => {
                        chartInstance.value.setOption({
                            series: series.map((item) => ({
                                ...item,
                                type: 'scatter',
                                symbol: 'circle',
                                symbolSize: SCATTER_SYMBOL_SIZE,
                            })),
                        });
                    },
                },
                myLine: {
                    onclick: () => {
                        chartInstance.value.setOption({
                            series: series.map((item) => ({
                                ...item,
                                type: 'line',
                                symbol: 'none',
                                smooth: true,
                            })),
                        });
                    },
                },
            },
        },
        title: {
            text: preData?.title,
        },
        xAxis: {
            name: timeCol?.title,
        },
        yAxis: {
            name: valueCol?.title,
            /** @description ä¸å¼ºåˆ¶ä¿ç•™0 */
            scale: true,
        },
        series: series,
    } as echarts.EChartsOption);
    chartInstance.value.setOption(combineOption, {
        notMerge: true,
    });
};
const drawChart = () => {
    const data = props.data;
    preData = data;
    const xType = 'time';
    timeIndex = data.cols.findIndex((item) => item.type === 'time');
    if (timeIndex === -1) {
        timeIndex = 0;
    }
    const timeCol = data.cols[timeIndex];
    timeCol = data.cols[timeIndex];
    valueIndex = data.cols.findIndex((item) => item.type === 'value');
    if (valueIndex === -1) {
        valueIndex = 2;
    }
    const valueCol = data.cols[valueIndex];
    valueCol = data.cols[valueIndex];
    let nameCol = null;
    groupedValues = null;
    if (data.chart === 'muli_line') {
        let nameIndex = data.cols.findIndex((item) => item.type === 'name');
        nameIndex = data.cols.findIndex((item) => item.type === 'name');
        if (nameIndex === -1) {
            nameIndex = 1;
        }
@@ -99,74 +198,7 @@
        groupedValues = _.groupBy(data.values, (item) => item[nameIndex]);
    }
    const seriesData = Object.keys(groupedValues).map((item) => {
        const values = groupedValues[item];
        return {
            name: item === 'default' ? '' : item,
            data: values.map((item) => [item[timeIndex], item[valueIndex]]),
            type: defaultDisplayType,
            symbol: 'none',
            smooth: true,
        };
    });
    const combineOption = _.defaultsDeep(getChatChartOption(), {
        grid: {
            bottom: 20,
        },
        toolbox: {
            show: true,
            feature: {
                myBar: {
                    onclick: () => {
                        chartInstance.value.setOption({
                            series: seriesData.map((item) => ({
                                ...item,
                                type: 'bar',
                                symbol: 'none',
                            })),
                        });
                    },
                },
                myScatter: {
                    onclick: () => {
                        chartInstance.value.setOption({
                            series: seriesData.map((item) => ({
                                ...item,
                                type: 'scatter',
                                symbol: 'circle',
                                symbolSize: SCATTER_SYMBOL_SIZE,
                            })),
                        });
                    },
                },
                myLine: {
                    onclick: () => {
                        chartInstance.value.setOption({
                            series: seriesData.map((item) => ({
                                ...item,
                                type: 'line',
                                symbol: 'none',
                                smooth: true,
                            })),
                        });
                    },
                },
            },
        },
        title: {
            text: data?.title,
        },
        xAxis: {
            name: timeCol?.title,
        },
        yAxis: {
            name: valueCol?.title,
        },
        series: seriesData,
    } as echarts.EChartsOption);
    chartInstance.value.setOption(combineOption);
    setNewOption();
};
const { chartContainerResize, chartInstance } = useDrawChatChart({ chartRef, drawChart });
@@ -198,18 +230,131 @@
    const title = res?.values?.title;
    const values = res?.values?.values ?? [];
    groupedValues = _.groupBy(values, (item) => item[nameIndex]);
    if (isMultiCompare.value) {
        handleMultiCompare();
    } else {
        chartInstance.value.setOption({
            title: {
                text: title,
            },
            series:
                groupedValues &&
                Object.keys(groupedValues).map((item) => {
                    const values = groupedValues[item];
                    return {
                        data: values.map((item) => [item[timeIndex], item[valueIndex]]),
                    };
                }),
        });
    }
};
//#region ====================== è®¾ç½®Y范围 ======================
const debounceSetYRange = debounce((val) => {
    chartInstance.value.setOption({
        title: {
            text: title,
        yAxis: {
            min: val.min,
            max: val.max,
        },
        series:
            groupedValues &&
            Object.keys(groupedValues).map(() => {
                return {
                    data: values.map((item) => [item[timeIndex], item[valueIndex]]),
                };
            }),
    });
});
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]]),
        type: defaultDisplayType,
        symbol: 'none',
        smooth: true,
    }));
    setNewOption(series, {
        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">${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: COMMON_DAY + ' 00:00:00',
            max: COMMON_DAY + ' 23:59:59',
            splitNumber: 10,
            axisLabel: {
                formatter: (val) => {
                    const newVal = moment(val).format('HH:mm');
                    return newVal;
                },
                showMaxLabel: true,
            },
        },
    });
};
const multiCompareChange = (val) => {
    if (!groupedValues) return;
    if (val) {
        handleMultiCompare();
    } else {
        setNewOption();
    }
};
//#endregion
</script>
<style scoped lang="scss"></style>
src/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
<template>
    <div class="flex items-center">
        <div class="flex items-center space-x-1">
            <div
                class="ywicon icon-pre"
                :class="{ 'cursor-not-allowed': !offsetClickIsAllow, 'cursor-pointer': offsetClickIsAllow }"
                @click="preDayClick"
            ></div>
            <el-date-picker
                style="width: 240px"
                v-model="dateValue"
                type="daterange"
                :start-placeholder="START_PLACEHOLDER"
                :end-placeholder="END_PLACEHOLDER"
                :value-format="valueFormat"
                :format="DEFAULT_FORMATS_DATE"
                :disabled-date="disabledDate"
                :clearable="false"
                :disabled="disabled"
            >
                <template v-for="(value, name) in $slots" #[name]="slotData">
                    <slot :name="name" v-bind="slotData || {}"></slot>
                </template>
            </el-date-picker>
            <div
                class="ywicon icon-next"
                :class="{ 'cursor-not-allowed': !offsetClickIsAllow, 'cursor-pointer': offsetClickIsAllow }"
                @click="nextDayClick"
            ></div>
        </div>
        <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)"
                :key="item"
            >
                {{ timeRangeEnumMapTitle[item] }}
            </div>
        </div>
    </div>
</template>
<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 } from 'vue';
import type { TimestampParam } from '../types';
import type { TimeRangeEnum } from './types';
import { timeRangeEnumMapTitle, timeRangeEnumMapValue } 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: {
        type: Object as PropType<TimestampParam>,
    },
    disabled: {
        type: Boolean,
        default: false,
    },
});
const dateValue = defineModel({
    type: definePropType<[string, string]>(Array),
});
const emit = defineEmits(['change']);
/**
 * éœ€è¦å¯¹ dateValue æ ¼å¼åŒ–,dataValue ç»“束时间不是23:59:59
 */
const formatDateValue = computed({
    get: () => {
        if (!dateValue.value) return null;
        return [moment(dateValue.value[0]).format('YYYY-MM-DD 00:00:00'), moment(dateValue.value[1]).format('YYYY-MM-DD 23:59:59')] as [
            string,
            string
        ];
    },
    set: (value) => {
        dateValue.value = value;
    },
});
const disabledDate = (date: Date) => {
    return date > CURRENT_DAY;
};
const quickPickValue = ref<TimeRangeEnum>(null);
const quickPickRangeClick = (val: TimeRangeEnum) => {
    if (quickPickValue.value === val) return;
    quickPickValue.value = val;
    formatDateValue.value = timeRangeEnumMapValue[val]().map((item) => formatDate(item)) as [string, string];
};
const offsetClickIsAllow = computed(() => !!dateValue.value && !props.disabled);
const preDayClick = () => {
    if (!dateValue.value) return;
    dateValue.value[0] = moment(dateValue.value[0]).subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss');
    dateValue.value = [...dateValue.value];
};
const nextDayClick = () => {
    if (!dateValue.value) return;
    dateValue.value[1] = moment(dateValue.value[1]).add(1, 'day').format('YYYY-MM-DD HH:mm:ss');
    dateValue.value = [...dateValue.value];
};
watch(
    () => formatDateValue.value,
    (val) => {
        emit('change', val);
    }
);
</script>
<style scoped lang="scss">
:deep(.el-date-editor .el-range__close-icon--hidden) {
    display: none;
}
</style>
src/components/chat/chatComponents/summaryCom/components/recordSet/components/YRange.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
<template>
    <div class="flex items-center">
        <el-input-number
            placeholder="最小值"
            :style="{
                width: inputWidth,
            }"
            class="rounded-full"
            v-model="yRange.min"
            @input="(val) => numInput(val, 'min')"
            :controls="false"
        ></el-input-number>
        <span class="bg-[#cdcdcd] h-[32px] inline-block flex-center">~</span>
        <el-input-number
            placeholder="最大值"
            :style="{
                width: inputWidth,
            }"
            class="round"
            v-model="yRange.max"
            :controls="false"
            @input="(val) => numInput(val, 'max')"
        ></el-input-number>
    </div>
</template>
<script setup lang="ts">
import { watch, type PropType, ref } from 'vue';
const emit = defineEmits(['input']);
const yRange = ref({
    min: null,
    max: null,
});
const realYRange = ref({
    min:null,
    max:null
})
const inputWidth = '82px';
const numInput = (val: any, type: 'min' | 'max') => {
    realYRange.value[type] = val;
    emit('input', realYRange.value);
};
</script>
<style scoped lang="scss"></style>
src/components/chat/chatComponents/summaryCom/components/recordSet/components/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import { getRecentDate, getRecentDateRange } from '/@/utils/util';
export const enum TimeRangeEnum {
    CurrentDay,
    ThreeDay,
    SevenDay,
}
export const timeRangeEnumMapTitle = {
    [TimeRangeEnum.CurrentDay]: '当日',
    [TimeRangeEnum.ThreeDay]: '近三日',
    [TimeRangeEnum.SevenDay]: '近七日',
};
export const timeRangeEnumMapValue = {
    [TimeRangeEnum.CurrentDay]: () => getRecentDateRange(1),
    [TimeRangeEnum.ThreeDay]: () => getRecentDateRange(3),
    [TimeRangeEnum.SevenDay]: () => getRecentDateRange(7),
};
src/components/form/datepicker/DatePicker.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
<template>
    <el-date-picker
        v-model="dateValue"
        type="daterange"
        :range-separator="RANGE_SEPARATOR"
        :start-placeholder="START_PLACEHOLDER"
        :end-placeholder="END_PLACEHOLDER"
        :shortcuts="shortcuts"
        :value-format="valueFormat"
        :disabled-date="disabledDate"
    >
        <template v-for="(value, name) in $slots" #[name]="slotData">
            <slot :name="name" v-bind="slotData || {}"></slot>
        </template>
    </el-date-picker>
</template>
<script setup lang="ts">
import { ElDatePicker } from 'element-plus';
import { definePropType } from 'element-plus/es/utils/vue/props/runtime';
import { CURRENT_DAY, DEFAULT_FORMATS_DATE, END_PLACEHOLDER, RANGE_SEPARATOR, SHORTCUTS, START_PLACEHOLDER } from './constants';
const props = defineProps({
    valueFormat: {
        type: String,
        default: DEFAULT_FORMATS_DATE,
    },
    shortcuts: {
        type: Array,
        default: () => [],
        // default: SHORTCUTS
    },
});
const dateValue = defineModel({
    type: definePropType<[string, string]>(Array),
});
const disabledDate = (date: Date) => {
    return date > CURRENT_DAY;
};
</script>
<style scoped lang="scss"></style>
src/utils/formatTime.ts
@@ -9,7 +9,7 @@
 * @description format å­£åº¦ + æ˜ŸæœŸ + å‡ å‘¨ï¼š"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
 * @returns è¿”回拼接后的时间字符串
 */
export function formatDate(date: Date, format: string): string {
export function formatDate(date: Date, format = 'YYYY-mm-dd HH:MM:SS'): string {
    let we = date.getDay(); // æ˜ŸæœŸ
    let z = getWeek(date); // å‘¨
    let qut = Math.floor((date.getMonth() + 3) / 3).toString(); // å­£åº¦
src/utils/util.ts
@@ -541,7 +541,8 @@
 * æœ€è¿‘ n å¤©çš„ startDate、endDate
 * @param dates
 */
export const getRecentDateRange = (dates: number) => {
export const getRecentDateRange = (dates: number, includesCurrent = true) => {
    dates = includesCurrent ? dates - 1 : dates;
    // èŽ·å–å½“å‰æ—¥æœŸ
    const endDate = new Date();
    const startDate = new Date();
vite.config.ts
@@ -35,7 +35,7 @@
            host: '0.0.0.0',
            port: env.VITE_PORT as unknown as number,
            open: JSON.parse(env.VITE_OPEN),
            hmr: true,
            hmr: false,
        },
        build: {
            // outDir: 'dist/' + mode.mode,