<template>
|
<div class="flex flex-col h100">
|
<el-row class="w100 h100">
|
<el-col :span="17" class="h-full">
|
<div class="h100 relative">
|
<div class="ml-6 h100">
|
<el-tabs v-model="state.activeMetricName" @tab-change="handleClick" class="h100">
|
<el-tab-pane label="基础信息" name="basicInformation" class="h-full">
|
<div class="h100 flex flex-col">
|
<div class="flex-0">
|
<el-descriptions title="指标信息">
|
<el-descriptions-item label="指标编号:" :span="2">{{
|
state.metricBasicInfo.descriptionQuotaItems.id ? state.metricBasicInfo.descriptionQuotaItems.id : '-'
|
}}</el-descriptions-item>
|
<el-descriptions-item label="指标名称:" :span="2">{{
|
state.metricBasicInfo.descriptionQuotaItems.title ? state.metricBasicInfo.descriptionQuotaItems.title : '-'
|
}}</el-descriptions-item>
|
<el-descriptions-item label="主题域:" :span="2">{{
|
state.metricBasicInfo.descriptionQuotaItems?.metrics_group
|
? themeDomainMap?.[state.metricBasicInfo.descriptionQuotaItems?.metrics_group]?.group_name
|
: '-'
|
}}</el-descriptions-item>
|
<el-descriptions-item label="单位:" :span="2">{{
|
state.metricBasicInfo.descriptionQuotaItems.metrics_unit
|
? state.metricBasicInfo.descriptionQuotaItems.metrics_unit
|
: '-'
|
}}</el-descriptions-item>
|
<el-descriptions-item label="指标全称:" :span="2">{{
|
state.metricBasicInfo.descriptionQuotaItems.full_name
|
? state.metricBasicInfo.descriptionQuotaItems.full_name
|
: '-'
|
}}</el-descriptions-item>
|
<el-descriptions-item label="指标定义:" :span="3">{{
|
state.metricBasicInfo.descriptionQuotaItems.metrics_define
|
? state.metricBasicInfo.descriptionQuotaItems.metrics_define
|
: '-'
|
}}</el-descriptions-item>
|
<el-descriptions-item label="类别:" :span="2">
|
{{
|
state.metricBasicInfo.descriptionQuotaItems.metrics_type
|
? state.metricBasicInfo.descriptionQuotaItems.metrics_type
|
: '-'
|
}}</el-descriptions-item
|
>
|
<el-descriptions-item label="重要性:" :span="2">
|
{{ eMetrics_Ops[state.metricBasicInfo.descriptionQuotaItems.metrics_important] }}
|
</el-descriptions-item>
|
<el-descriptions-item label="计算方法:" :span="3">
|
{{
|
state.metricBasicInfo.descriptionQuotaItems.calcu_method
|
? state.metricBasicInfo.descriptionQuotaItems.calcu_method
|
: '-'
|
}}
|
</el-descriptions-item>
|
</el-descriptions>
|
<el-divider />
|
</div>
|
<div class="flex-auto flex flex-col">
|
<span class="flex-0 text-[16px] font-bold mb-[4px]">指标图谱</span>
|
<TreeGraph v-if="graphData" class="flex-auto" :data="graphData" :maxCount="maxCount" />
|
</div>
|
</div>
|
</el-tab-pane>
|
<el-tab-pane label="指标探索" name="indicatorExploration" class="h-full">
|
<div class="h-full flex-column">
|
<el-form :model="dialogFormValue" class="flex-0" ref="dialogFormRef" :rules="dialogFormRules">
|
<el-form-item label="日期区间:" prop="rangValue" v-if="currentMetrics?.is_time_values ?? true">
|
<TimeRange v-model="dialogFormValue.rangValue" @change="query(false)" />
|
</el-form-item>
|
<el-row :gutter="35">
|
<el-col :span="12" class="mb20">
|
<el-form-item label="分组维度:" prop="rangValue">
|
<el-select v-model="dialogFormValue.groupDimList" multiple @change="query(false)">
|
<el-option
|
v-for="item in currentMetrics?.dimensions ?? []"
|
:key="item.id"
|
:label="item.title"
|
:value="item.id"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12" class="mb20" v-for="item in filterDimList" :key="item.id"
|
><el-form-item :label="`${item.title}:`" :prop="`${item.id}`">
|
<el-input v-model="dialogFormValue[item.id]" @input="(val) => filterDimInput(val, item)"> </el-input>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
<div class="flex-auto mt-4">
|
<SummaryCom
|
v-loading="queryLoading"
|
ref="summaryComRef"
|
:data="querySummaryData"
|
:origin-data="{
|
content: {
|
origin: {
|
summary: querySummaryData,
|
},
|
},
|
}"
|
/>
|
<!-- <RecordSet v-bind="recordSetProps" class="h-full" /> -->
|
<!-- <RecordSetTable v-bind="recordSetTableProps" class="h-full"/> -->
|
</div>
|
</div>
|
</el-tab-pane>
|
<!-- <el-tab-pane label="指标图谱" name="metricGraph" class="h-full"> <div class="h-full"></div></el-tab-pane> -->
|
</el-tabs>
|
</div>
|
<div class="list_btn" @click="handleExitFlow">
|
<el-button style="width: 40px" link>
|
<el-icon style="font-size: 16px !important; color: #1677ff">
|
<ArrowLeft />
|
</el-icon>
|
</el-button>
|
<span class="text-[14px] text-[#1677ff] font-[400] mr-2">返回列表页</span>
|
</div>
|
</div>
|
</el-col>
|
<el-col :span="7">
|
<div class="set-right-height ml-[10px]">
|
<div class="mb-[5px]">
|
<div class="set-visit">
|
<i class="ywifont ywicon-fenshu_an w-[18px] h-[18px] text-yellow-300 !text-[20px]"></i>
|
<div>关键信息</div>
|
</div>
|
</div>
|
<div class="set-detail">
|
<el-divider />
|
<div class="section___vePzi">
|
<div class="item___title">
|
<i class="ywifont ywicon-zhibiao size-[18px] !text-[15px]"></i>
|
<span>{{state.metricBasicInfo.descriptionQuotaItems.title ? state.metricBasicInfo.descriptionQuotaItems.title : '-'}}</span>
|
</div>
|
<div class="item___txXyB">
|
<div class="item_name">重要性:</div>
|
<div class="item_value">{{ eMetrics_Ops[state.metricBasicInfo.descriptionQuotaItems.metrics_important] }}</div>
|
</div>
|
<div class="item___txXyB">
|
<div class="item_name">所属领域:</div>
|
<div class="item_value">
|
{{
|
state.metricBasicInfo.descriptionQuotaItems.metrics_type
|
? state.metricBasicInfo.descriptionQuotaItems.metrics_type
|
: '-'
|
}}
|
</div>
|
</div>
|
<div class="item___txXyB">
|
<div class="item_name">描述:</div>
|
<div class="item_value">
|
{{
|
state.metricBasicInfo.descriptionQuotaItems.metrics_define
|
? state.metricBasicInfo.descriptionQuotaItems.metrics_define
|
: '-'
|
}}
|
</div>
|
</div>
|
</div>
|
<el-divider />
|
<div class="section___vePzi">
|
<div class="item___title">
|
<i class="ywifont ywicon-gongzuozongjie w-[18px] h-[18px] !text-[18px]"></i>
|
<span>创建信息</span>
|
</div>
|
<div class="item___txXyB">
|
<div class="item_name">创建人:</div>
|
<div class="item_value">
|
{{
|
state.metricBasicInfo.descriptionQuotaItems.create_user
|
? state.metricBasicInfo.descriptionQuotaItems.create_user
|
: '-'
|
}}
|
</div>
|
</div>
|
<div class="item___txXyB">
|
<div class="item_name">创建时间:</div>
|
<div class="item_value">
|
{{
|
state.metricBasicInfo.descriptionQuotaItems.create_time
|
? state.metricBasicInfo.descriptionQuotaItems.create_time
|
: '-'
|
}}
|
</div>
|
</div>
|
</div>
|
<el-divider />
|
<div class="section___vePzi">
|
<div class="item___title">
|
<i class="ywifont ywicon-jiankong w-[18px] h-[18px] !text-[20px]"></i>
|
<span>应用信息 </span>
|
</div>
|
<div class="text-[#344767]">
|
<div class="item___bg">
|
<div class="block px-[16px] py-[4px]">
|
<div class="subTitle___zya5g">下钻维度</div>
|
<div class="ml-0 mt-[20px]">
|
<div class="flex flex-wrap gap-[5px] items-center">
|
<div v-for="item in currentMetrics?.dimensions ?? []" :key="item.id">
|
<el-tag :style="{ backgroundColor: 'e6f4ff', color: '#0958d9', borderColor: '#91caff' }">{{
|
item.title
|
}}</el-tag>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</el-col>
|
</el-row>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import * as echarts from 'echarts';
|
import { nextTick, onMounted, reactive, ref } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
import * as metricApi from '/@/api/metrics';
|
import TimeRange from '/@/components/chat/chatComponents/summaryCom/components/recordSet/components/TimeRange.vue';
|
import TreeGraph from '/@/components/graph/treeGraph/TreeGraph.vue';
|
import { formatTime, getDefaultPeriod } from '/@/utils/istation/common.js';
|
import { eMetrics_Ops } from '/@/views/types/metrics';
|
|
import type { FormRules } from 'element-plus/es/components/form/src/types';
|
import _, { debounce } from 'lodash';
|
import { computed } from 'vue';
|
import type { OrgTreeItem } from '../agentGraph/types';
|
import SummaryCom from './components/SummaryCom.vue';
|
import { chatMetricsJsonByPost } from '/@/api/metrics';
|
import { getSceneGroupTreeByPost } from '/@/api/scene';
|
import { useCompRef } from '/@/utils/types';
|
import { getItemMap } from '/@/utils/util';
|
const defaultTime = ref<[Date, Date]>([new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]);
|
const router = useRouter();
|
const route = useRoute();
|
// 定义变量内容
|
const themeDomainData = ref([]);
|
const initThemeDomainData = async () => {
|
const res = await getSceneGroupTreeByPost();
|
themeDomainData.value = res.groups || [];
|
};
|
const themeDomainMap = computed(() => {
|
return getItemMap(themeDomainData.value, 'group_id');
|
});
|
const state = reactive({
|
metricInfo: {
|
name: '',
|
id: '',
|
} as any,
|
activeMetricName: 'basicInformation',
|
metricBasicInfo: {
|
descriptionQuotaItems: {},
|
} as any,
|
exploreForm: {
|
dateRangeExplore: '' as any,
|
dimensionDrilling: 0,
|
dimensionFiltering: {
|
user: '',
|
unit: '',
|
someone: '',
|
},
|
},
|
resultList: [
|
{
|
id: 1,
|
department: 'HR',
|
},
|
{
|
id: 2,
|
department: 'marketing',
|
},
|
{
|
id: 3,
|
department: 'sales',
|
},
|
{
|
id: 4,
|
department: 'strategy',
|
},
|
],
|
chartStatus: false,
|
});
|
|
//返回到知识库列表
|
const handleExitFlow = () => {
|
//是否显示返回
|
router.back();
|
};
|
|
//#endregion
|
//#region ====================== 基础信息 ======================
|
const currentMetrics = ref(null);
|
const tableData = ref([]);
|
const getTableData = async (id) => {
|
const res = await metricApi.getMetricNameListByPost();
|
const data = res?.values ?? [];
|
const filterData = data.filter((item) => item.id == id);
|
state.metricBasicInfo.descriptionQuotaItems = filterData[0]; //左侧描述内容
|
currentMetrics.value = filterData[0];
|
filterDimList.value.forEach((item) => {
|
if (!dialogFormValue.value) {
|
dialogFormValue.value = {} as any;
|
}
|
dialogFormValue.value[item.id] = '';
|
});
|
};
|
//#endregion
|
//#region ====================== 指标图谱查询 ======================
|
// 时间限制
|
const disablesDate = (time) => {
|
return time.getTime() > new Date().getTime();
|
};
|
//获取日期的时间
|
const getDateTime = (val) => {
|
state.exploreForm.dateRangeExplore[0] = val[0];
|
state.exploreForm.dateRangeExplore[1] = val[1];
|
};
|
const handleDimensionFilteringChange = (val) => {
|
state.exploreForm.dimensionFiltering.user = val;
|
};
|
const handleExplore = () => {
|
console.log(state.exploreForm);
|
};
|
//#endregion
|
|
//#region ====================== 图表resize ======================
|
const getChartInstance = (chartRef: HTMLDivElement) => {
|
let chartInstance = echarts.getInstanceByDom(chartRef);
|
if (chartInstance) return chartInstance;
|
const { width, height } = chartRef.getBoundingClientRect();
|
chartInstance = echarts.init(chartRef, undefined, {
|
width,
|
height,
|
});
|
window.addEventListener('resize', () => {
|
const { width, height } = chartRef.getBoundingClientRect();
|
chartInstance = echarts.init(chartRef, undefined, {
|
width,
|
height,
|
});
|
chartInstance.resize({ width, height });
|
});
|
return chartInstance;
|
};
|
const resizeChart = () => {
|
const { width, height } = chartExplorationRef.value.getBoundingClientRect();
|
const chartInstance = getChartInstance(chartExplorationRef.value);
|
chartInstance && chartInstance.resize({ width, height });
|
};
|
//#endregion
|
//#region ====================== init 图表 ======================
|
const chartExplorationRef = ref(null);
|
const initChartExploration = () => {
|
const testData = [
|
{
|
sys_imp_date: '2024-10-03',
|
stay_hours: 5.058984585654882,
|
},
|
{
|
sys_imp_date: '2024-10-05',
|
stay_hours: 10.25773691448837,
|
},
|
{
|
sys_imp_date: '2024-10-06',
|
stay_hours: 8.523378141884379,
|
},
|
{
|
sys_imp_date: '2024-10-07',
|
stay_hours: 7.073257474459168,
|
},
|
{
|
sys_imp_date: '2024-10-08',
|
stay_hours: 6.69083423985072,
|
},
|
];
|
let xData = [];
|
let yData = [];
|
testData.forEach((item) => {
|
xData.push(item.sys_imp_date);
|
yData.push(item.stay_hours);
|
});
|
const chartExploration = {
|
tooltip: {
|
trigger: 'axis',
|
},
|
grid: {
|
top: 70,
|
left: 10,
|
right: 40,
|
bottom: 0,
|
containLabel: true,
|
},
|
xAxis: {
|
name: '日期',
|
type: 'category',
|
|
axisTick: {
|
alignWithLabel: true,
|
},
|
data: xData,
|
axisLabel: {
|
formatter: function (value) {
|
const date = new Date(value);
|
return formatTime('yyyy-MM-dd', date);
|
},
|
},
|
},
|
yAxis: [
|
{
|
type: 'value',
|
name: '停留时长',
|
nameGap: 30,
|
},
|
],
|
series: [
|
{
|
name: 'PV',
|
type: 'line',
|
stack: '总量',
|
data: yData,
|
},
|
],
|
};
|
const chartInstance = getChartInstance(chartExplorationRef.value);
|
chartInstance.setOption(chartExploration);
|
chartInstance.resize();
|
};
|
const handleClick = (val) => {
|
const { startDate, endDate } = getDefaultPeriod('7Days');
|
const dateRange = [startDate.format('YYYY-MM-DD HH:mm:ss'), endDate.format('YYYY-MM-DD HH:mm:ss')];
|
state.exploreForm.dateRangeExplore = dateRange;
|
if (val == 'indicatorExploration') {
|
nextTick(() => {
|
if (state.chartStatus) {
|
resizeChart();
|
return;
|
}
|
initChartExploration();
|
state.chartStatus = true;
|
});
|
}
|
|
state.activeMetricName = val;
|
};
|
//#endregion
|
|
//#region ====================== 指标图谱 ======================
|
|
const filterDimList = computed(
|
() => currentMetrics?.value?.dimensions ?? [].filter((item) => item.filter_type === 'str_eq' && item.type === '字符串')
|
);
|
const dialogFormRules = ref<FormRules>({
|
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
|
prompt: [{ required: true, message: '请输入提示词', trigger: 'blur' }],
|
});
|
|
const summaryComRef = useCompRef(SummaryCom);
|
const dialogFormValue = ref({
|
rangValue: ['2024-10-21 00:00:00', '2024-10-23 23:59:59'],
|
groupDimList: [],
|
});
|
|
const currentMetricsId = computed(() => router.currentRoute.value.query.id as string);
|
const querySummaryData = ref([]);
|
const queryLoading = ref(false);
|
/**
|
* 按查询条件查找
|
*/
|
const query = async (isFirst: boolean) => {
|
if (!currentMetricsId.value) return;
|
const groupDimStr = dialogFormValue.value.groupDimList.join(',');
|
const sendGroupDims = groupDimStr || undefined;
|
queryLoading.value = true;
|
const res = await chatMetricsJsonByPost(
|
{
|
metrics_id: currentMetricsId.value,
|
start_time: dialogFormValue.value.rangValue[0],
|
end_time: dialogFormValue.value.rangValue[1],
|
group_dims: sendGroupDims,
|
filter_dims: JSON.stringify(_.omit(dialogFormValue.value, ['groupDimList', 'rangValue'])),
|
},
|
{
|
loading: false,
|
}
|
).finally(() => {
|
queryLoading.value = false;
|
});
|
querySummaryData.value = res?.summary ?? [];
|
if (!isFirst) {
|
nextTick(() => {
|
nextTick(() => {
|
summaryComRef.value.updateSummary();
|
});
|
});
|
}
|
};
|
const debounceQuery = debounce(query, 600);
|
const filterDimInput = (val, dim) => {
|
debounceQuery(false);
|
};
|
|
//#endregion
|
|
//#region ====================== 指标图谱 ======================
|
|
const maxCount = ref(null);
|
const graphData = ref(null);
|
const convertOrgTreeToTreeNode = (orgTreeData: OrgTreeItem) => {
|
const treeData = {
|
id: orgTreeData.treeId,
|
label: orgTreeData.label,
|
data: orgTreeData,
|
children: orgTreeData.children?.length > 0 ? orgTreeData.children.map((item) => convertOrgTreeToTreeNode(item)) : [],
|
};
|
return treeData;
|
};
|
const getGraphTreeData = () => {
|
if (!currentMetricsId.value) return;
|
/** @description 维度数量 */
|
let dimensionCount = 0;
|
/** @description 指标数量 */
|
let metricsCount = 1;
|
|
const metricsTreeId = `metrics-${currentMetricsId.value}`;
|
const dimensionList = currentMetrics.value.dimensions ?? [];
|
dimensionCount = dimensionList.length;
|
let logicTree: OrgTreeItem = {
|
treeId: metricsTreeId,
|
logicId: currentMetricsId.value,
|
type: 'metrics',
|
|
model: currentMetrics.value,
|
get label() {
|
return this.model.title;
|
},
|
level: 0,
|
children: dimensionList.map((item) => {
|
const dimensionTreeId = `${metricsTreeId}-dimension-${item.id}`;
|
return {
|
treeId: dimensionTreeId,
|
logicId: item.id,
|
type: 'dimension',
|
model: item,
|
get label() {
|
return this.model.title;
|
},
|
level: 1,
|
};
|
}),
|
};
|
const resData = logicTree;
|
maxCount.value = Math.max(dimensionCount, metricsCount);
|
graphData.value = convertOrgTreeToTreeNode(resData);
|
};
|
//#endregion
|
|
onMounted(async () => {
|
const { id } = route.query;
|
query(true);
|
initThemeDomainData();
|
await getTableData(id);
|
if (!graphData.value) {
|
getGraphTreeData();
|
}
|
});
|
</script>
|
<style scoped lang="scss">
|
:deep(.el-tabs__header) {
|
background-color: #fff;
|
border-radius: 8px;
|
}
|
:deep(.el-tabs__content) {
|
height: 100%;
|
background-color: #fff;
|
border-radius: 16px;
|
overflow-y: auto;
|
padding: 10px 20px;
|
box-sizing: border-box;
|
}
|
.set-right-height {
|
padding: 20px;
|
color: #344767;
|
background-color: #fff;
|
height: 100%;
|
border-radius: 6px;
|
.set-visit {
|
box-sizing: border-box;
|
column-gap: 8px;
|
row-gap: 8px;
|
align-items: center;
|
display: inline-flex;
|
font-weight: 600;
|
font-size: 18px;
|
}
|
.set-detail {
|
width: 100%;
|
height: calc(100% - 92px);
|
position: relative;
|
display: flex;
|
flex-direction: column;
|
overflow: scroll;
|
overflow: hidden;
|
background-image: none;
|
border-radius: 6px;
|
.section___vePzi {
|
padding: 0px 16px;
|
color: #344767;
|
line-height: 1.25;
|
background: transparent;
|
box-shadow: none;
|
opacity: 1;
|
.item___title {
|
padding: unset;
|
color: #344767;
|
background: transparent;
|
box-shadow: none;
|
opacity: 1;
|
span {
|
margin-left: 8px;
|
color: #344767;
|
font-weight: 600;
|
font-size: 16px;
|
line-height: 1.625;
|
letter-spacing: 0.0075em;
|
text-transform: capitalize;
|
text-decoration: none;
|
vertical-align: unset;
|
opacity: 1;
|
}
|
}
|
.item___txXyB {
|
display: flex;
|
padding-top: 8px;
|
padding-right: 16px;
|
padding-bottom: 8px;
|
color: #344767;
|
background: transparent;
|
box-shadow: none;
|
opacity: 1;
|
.item_name {
|
min-width: fit-content;
|
margin: 0 10px 0 0;
|
color: #344767;
|
font-weight: 700;
|
font-size: 14px;
|
line-height: 1.5;
|
letter-spacing: 0.02857em;
|
text-transform: capitalize;
|
text-decoration: none;
|
vertical-align: unset;
|
opacity: 1;
|
}
|
.item_value {
|
margin: 0;
|
color: #7b809a;
|
font-weight: 400;
|
font-size: 14px;
|
line-height: 1.5;
|
letter-spacing: 0.02857em;
|
text-transform: none;
|
text-decoration: none;
|
vertical-align: unset;
|
opacity: 1;
|
}
|
}
|
.item___bg {
|
position: relative;
|
margin: 0;
|
padding: 8px 0;
|
list-style: none;
|
background-color: #f9fafb;
|
.subTitle___zya5g {
|
margin: 0;
|
color: #7b809a;
|
font-weight: 700;
|
font-size: 14px;
|
line-height: 1.25;
|
letter-spacing: 0.03333em;
|
text-transform: uppercase;
|
text-decoration: none;
|
vertical-align: unset;
|
opacity: 1;
|
}
|
}
|
}
|
}
|
}
|
.activeColor {
|
border-color: #0062be;
|
background-color: #f6f5ff;
|
}
|
.list_btn {
|
outline: none;
|
position: absolute;
|
top: 0;
|
right: 0;
|
display: inline-block;
|
font-weight: 400;
|
white-space: nowrap;
|
text-align: center;
|
background-image: none;
|
background-color: transparent;
|
border: 1px solid transparent;
|
cursor: pointer;
|
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
user-select: none;
|
touch-action: manipulation;
|
line-height: 1.5714285714285714;
|
margin-top: 5px;
|
margin-right: 6px;
|
}
|
.set_explore {
|
position: relative;
|
overflow: hidden;
|
background-color: #fff;
|
background-image: none;
|
border-radius: 6px;
|
// box-shadow: #888 0 0 1px, #1d293914 0 1px 3px;
|
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
&_pad {
|
padding: 10px 10px 0px;
|
}
|
}
|
</style>
|