mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-15 14:36:47 +00:00
add drill down dimensions and metric period compare and modify layout (#22)
* [feature](webapp) add drill down dimensions and metric period compare and modify layout * [feature](webapp) add drill down dimensions and metric period compare and modify layout --------- Co-authored-by: williamhliu <williamhliu@tencent.com>
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import { MsgDataType, MsgValidTypeEnum } from '../../common/type';
|
||||
import { MsgDataType } from '../../common/type';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Typing from './Typing';
|
||||
import ChatMsg from '../ChatMsg';
|
||||
import { chatQuery } from '../../service';
|
||||
import { MSG_VALID_TIP, PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants';
|
||||
import { PARSE_ERROR_TIP, PREFIX_CLS } from '../../common/constants';
|
||||
import Text from './Text';
|
||||
import Tools from '../Tools';
|
||||
import SemanticDetail from '../SemanticDetail';
|
||||
import IconFont from '../IconFont';
|
||||
|
||||
type Props = {
|
||||
@@ -19,8 +18,6 @@ type Props = {
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
onMsgDataLoaded?: (data: MsgDataType) => void;
|
||||
onSelectSuggestion?: (value: string) => void;
|
||||
onUpdateMessageScroll?: () => void;
|
||||
};
|
||||
|
||||
const ChatItem: React.FC<Props> = ({
|
||||
@@ -33,12 +30,9 @@ const ChatItem: React.FC<Props> = ({
|
||||
triggerResize,
|
||||
msgData,
|
||||
onMsgDataLoaded,
|
||||
onSelectSuggestion,
|
||||
onUpdateMessageScroll,
|
||||
}) => {
|
||||
const [data, setData] = useState<MsgDataType>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [metricInfoList, setMetricInfoList] = useState<any[]>([]);
|
||||
const [tip, setTip] = useState('');
|
||||
|
||||
const updateData = (res: Result<MsgDataType>) => {
|
||||
@@ -51,8 +45,8 @@ const ChatItem: React.FC<Props> = ({
|
||||
return false;
|
||||
}
|
||||
const { queryColumns, queryResults, queryState, queryMode } = res.data || {};
|
||||
if (queryState !== MsgValidTypeEnum.NORMAL && queryState !== MsgValidTypeEnum.EMPTY) {
|
||||
setTip(MSG_VALID_TIP[queryState || MsgValidTypeEnum.INVALID]);
|
||||
if (queryState !== 'SUCCESS') {
|
||||
setTip(PARSE_ERROR_TIP);
|
||||
return false;
|
||||
}
|
||||
if ((queryColumns && queryColumns.length > 0 && queryResults) || queryMode === 'INSTRUCTION') {
|
||||
@@ -109,12 +103,9 @@ const ChatItem: React.FC<Props> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const onCheckMetricInfo = (data: any) => {
|
||||
setMetricInfoList([...metricInfoList, data]);
|
||||
if (onUpdateMessageScroll) {
|
||||
onUpdateMessageScroll();
|
||||
}
|
||||
};
|
||||
const isMetricCard =
|
||||
(data.queryMode === 'METRIC_DOMAIN' || data.queryMode === 'METRIC_FILTER') &&
|
||||
data.queryResults?.length === 1;
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
@@ -126,22 +117,9 @@ const ChatItem: React.FC<Props> = ({
|
||||
data={data}
|
||||
isMobileMode={isMobileMode}
|
||||
triggerResize={triggerResize}
|
||||
onCheckMetricInfo={onCheckMetricInfo}
|
||||
/>
|
||||
<Tools data={data} isLastMessage={isLastMessage} isMobileMode={isMobileMode} />
|
||||
{metricInfoList.length > 0 && (
|
||||
<div className={`${prefixCls}-metric-info-list`}>
|
||||
{metricInfoList.map(item => (
|
||||
<SemanticDetail
|
||||
dataSource={item}
|
||||
onDimensionSelect={(value: string) => {
|
||||
if (onSelectSuggestion) {
|
||||
onSelectSuggestion(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!isMetricCard && (
|
||||
<Tools data={data} isLastMessage={isLastMessage} isMobileMode={isMobileMode} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
}
|
||||
|
||||
&-content {
|
||||
// flex: 1;
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
|
||||
@@ -42,7 +41,7 @@
|
||||
|
||||
&-typing-bubble {
|
||||
width: fit-content;
|
||||
padding: 8px 16px !important;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
&-text-bubble {
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
import { CHART_BLUE_COLOR, CHART_SECONDARY_COLOR, PREFIX_CLS } from '../../../common/constants';
|
||||
import { MsgDataType } from '../../../common/type';
|
||||
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||
import { getChartLightenColor, getFormattedValue } from '../../../utils/utils';
|
||||
import type { ECharts } from 'echarts';
|
||||
import * as echarts from 'echarts';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import NoPermissionChart from '../NoPermissionChart';
|
||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||
import { Spin } from 'antd';
|
||||
import FilterSection from '../FilterSection';
|
||||
|
||||
type Props = {
|
||||
data: MsgDataType;
|
||||
triggerResize?: boolean;
|
||||
drillDownDimension?: DrillDownDimensionType;
|
||||
loading: boolean;
|
||||
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
||||
onApplyAuth?: (domain: string) => void;
|
||||
};
|
||||
|
||||
const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
const BarChart: React.FC<Props> = ({
|
||||
data,
|
||||
triggerResize,
|
||||
drillDownDimension,
|
||||
loading,
|
||||
onSelectDimension,
|
||||
onApplyAuth,
|
||||
}) => {
|
||||
const chartRef = useRef<any>();
|
||||
const [instance, setInstance] = useState<ECharts>();
|
||||
|
||||
const { queryColumns, queryResults, entityInfo } = data;
|
||||
const { queryColumns, queryResults, entityInfo, chatContext, queryMode } = data;
|
||||
|
||||
const { dateInfo } = chatContext || {};
|
||||
|
||||
const categoryColumnName =
|
||||
queryColumns?.find(column => column.showType === 'CATEGORY')?.nameEn || '';
|
||||
const metricColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||
@@ -35,13 +51,13 @@ const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
);
|
||||
const xData = data.map(item => item[categoryColumnName]);
|
||||
instanceObj.setOption({
|
||||
legend: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
icon: 'rect',
|
||||
itemWidth: 15,
|
||||
itemHeight: 5,
|
||||
},
|
||||
// legend: {
|
||||
// left: 0,
|
||||
// top: 0,
|
||||
// icon: 'rect',
|
||||
// itemWidth: 15,
|
||||
// itemHeight: 5,
|
||||
// },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
axisTick: {
|
||||
@@ -99,7 +115,7 @@ const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
left: '2%',
|
||||
right: '1%',
|
||||
bottom: '3%',
|
||||
top: 50,
|
||||
top: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
series: {
|
||||
@@ -150,7 +166,30 @@ const BarChart: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={`${PREFIX_CLS}-bar`} ref={chartRef} />;
|
||||
return (
|
||||
<div>
|
||||
<div className={`${PREFIX_CLS}-bar-metric-name`}>{metricColumn?.name}</div>
|
||||
<FilterSection chatContext={chatContext} />
|
||||
{dateInfo && (
|
||||
<div className={`${PREFIX_CLS}-bar-date-range`}>
|
||||
{dateInfo.startDate === dateInfo.endDate
|
||||
? dateInfo.startDate
|
||||
: `${dateInfo.startDate} ~ ${dateInfo.endDate}`}
|
||||
</div>
|
||||
)}
|
||||
<Spin spinning={loading}>
|
||||
<div className={`${PREFIX_CLS}-bar-chart`} ref={chartRef} />
|
||||
</Spin>
|
||||
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
||||
<DrillDownDimensions
|
||||
domainId={chatContext.domainId}
|
||||
drillDownDimension={drillDownDimension}
|
||||
dimensionFilters={chatContext.dimensionFilters}
|
||||
onSelectDimension={onSelectDimension}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BarChart;
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
@bar-cls: ~'@{supersonic-chat-prefix}-bar';
|
||||
|
||||
.@{bar-cls} {
|
||||
height: 270px;
|
||||
margin-top: 16px;
|
||||
|
||||
&-chart {
|
||||
height: 260px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&-metric-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-date-range {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { ChatContextType } from '../../../common/type';
|
||||
|
||||
type Props = {
|
||||
chatContext?: ChatContextType;
|
||||
};
|
||||
|
||||
const FilterSection: React.FC<Props> = ({ chatContext }) => {
|
||||
const prefixCls = `${PREFIX_CLS}-filter-section`;
|
||||
|
||||
const { dimensionFilters } = chatContext || {};
|
||||
|
||||
const hasFilterSection = dimensionFilters && dimensionFilters.length > 0;
|
||||
|
||||
return hasFilterSection ? (
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-field-label`}>筛选条件:</div>
|
||||
<div className={`${prefixCls}-filter-values`}>
|
||||
{dimensionFilters.map(filterItem => {
|
||||
const filterValue =
|
||||
typeof filterItem.value === 'string' ? [filterItem.value] : filterItem.value || [];
|
||||
return (
|
||||
<div
|
||||
className={`${prefixCls}-filter-item`}
|
||||
key={filterItem.name}
|
||||
title={filterValue.join('、')}
|
||||
>
|
||||
<span className={`${prefixCls}-field-name`}>{filterItem.name}:</span>
|
||||
<span className={`${prefixCls}-filter-value`}>{filterValue.join('、')}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default FilterSection;
|
||||
@@ -0,0 +1,33 @@
|
||||
@import '../../../styles/index.less';
|
||||
|
||||
@filter-section-prefix-cls: ~'@{supersonic-chat-prefix}-filter-section';
|
||||
|
||||
.@{filter-section-prefix-cls} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
|
||||
&-filter-values {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
}
|
||||
|
||||
&-filter-item {
|
||||
padding: 2px 12px;
|
||||
color: var(--text-color-third);
|
||||
background-color: #edf2f2;
|
||||
border-radius: 13px;
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-filter-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,14 @@ const Message: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
{domainName && <div className={`${prefixCls}-domain-name`}>{domainName}</div>}
|
||||
<div className={`${prefixCls}-title-bar`}>
|
||||
{domainName && <div className={`${prefixCls}-domain-name`}>{domainName}</div>}
|
||||
{position === 'left' && leftTitle && (
|
||||
<div className={`${prefixCls}-top-bar`} title={leftTitle}>
|
||||
({leftTitle})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${prefixCls}-content`}>
|
||||
<div className={`${prefixCls}-body`}>
|
||||
<div
|
||||
@@ -75,14 +82,9 @@ const Message: React.FC<Props> = ({
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{position === 'left' && title && (
|
||||
<div className={`${prefixCls}-top-bar`} title={leftTitle}>
|
||||
{leftTitle}
|
||||
</div>
|
||||
)}
|
||||
{(entityInfoList.length > 0 || hasFilterSection) && (
|
||||
{entityInfoList.length > 0 && (
|
||||
<div className={`${prefixCls}-info-bar`}>
|
||||
{filterSection}
|
||||
{/* {filterSection} */}
|
||||
{entityInfoList.length > 0 && (
|
||||
<div className={`${prefixCls}-main-entity-info`}>
|
||||
{entityInfoList.slice(0, 4).map(dimension => {
|
||||
|
||||
@@ -3,11 +3,28 @@
|
||||
@msg-prefix-cls: ~'@{supersonic-chat-prefix}-message';
|
||||
|
||||
.@{msg-prefix-cls} {
|
||||
&-title-bar {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
column-gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&-domain-name {
|
||||
color: var(--text-color);
|
||||
margin-bottom: 2px;
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&-top-bar {
|
||||
position: relative;
|
||||
max-width: 80%;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-content {
|
||||
@@ -20,6 +37,7 @@
|
||||
}
|
||||
|
||||
&-bubble {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
min-width: 1px;
|
||||
max-width: 100%;
|
||||
@@ -30,18 +48,6 @@
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&-top-bar {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
padding: 4px 0 8px;
|
||||
color: var(--text-color-third);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
&-filter-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -77,7 +83,7 @@
|
||||
align-items: center;
|
||||
row-gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
margin-top: 4px;
|
||||
column-gap: 20px;
|
||||
color: var(--text-color-secondary);
|
||||
background: rgba(133, 156, 241, 0.1);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import classNames from 'classnames';
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import IconFont from '../../IconFont';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const PeriodCompareItem: React.FC<Props> = ({ title, value }) => {
|
||||
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
||||
|
||||
const itemValueClass = classNames(`${prefixCls}-period-compare-item-value`, {
|
||||
[`${prefixCls}-period-compare-item-value-up`]: !value.includes('-'),
|
||||
[`${prefixCls}-period-compare-item-value-down`]: value.includes('-'),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`${prefixCls}-period-compare-item`}>
|
||||
<div className={`${prefixCls}-period-compare-item-title`}>{title}</div>
|
||||
<div className={itemValueClass}>
|
||||
<IconFont type={!value.includes('-') ? 'icon-shangsheng' : 'icon-xiajiang'} />
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PeriodCompareItem;
|
||||
@@ -1,36 +1,77 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { getFormattedValue } from '../../../utils/utils';
|
||||
import { formatByThousandSeperator } from '../../../utils/utils';
|
||||
import ApplyAuth from '../ApplyAuth';
|
||||
import { MsgDataType } from '../../../common/type';
|
||||
import { DrillDownDimensionType, MsgDataType } from '../../../common/type';
|
||||
import PeriodCompareItem from './PeriodCompareItem';
|
||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||
import { Spin } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
data: MsgDataType;
|
||||
drillDownDimension?: DrillDownDimensionType;
|
||||
loading: boolean;
|
||||
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
||||
onApplyAuth?: (domain: string) => void;
|
||||
};
|
||||
|
||||
const MetricCard: React.FC<Props> = ({ data, onApplyAuth }) => {
|
||||
const { queryColumns, queryResults, entityInfo } = data;
|
||||
const MetricCard: React.FC<Props> = ({
|
||||
data,
|
||||
drillDownDimension,
|
||||
loading,
|
||||
onSelectDimension,
|
||||
onApplyAuth,
|
||||
}) => {
|
||||
const { queryMode, queryColumns, queryResults, entityInfo, aggregateInfo, chatContext } = data;
|
||||
|
||||
const { metricInfos } = aggregateInfo || {};
|
||||
const { dateInfo } = chatContext || {};
|
||||
const { startDate, endDate } = dateInfo || {};
|
||||
|
||||
const indicatorColumn = queryColumns?.find(column => column.showType === 'NUMBER');
|
||||
const indicatorColumnName = indicatorColumn?.nameEn || '';
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-metric-card`;
|
||||
|
||||
const indicatorClass = classNames(`${prefixCls}-indicator`, {
|
||||
[`${prefixCls}-indicator-period-compare`]: metricInfos?.length > 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-indicator`}>
|
||||
{/* <div className={`${prefixCls}-date-range`}>
|
||||
{startTime === endTime ? startTime : `${startTime} ~ ${endTime}`}
|
||||
</div> */}
|
||||
{indicatorColumn && !indicatorColumn?.authorized ? (
|
||||
<ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} />
|
||||
) : (
|
||||
<div className={`${prefixCls}-indicator-value`}>
|
||||
{getFormattedValue(queryResults?.[0]?.[indicatorColumnName])}
|
||||
<div className={`${prefixCls}-indicator-name`}>{indicatorColumn?.name}</div>
|
||||
<Spin spinning={loading}>
|
||||
<div className={indicatorClass}>
|
||||
<div className={`${prefixCls}-date-range`}>
|
||||
{startDate === endDate ? startDate : `${startDate} ~ ${endDate}`}
|
||||
</div>
|
||||
)}
|
||||
{/* <div className={`${prefixCls}-indicator-name`}>{query}</div> */}
|
||||
</div>
|
||||
{indicatorColumn && !indicatorColumn?.authorized ? (
|
||||
<ApplyAuth domain={entityInfo?.domainInfo.name || ''} onApplyAuth={onApplyAuth} />
|
||||
) : (
|
||||
<div className={`${prefixCls}-indicator-value`}>
|
||||
{formatByThousandSeperator(queryResults?.[0]?.[indicatorColumnName])}
|
||||
</div>
|
||||
)}
|
||||
{metricInfos?.length > 0 && (
|
||||
<div className={`${prefixCls}-period-compare`}>
|
||||
{Object.keys(metricInfos[0].statistics).map((key: any) => (
|
||||
<PeriodCompareItem title={key} value={metricInfos[0].statistics[key]} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
||||
<div className={`${prefixCls}-drill-down-dimensions`}>
|
||||
<DrillDownDimensions
|
||||
domainId={chatContext.domainId}
|
||||
dimensionFilters={chatContext.dimensionFilters}
|
||||
drillDownDimension={drillDownDimension}
|
||||
onSelectDimension={onSelectDimension}
|
||||
isMetricCard
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,34 +3,78 @@
|
||||
@metric-card-prefix-cls: ~'@{supersonic-chat-prefix}-metric-card';
|
||||
|
||||
.@{metric-card-prefix-cls} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
height: 130px;
|
||||
row-gap: 4px;
|
||||
|
||||
&-indicator-name {
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&-indicator-period-compare {
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-date-range {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&-indicator-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
font-size: 40px;
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
|
||||
&-indicator-name {
|
||||
&-period-compare {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 40px;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&-period-compare-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
&-period-compare-item-title {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-period-compare-item-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-period-compare-item-value-up {
|
||||
color: rgb(252, 103, 114);
|
||||
}
|
||||
|
||||
&-period-compare-item-value-down {
|
||||
color: rgb(45, 202, 147);
|
||||
}
|
||||
|
||||
&-drill-down-dimensions {
|
||||
position: absolute;
|
||||
bottom: -38px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PREFIX_CLS } from '../../../common/constants';
|
||||
import { formatByThousandSeperator } from '../../../utils/utils';
|
||||
import { AggregateInfoType } from '../../../common/type';
|
||||
import PeriodCompareItem from '../MetricCard/PeriodCompareItem';
|
||||
|
||||
type Props = {
|
||||
aggregateInfo: AggregateInfoType;
|
||||
};
|
||||
|
||||
const MetricInfo: React.FC<Props> = ({ aggregateInfo }) => {
|
||||
const { metricInfos } = aggregateInfo || {};
|
||||
const metricInfo = metricInfos?.[0] || {};
|
||||
const { date, value, statistics } = metricInfo || {};
|
||||
|
||||
const prefixCls = `${PREFIX_CLS}-metric-info`;
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-indicator`}>
|
||||
<div className={`${prefixCls}-date`}>{date}</div>
|
||||
<div className={`${prefixCls}-indicator-value`}>{formatByThousandSeperator(value)}</div>
|
||||
{metricInfos?.length > 0 && (
|
||||
<div className={`${prefixCls}-period-compare`}>
|
||||
{Object.keys(statistics).map((key: any) => (
|
||||
<PeriodCompareItem title={key} value={metricInfos[0].statistics[key]} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricInfo;
|
||||
@@ -1,24 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CLS_PREFIX, DATE_TYPES } from '../../../common/constants';
|
||||
import { ColumnType, FieldType, MsgDataType } from '../../../common/type';
|
||||
import { ColumnType, DrillDownDimensionType, FieldType, MsgDataType } from '../../../common/type';
|
||||
import { isMobile } from '../../../utils/utils';
|
||||
import { queryData } from '../../../service';
|
||||
import MetricTrendChart from './MetricTrendChart';
|
||||
import classNames from 'classnames';
|
||||
import { Spin } from 'antd';
|
||||
import Table from '../Table';
|
||||
import DrillDownDimensions from '../../DrillDownDimensions';
|
||||
import MetricInfo from './MetricInfo';
|
||||
import FilterSection from '../FilterSection';
|
||||
import moment from 'moment';
|
||||
|
||||
type Props = {
|
||||
data: MsgDataType;
|
||||
triggerResize?: boolean;
|
||||
onApplyAuth?: (domain: string) => void;
|
||||
onCheckMetricInfo?: (data: any) => void;
|
||||
};
|
||||
|
||||
const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onCheckMetricInfo }) => {
|
||||
const { queryColumns, queryResults, entityInfo, chatContext } = data;
|
||||
const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth }) => {
|
||||
const { queryColumns, queryResults, entityInfo, chatContext, queryMode, aggregateInfo } = data;
|
||||
|
||||
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES[0];
|
||||
const dateOptions = DATE_TYPES[chatContext?.dateInfo?.period] || DATE_TYPES.DAY;
|
||||
const initialDateOption = dateOptions.find(
|
||||
(option: any) => option.value === chatContext?.dateInfo?.unit
|
||||
)?.value;
|
||||
@@ -29,6 +32,7 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
||||
const [activeMetricField, setActiveMetricField] = useState<FieldType>(chatContext.metrics?.[0]);
|
||||
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
||||
const [currentDateOption, setCurrentDateOption] = useState<number>(initialDateOption);
|
||||
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const dateField: any = columns.find(
|
||||
@@ -57,9 +61,21 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
||||
|
||||
const selectDateOption = (dateOption: number) => {
|
||||
setCurrentDateOption(dateOption);
|
||||
const endDate = moment().subtract(1, 'days').format('YYYY-MM-DD');
|
||||
const startDate = moment(endDate)
|
||||
.subtract(dateOption - 1, 'days')
|
||||
.format('YYYY-MM-DD');
|
||||
onLoadData({
|
||||
metrics: [activeMetricField],
|
||||
dateInfo: { ...chatContext?.dateInfo, unit: dateOption },
|
||||
dimensions: drillDownDimension
|
||||
? [...(chatContext.dimensions || []), drillDownDimension]
|
||||
: undefined,
|
||||
dateInfo: {
|
||||
...chatContext?.dateInfo,
|
||||
startDate,
|
||||
endDate,
|
||||
unit: dateOption,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -67,10 +83,23 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
||||
setActiveMetricField(metricField);
|
||||
onLoadData({
|
||||
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
||||
dimensions: drillDownDimension
|
||||
? [...(chatContext.dimensions || []), drillDownDimension]
|
||||
: undefined,
|
||||
metrics: [metricField],
|
||||
});
|
||||
};
|
||||
|
||||
const onSelectDimension = (dimension?: DrillDownDimensionType) => {
|
||||
setDrillDownDimension(dimension);
|
||||
onLoadData({
|
||||
dateInfo: { ...chatContext.dateInfo, unit: currentDateOption || chatContext.dateInfo.unit },
|
||||
metrics: [activeMetricField],
|
||||
dimensions:
|
||||
dimension === undefined ? undefined : [...(chatContext.dimensions || []), dimension],
|
||||
});
|
||||
};
|
||||
|
||||
if (!currentMetricField) {
|
||||
return null;
|
||||
}
|
||||
@@ -80,6 +109,33 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<div className={`${prefixCls}-charts`}>
|
||||
{chatContext.metrics.length > 0 && (
|
||||
<div className={`${prefixCls}-metric-fields`}>
|
||||
{chatContext.metrics.map((metricField: FieldType) => {
|
||||
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
||||
[`${prefixCls}-metric-field-active`]:
|
||||
activeMetricField?.bizName === metricField.bizName &&
|
||||
chatContext.metrics.length > 1,
|
||||
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={metricFieldClass}
|
||||
key={metricField.bizName}
|
||||
onClick={() => {
|
||||
if (chatContext.metrics.length > 1) {
|
||||
onSwitchMetric(metricField);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{metricField.name}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{aggregateInfo?.metricInfos?.length > 0 && <MetricInfo aggregateInfo={aggregateInfo} />}
|
||||
<FilterSection chatContext={chatContext} />
|
||||
<div className={`${prefixCls}-date-options`}>
|
||||
{dateOptions.map((dateOption: { label: string; value: number }, index: number) => {
|
||||
const dateOptionClass = classNames(`${prefixCls}-date-option`, {
|
||||
@@ -107,41 +163,10 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{chatContext.metrics.length > 0 && (
|
||||
<div className={`${prefixCls}-metric-fields`}>
|
||||
{chatContext.metrics.map((metricField: FieldType) => {
|
||||
const metricFieldClass = classNames(`${prefixCls}-metric-field`, {
|
||||
[`${prefixCls}-metric-field-active`]:
|
||||
activeMetricField?.bizName === metricField.bizName &&
|
||||
chatContext.metrics.length > 1,
|
||||
[`${prefixCls}-metric-field-single`]: chatContext.metrics.length === 1,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={metricFieldClass}
|
||||
key={metricField.bizName}
|
||||
onClick={() => {
|
||||
if (chatContext.metrics.length > 1) {
|
||||
onSwitchMetric(metricField);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* <SemanticInfoPopover
|
||||
classId={chatContext.domainId}
|
||||
uniqueId={metricField.bizName}
|
||||
onDetailBtnClick={onCheckMetricInfo}
|
||||
> */}
|
||||
{metricField.name}
|
||||
{/* </SemanticInfoPopover> */}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{dataSource?.length === 1 ? (
|
||||
<Table data={data} onApplyAuth={onApplyAuth} />
|
||||
) : (
|
||||
<Spin spinning={loading}>
|
||||
<Spin spinning={loading}>
|
||||
{dataSource?.length === 1 ? (
|
||||
<Table data={{ ...data, queryResults: dataSource }} onApplyAuth={onApplyAuth} />
|
||||
) : (
|
||||
<MetricTrendChart
|
||||
domain={entityInfo?.domainInfo.name}
|
||||
dateColumnName={dateColumnName}
|
||||
@@ -151,7 +176,15 @@ const MetricTrend: React.FC<Props> = ({ data, triggerResize, onApplyAuth, onChec
|
||||
triggerResize={triggerResize}
|
||||
onApplyAuth={onApplyAuth}
|
||||
/>
|
||||
</Spin>
|
||||
)}
|
||||
</Spin>
|
||||
{(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && (
|
||||
<DrillDownDimensions
|
||||
domainId={chatContext.domainId}
|
||||
drillDownDimension={drillDownDimension}
|
||||
dimensionFilters={chatContext.dimensionFilters}
|
||||
onSelectDimension={onSelectDimension}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
@metric-trend-prefix-cls: ~'@{supersonic-chat-prefix}-metric-trend';
|
||||
|
||||
@metric-info-prefix-cls: ~'@{supersonic-chat-prefix}-metric-info';
|
||||
|
||||
.@{metric-trend-prefix-cls} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
row-gap: 4px;
|
||||
|
||||
@@ -35,14 +37,15 @@
|
||||
}
|
||||
|
||||
&-flow-trend-chart {
|
||||
height: 270px;
|
||||
margin-top: 4px;
|
||||
height: 230px;
|
||||
}
|
||||
|
||||
&-charts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
row-gap: 16px;
|
||||
row-gap: 12px;
|
||||
}
|
||||
|
||||
&-metric-fields {
|
||||
@@ -50,6 +53,8 @@
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
row-gap: 12px;
|
||||
color: var(--text-color);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&-metric-field {
|
||||
@@ -85,10 +90,11 @@
|
||||
padding-left: 0;
|
||||
font-weight: 500;
|
||||
cursor: default;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 15px;
|
||||
color: var(--text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,3 +139,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.@{metric-info-prefix-cls} {
|
||||
&-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&-date {
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-indicator-value {
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
font-size: 36px;
|
||||
line-height: 40px;
|
||||
margin-top: 2px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
&-period-compare {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 20px;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ import Message from './Message';
|
||||
import MetricCard from './MetricCard';
|
||||
import MetricTrend from './MetricTrend';
|
||||
import Table from './Table';
|
||||
import { MsgDataType } from '../../common/type';
|
||||
import { ColumnType, DrillDownDimensionType, MsgDataType } from '../../common/type';
|
||||
import { useState } from 'react';
|
||||
import { queryData } from '../../service';
|
||||
|
||||
type Props = {
|
||||
question: string;
|
||||
@@ -12,7 +14,6 @@ type Props = {
|
||||
data: MsgDataType;
|
||||
isMobileMode?: boolean;
|
||||
triggerResize?: boolean;
|
||||
onCheckMetricInfo?: (data: any) => void;
|
||||
};
|
||||
|
||||
const ChatMsg: React.FC<Props> = ({
|
||||
@@ -21,48 +22,95 @@ const ChatMsg: React.FC<Props> = ({
|
||||
data,
|
||||
isMobileMode,
|
||||
triggerResize,
|
||||
onCheckMetricInfo,
|
||||
}) => {
|
||||
const { queryColumns, queryResults, chatContext, entityInfo, queryMode } = data;
|
||||
|
||||
const [columns, setColumns] = useState<ColumnType[]>(queryColumns);
|
||||
const [dataSource, setDataSource] = useState<any[]>(queryResults);
|
||||
|
||||
const [drillDownDimension, setDrillDownDimension] = useState<DrillDownDimensionType>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (!queryColumns || !queryResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const singleData = queryResults.length === 1;
|
||||
const dateField = queryColumns.find(item => item.showType === 'DATE' || item.type === 'DATE');
|
||||
const categoryField = queryColumns.filter(item => item.showType === 'CATEGORY');
|
||||
const metricFields = queryColumns.filter(item => item.showType === 'NUMBER');
|
||||
const singleData = dataSource.length === 1;
|
||||
const dateField = columns.find(item => item.showType === 'DATE' || item.type === 'DATE');
|
||||
const categoryField = columns.filter(item => item.showType === 'CATEGORY');
|
||||
const metricFields = columns.filter(item => item.showType === 'NUMBER');
|
||||
|
||||
const isMetricCard =
|
||||
(queryMode === 'METRIC_DOMAIN' || queryMode === 'METRIC_FILTER') && singleData;
|
||||
|
||||
const onLoadData = async (value: any) => {
|
||||
setLoading(true);
|
||||
const { data } = await queryData({
|
||||
...chatContext,
|
||||
...value,
|
||||
});
|
||||
setLoading(false);
|
||||
if (data.code === 200) {
|
||||
setColumns(data.data?.queryColumns || []);
|
||||
setDataSource(data.data?.queryResults || []);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectDimension = (dimension?: DrillDownDimensionType) => {
|
||||
setDrillDownDimension(dimension);
|
||||
onLoadData({
|
||||
dimensions:
|
||||
dimension === undefined ? undefined : [...(chatContext.dimensions || []), dimension],
|
||||
});
|
||||
};
|
||||
|
||||
const getMsgContent = () => {
|
||||
if (isMetricCard) {
|
||||
return (
|
||||
<MetricCard
|
||||
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||
loading={loading}
|
||||
drillDownDimension={drillDownDimension}
|
||||
onSelectDimension={onSelectDimension}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (
|
||||
categoryField.length > 1 ||
|
||||
queryMode === 'ENTITY_DETAIL' ||
|
||||
queryMode === 'ENTITY_DIMENSION' ||
|
||||
(categoryField.length === 1 && metricFields.length === 0)
|
||||
) {
|
||||
return <Table data={data} />;
|
||||
return <Table data={{ ...data, queryColumns: columns, queryResults: dataSource }} />;
|
||||
}
|
||||
if (dateField && metricFields.length > 0) {
|
||||
return (
|
||||
<MetricTrend
|
||||
data={data}
|
||||
triggerResize={triggerResize}
|
||||
onCheckMetricInfo={onCheckMetricInfo}
|
||||
/>
|
||||
);
|
||||
if (!dataSource.every(item => item[dateField.nameEn] === dataSource[0][dateField.nameEn])) {
|
||||
return (
|
||||
<MetricTrend
|
||||
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||
triggerResize={triggerResize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (singleData) {
|
||||
return <MetricCard data={data} />;
|
||||
}
|
||||
return <Bar data={data} triggerResize={triggerResize} />;
|
||||
return (
|
||||
<Bar
|
||||
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||
triggerResize={triggerResize}
|
||||
loading={loading}
|
||||
drillDownDimension={drillDownDimension}
|
||||
onSelectDimension={onSelectDimension}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
let width = '100%';
|
||||
if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
||||
if (queryColumns.length === 1) {
|
||||
if (isMetricCard) {
|
||||
width = '370px';
|
||||
} else if (categoryField.length > 1 && !isMobile && !isMobileMode) {
|
||||
if (columns.length === 1) {
|
||||
width = '600px';
|
||||
} else if (queryColumns.length === 2) {
|
||||
} else if (columns.length === 2) {
|
||||
width = '1000px';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
import { DrillDownDimensionType, FilterItemType } from '../../common/type';
|
||||
import { queryDrillDownDimensions } from '../../service';
|
||||
import { Dropdown } from 'antd';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
domainId: number;
|
||||
drillDownDimension?: DrillDownDimensionType;
|
||||
isMetricCard?: boolean;
|
||||
dimensionFilters?: FilterItemType[];
|
||||
onSelectDimension: (dimension?: DrillDownDimensionType) => void;
|
||||
};
|
||||
|
||||
const DEFAULT_DIMENSION_COUNT = 5;
|
||||
const MAX_DIMENSION_COUNT = 20;
|
||||
|
||||
const DrillDownDimensions: React.FC<Props> = ({
|
||||
domainId,
|
||||
drillDownDimension,
|
||||
isMetricCard,
|
||||
dimensionFilters,
|
||||
onSelectDimension,
|
||||
}) => {
|
||||
const [dimensions, setDimensions] = useState<DrillDownDimensionType[]>([]);
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-drill-down-dimensions`;
|
||||
|
||||
const initData = async () => {
|
||||
const res = await queryDrillDownDimensions(domainId);
|
||||
setDimensions(
|
||||
res.data.data.dimensions
|
||||
.filter(dimension => !dimensionFilters?.some(filter => filter.name === dimension.name))
|
||||
.slice(0, MAX_DIMENSION_COUNT)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
const cancelDrillDown = () => {
|
||||
onSelectDimension(undefined);
|
||||
};
|
||||
|
||||
const defaultDimensions = dimensions.slice(0, DEFAULT_DIMENSION_COUNT);
|
||||
|
||||
const drillDownDimensionsSectionClass = classNames(`${prefixCls}-section`, {
|
||||
[`${prefixCls}-metric-card`]: isMetricCard,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={prefixCls}>
|
||||
<div className={drillDownDimensionsSectionClass}>
|
||||
<div className={`${prefixCls}-title`}>快速维度下钻:</div>
|
||||
<div className={`${prefixCls}-content`}>
|
||||
{defaultDimensions.map((dimension, index) => {
|
||||
const itemNameClass = classNames(`${prefixCls}-content-item-name`, {
|
||||
[`${prefixCls}-content-item-active`]: drillDownDimension?.id === dimension.id,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<span
|
||||
className={itemNameClass}
|
||||
onClick={() => {
|
||||
onSelectDimension(
|
||||
drillDownDimension?.id === dimension.id ? undefined : dimension
|
||||
);
|
||||
}}
|
||||
>
|
||||
{dimension.name}
|
||||
</span>
|
||||
{index !== defaultDimensions.length - 1 && <span>、</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{dimensions.length > DEFAULT_DIMENSION_COUNT && (
|
||||
<div>
|
||||
<span>、</span>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: dimensions.slice(DEFAULT_DIMENSION_COUNT).map(dimension => {
|
||||
const itemNameClass = classNames({
|
||||
[`${prefixCls}-menu-item-active`]: drillDownDimension?.id === dimension.id,
|
||||
});
|
||||
return {
|
||||
label: (
|
||||
<span
|
||||
className={itemNameClass}
|
||||
onClick={() => {
|
||||
onSelectDimension(dimension);
|
||||
}}
|
||||
>
|
||||
{dimension.name}
|
||||
</span>
|
||||
),
|
||||
key: dimension.id,
|
||||
};
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className={`${prefixCls}-content-item-name`}>更多</span>
|
||||
<DownOutlined className={`${prefixCls}-down-arrow`} />
|
||||
</span>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
{drillDownDimension && (
|
||||
<div className={`${prefixCls}-cancel-drill-down`} onClick={cancelDrillDown}>
|
||||
取消下钻
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrillDownDimensions;
|
||||
@@ -0,0 +1,74 @@
|
||||
@import '../../styles/index.less';
|
||||
|
||||
@drill-down-dimensions-prefix-cls: ~'@{supersonic-chat-prefix}-drill-down-dimensions';
|
||||
|
||||
.@{drill-down-dimensions-prefix-cls} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 6px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&-metric-card {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 0 2px rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
width: fit-content;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: var(--text-color-third);
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-content-item-name {
|
||||
color: var(--chat-blue);
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid var(--chat-blue);
|
||||
padding: 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-content-item-active {
|
||||
color: #fff;
|
||||
border-bottom: none;
|
||||
background-color: var(--chat-blue);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&-menu-item-active {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
|
||||
&-down-arrow {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
|
||||
&-cancel-drill-down {
|
||||
margin-left: 20px;
|
||||
color: var(--text-color-third);
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
border: 1px solid var(--text-color-third);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
border-color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_hsbqfckf8tl.js',
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4120566_imm6kslj5s.js',
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
import { Row, Col } from 'antd';
|
||||
|
||||
type Props = {
|
||||
description: string;
|
||||
basicInfoList: any[];
|
||||
};
|
||||
|
||||
const BasicInfoSection: React.FC<Props> = ({ description = '', basicInfoList }) => {
|
||||
const prefixCls = `${CLS_PREFIX}-semantic-detail`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${prefixCls}-info-bar`}>
|
||||
<div className={`${prefixCls}-main-entity-info`}>
|
||||
{basicInfoList.map(item => {
|
||||
return (
|
||||
<div className={`${prefixCls}-info-item`}>
|
||||
<div className={`${prefixCls}-info-name`}>{item.name}:</div>
|
||||
<div className={`${prefixCls}-info-value`}>{item.value}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{description && (
|
||||
<>
|
||||
<Row>
|
||||
<Col flex="0 0 52px">
|
||||
<div className={`${prefixCls}-description`}> 口径: </div>
|
||||
</Col>
|
||||
<Col flex="1 1 auto">
|
||||
<div className={`${prefixCls}-description`}>{description}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicInfoSection;
|
||||
@@ -1,97 +0,0 @@
|
||||
import { message, Row, Col } from 'antd';
|
||||
import { isMobile } from '../../utils/utils';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getRelatedDimensionFromStatInfo } from '../../service';
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
|
||||
type Props = {
|
||||
classId?: number;
|
||||
uniqueId: string | number;
|
||||
onSelect?: (value: string) => void;
|
||||
};
|
||||
const PAGE_SIZE = isMobile ? 3 : 10;
|
||||
|
||||
const DimensionSection: React.FC<Props> = ({ classId, uniqueId, onSelect }) => {
|
||||
const [dimensions, setDimensions] = useState<string[]>([]);
|
||||
const [dimensionIndex, setDimensionIndex] = useState(0);
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-semantic-detail`;
|
||||
|
||||
const queryDimensionList = async () => {
|
||||
const { data: resData } = await getRelatedDimensionFromStatInfo({
|
||||
classId,
|
||||
uniqueId,
|
||||
});
|
||||
const { code, data, msg } = resData;
|
||||
if (code === '0') {
|
||||
setDimensions(
|
||||
data.map(item => {
|
||||
return item.name;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
queryDimensionList();
|
||||
}, []);
|
||||
|
||||
const reloadDimensionCmds = () => {
|
||||
const dimensionPageCount = Math.ceil(dimensions.length / PAGE_SIZE);
|
||||
setDimensionIndex((dimensionIndex + 1) % dimensionPageCount);
|
||||
};
|
||||
|
||||
const dimensionList = dimensions.slice(
|
||||
dimensionIndex * PAGE_SIZE,
|
||||
(dimensionIndex + 1) * PAGE_SIZE
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{dimensionList.length > 0 && (
|
||||
<div className={`${prefixCls}-content-section`}>
|
||||
<Row>
|
||||
<Col flex="0 0 80px">
|
||||
<div className={`${prefixCls}-label`}> 常用维度:</div>
|
||||
</Col>
|
||||
<Col flex="1 1" className={`${prefixCls}-content-col`}>
|
||||
<div className={`${prefixCls}-content-col-box`}>
|
||||
{dimensionList.map((dimension, index) => {
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
className={`${prefixCls}-section-item`}
|
||||
onClick={() => {
|
||||
onSelect?.(dimension);
|
||||
}}
|
||||
>
|
||||
{dimension}
|
||||
</span>
|
||||
{index < dimensionList.length - 1 && '、'}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Col>
|
||||
<Col flex="0 1 50px">
|
||||
<div
|
||||
className={`${prefixCls}-reload`}
|
||||
onClick={() => {
|
||||
reloadDimensionCmds();
|
||||
}}
|
||||
>
|
||||
<ReloadOutlined className={`${prefixCls}-reload-icon`} />
|
||||
{!isMobile && <div className={`${prefixCls}-reload-label`}>换一批</div>}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DimensionSection;
|
||||
@@ -1,112 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getMetricQueryInfo } from '../../service';
|
||||
import { message, Row, Col } from 'antd';
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
|
||||
type Props = {
|
||||
classId: number;
|
||||
metricName: string;
|
||||
onSelect?: (value: string) => void;
|
||||
};
|
||||
|
||||
const RecommendQuestions: React.FC<Props> = ({ classId, metricName, onSelect }) => {
|
||||
const [moreMode, setMoreMode] = useState<boolean>(false);
|
||||
const [questionData, setQuestionData] = useState<any[]>([]);
|
||||
|
||||
const prefixCls = `${CLS_PREFIX}-semantic-detail`;
|
||||
|
||||
const queryMetricQueryInfo = async () => {
|
||||
const { data: resData } = await getMetricQueryInfo({
|
||||
classId,
|
||||
metricName,
|
||||
});
|
||||
const { code, data, msg } = resData;
|
||||
if (code === '0') {
|
||||
setQuestionData(data);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
queryMetricQueryInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`${prefixCls}-recommend-questions`}>
|
||||
<div className={`${prefixCls}-header`}>
|
||||
<Row>
|
||||
<Col flex="0 0 85px">
|
||||
<div className={`${prefixCls}-label`}> 大家都在问: </div>
|
||||
</Col>
|
||||
<Col flex="1 1" className={`${prefixCls}-content-col`}>
|
||||
{!moreMode && (
|
||||
<div className={`${prefixCls}-content-col-box`}>
|
||||
{questionData.slice(0, 5).map((item, index) => {
|
||||
const { question } = item;
|
||||
return (
|
||||
<>
|
||||
{index !== 0 && '、'}
|
||||
<span
|
||||
key={question}
|
||||
className={`${prefixCls}-question`}
|
||||
onClick={() => {
|
||||
onSelect?.(question);
|
||||
}}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: question }} />
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col flex="0 1 30px">
|
||||
{!moreMode ? (
|
||||
<span
|
||||
onClick={() => {
|
||||
setMoreMode(true);
|
||||
}}
|
||||
className={`${prefixCls}-more`}
|
||||
>
|
||||
更多
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`${prefixCls}-more`}
|
||||
onClick={() => {
|
||||
setMoreMode(false);
|
||||
}}
|
||||
>
|
||||
收起
|
||||
</span>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
{moreMode && (
|
||||
<div className={`${prefixCls}-recommend-questions-content`}>
|
||||
<div className={`${prefixCls}-questions`}>
|
||||
{questionData.map(item => {
|
||||
const { question } = item;
|
||||
return (
|
||||
<div
|
||||
key={question}
|
||||
className={`${prefixCls}-question`}
|
||||
onClick={() => {
|
||||
onSelect?.(question);
|
||||
}}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: question }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendQuestions;
|
||||
@@ -1,53 +0,0 @@
|
||||
import Message from '../ChatMsg/Message';
|
||||
import { Space, Row, Col, Divider } from 'antd';
|
||||
import BasicInfoSection from './BasicInfoSection';
|
||||
import DimensionSection from './DimensionSection';
|
||||
import RecommendSection from './RecommendSection';
|
||||
import SemanticTypeTag from '../ChatMsg/SemanticInfoPopover/SemanticTypeTag';
|
||||
import { CLS_PREFIX } from '../../common/constants';
|
||||
|
||||
type Props = {
|
||||
dataSource?: any;
|
||||
onDimensionSelect?: (value: any) => void;
|
||||
};
|
||||
|
||||
const SemanticDetail: React.FC<Props> = ({ dataSource, onDimensionSelect }) => {
|
||||
const { name, nameEn, createdBy, description, className, classId, semanticInfoType } = dataSource;
|
||||
|
||||
const semanticDetailCls = `${CLS_PREFIX}-semantic-detail`;
|
||||
|
||||
return (
|
||||
<Message position="left" width="100%">
|
||||
<div>
|
||||
<div>
|
||||
<Row>
|
||||
<Col flex="1">
|
||||
<Space size={20}>
|
||||
<span className={`${semanticDetailCls}-title`}>{`指标详情: ${name}`}</span>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col flex="0 1 40px">
|
||||
<SemanticTypeTag infoType={semanticInfoType} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<BasicInfoSection
|
||||
description={description}
|
||||
basicInfoList={[
|
||||
{
|
||||
name: '主题域',
|
||||
value: className,
|
||||
},
|
||||
{ name: '创建人', value: createdBy },
|
||||
]}
|
||||
/>
|
||||
<Divider style={{ margin: '12px 0 16px 0px' }} />
|
||||
<DimensionSection classId={classId} uniqueId={nameEn} onSelect={onDimensionSelect} />
|
||||
<Divider style={{ margin: '6px 0 12px 0px' }} />
|
||||
<RecommendSection classId={classId} metricName={name} onSelect={onDimensionSelect} />
|
||||
</div>
|
||||
</Message>
|
||||
);
|
||||
};
|
||||
|
||||
export default SemanticDetail;
|
||||
@@ -1,129 +0,0 @@
|
||||
@import '../../styles/variables.less';
|
||||
|
||||
@semantic-detail-cls: ~'@{supersonic-chat-prefix}-semantic-detail';
|
||||
|
||||
.@{semantic-detail-cls} {
|
||||
&-info-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
&-description {
|
||||
font-size: 13px;
|
||||
color: var(--text-color-fourth);
|
||||
}
|
||||
|
||||
&-main-entity-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
&-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-info-name {
|
||||
color: var(--text-color-fourth);
|
||||
}
|
||||
|
||||
&-info-value {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
margin-left: 15px;
|
||||
line-height: 30px;
|
||||
&::before {
|
||||
display: block;
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 0;
|
||||
top: 6px;
|
||||
height: 15px;
|
||||
width: 3px;
|
||||
font-size: 0;
|
||||
background: #0e73ff;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #0e73ff;
|
||||
}
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-fourth);
|
||||
}
|
||||
|
||||
&-section-item {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-reload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text-color-fourth);
|
||||
font-size: 12px;
|
||||
column-gap: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-reload-icon {
|
||||
font-size: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-header {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-fourth);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&-more {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-recommend-questions-content {
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&-question {
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
&:hover {
|
||||
color: var(--chat-blue);
|
||||
}
|
||||
}
|
||||
|
||||
&-content-col {
|
||||
min-width: 300px;
|
||||
overflow-y: hidden;
|
||||
height: 32px;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
&-content-col-box{
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user