import React from 'react';
import { Button, Form, Select } from 'antd';
import { useDrop } from 'react-dnd';
import classNames from 'classnames';
import { connect } from 'sdk';
import ChartAnalyse from './ChartAnalyse';
import Attribution from './Attribution';
import Alert from './Alert';
import Empty from './Empty';
import styles from './RightPanel.less';
const toolbarTabData = [
{
name: '图表分析',
key: 'chart',
component: <ChartAnalyse />,
},
{
name: '生成报表',
key: 'table',
disabled: true,
component: <Empty description="生成报表功能即将支持" />,
},
{
name: '归因分析',
key: 'attribution',
component: <Attribution />,
},
{
name: '预警设置',
key: 'warning',
component: <Alert />,
},
{
name: '指标预测',
key: 'predict',
disabled: true,
component: <Empty description="指标预测功能即将支持" />,
},
];
const RightPanel = ({ dragKpiList, activeTab, dispatch, time_unit }) => {
const toolbarTab = toolbarTabData.map(({ name, ...t }) => (
<Button
{...t}
style={{ marginLeft: 10, borderRadius: 4 }}
type={activeTab === t.key ? 'primary' : 'default'}
onClick={() => {
dispatch({
type: 'kpiAnalyse/save',
payload: { activeTab: t.key },
});
}}
>
{name}
</Button>
));
const [dropCollectedProps, drop] = useDrop({
accept: 'DND_KPI_TYPE',
drop: item => {
if (item.kpiInfo) {
// 将拖放的指标进行保存
const isExist = dragKpiList.some(t => t === item.kpiInfo);
if (!isExist) {
dispatch({
type: 'kpiAnalyse/save',
payload: {
dragKpiList: [...dragKpiList, item.kpiInfo],
},
});
}
}
},
collect: monitor => ({
isOver: !!monitor.isOver(),
}),
});
const contentClassName = classNames({
[styles.content]: true,
[styles.contentDrag]: dropCollectedProps.isOver,
});
const dimensionChange = v => {
dispatch({
type: 'kpiAnalyse/save',
payload: { time_unit: v },
});
};
let content;
for (const barData of toolbarTabData) {
if (barData.key === activeTab) {
content = barData.component;
break;
}
}
return (
<div className={styles.container}>
<div className={styles.toolbar}>
<div className={styles.title}>
<div>画布</div>
<div className={styles.dimensionWrap}>
<Form.Item label="维度" style={{ marginBottom: 0 }}>
<Select
value={time_unit}
style={{ width: 110, borderRadius: 4 }}
size="small"
bordered={false}
className={styles.dimension}
onChange={dimensionChange}
>
<Select.Option value="day">时间: 日度</Select.Option>
<Select.Option value="month">时间: 月度</Select.Option>
<Select.Option value="year">时间: 年度</Select.Option>
</Select>
</Form.Item>
</div>
</div>
<div>{toolbarTab}</div>
</div>
<div className={contentClassName} ref={drop}>
{dragKpiList.length ? content : <Empty description="左侧拖入指标进行分析" />}
</div>
</div>
);
};
export default connect(state => ({
dragKpiList: state.kpiAnalyse.dragKpiList,
activeTab: state.kpiAnalyse.activeTab,
time_unit: state.kpiAnalyse.time_unit,
}))(RightPanel);
import React from 'react';
import { useDrag } from 'react-dnd';
export default props => {
const { name } = props;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [collectedProps, drag] = useDrag({
item: {
type: 'DND_KPI_TYPE',
kpiInfo: props,
},
canDrag: () => {
if (props.kpiType === 'domain') {
return false;
}
return true;
},
});
return (
<div ref={drag} style={{ height: 40, lineHeight: '40px' }}>
{name}
</div>
);
};
import React, { useState, useEffect } from 'react';
import { Spin, Tree } from 'antd';
import { DownCircleTwoTone } from '@ant-design/icons';
import { connect } from 'sdk';
import { useLoadingReq } from '@/util/hooks';
import { immutableSet } from '@/util';
import { getDomainList } from '@/service/domain';
import { getKpiSet, getKpiList } from '@/service/kpi';
import KpiItem from './KpiItem';
import styles from './KpiList.less';
const { TreeNode } = Tree;
const TYPE = 'kpiType';
function generateTreeNodes(treeData = []) {
return treeData.map(t => {
const key = `${t[TYPE]}-${t.code}`;
const title = <KpiItem {...t} />;
const selectable = t[TYPE] !== 'domain';
// eslint-disable-next-line no-param-reassign
t.uniqId = key;
const treeNodeProp = {
key,
title,
selectable,
nodeInfo: t,
};
if (t.children && t.children.length) {
return <TreeNode {...treeNodeProp}>{generateTreeNodes(t.children)}</TreeNode>;
}
return (
<TreeNode {...treeNodeProp} isLeaf={!t.has_child_kpi || (t.children && t.children.length)} />
);
});
}
export default connect(state => ({
dragKpiList: state.kpiAnalyse.dragKpiList,
}))(({ dragKpiList }) => {
const [treeData, setTreeData] = useState([]);
// 初始化数据, 先把第一层主题域和第二层指标集拉下来
async function initTreeData() {
let domainList = await getDomainList({ category: 'domain' });
const kpiSet = await Promise.all(domainList.map(t => getKpiSet({ domain_code: t.code })));
domainList = domainList.map((domain, index) => ({
...domain,
[TYPE]: 'domain',
children: kpiSet[index].map(t => ({
...t,
kpi_set_code: t.code,
[TYPE]: 'kpiSet',
})),
}));
setTreeData(domainList);
}
const [loading, initTreeDataWithLoading] = useLoadingReq(initTreeData);
useEffect(() => {
initTreeDataWithLoading();
}, []);
// 动态加载数据
async function onLoadData(rest) {
const { children, nodeInfo, pos } = rest;
if (children) {
return;
}
let params = {};
if (nodeInfo[TYPE] === 'kpiSet') {
params = {
kpi_set_code: nodeInfo.code,
};
} else {
params = {
kpi_code: nodeInfo.code,
level: nodeInfo.level,
};
}
const result = await getKpiList(params);
const paths = pos.split('-');
paths.shift();
const path = `${paths.join('.children.')}.children`;
const newTreeData = immutableSet(
treeData,
path,
result.kpi_list.map(t => ({
...t,
name: t.kpi_name,
code: t.kpi_code,
kpi_set_code: nodeInfo.kpi_set_code || nodeInfo.code,
})),
);
setTreeData(newTreeData);
}
const treeNodes = generateTreeNodes(treeData);
const defaultExpandedKeys = treeNodes.map(t => t.key);
const selectedKeys = dragKpiList.map(t => `${t[TYPE]}-${t.code}`);
return (
<div className={styles.container}>
<div className={styles.title}>指标库</div>
<div className={styles.list}>
{loading ? (
<Spin tip="Loading..." delay={300} />
) : (
<Tree
showIcon
blockNode
multiple
loadData={onLoadData}
defaultExpandedKeys={defaultExpandedKeys}
selectedKeys={selectedKeys}
switcherIcon={<DownCircleTwoTone />}
>
{treeNodes}
</Tree>
)}
</div>
</div>
);
});
import _ from 'lodash';
export function importAll(requireContextObj) {
const cache = {};
requireContextObj.keys().forEach(key => {
const fileName = key.replace('./', '').replace('.jsx', '');
cache[fileName] = requireContextObj(key).default;
});
return cache;
}
export const capitalize = (str = '') =>
str
.split('')
.map((t, index) => (index === 0 ? t.toUpperCase() : t))
.join('');
// js 树形结构数据 已知某一子节点 一次向上获取所有父节点
export function treeFindPath(tree, func, path = []) {
if (!tree) return [];
// eslint-disable-next-line no-restricted-syntax
for (const data of tree) {
// 这里按照你的需求来存放最后返回的内容吧
path.push(data.kpi_code);
if (func(data)) return path;
if (data.children) {
const findChildren = treeFindPath(data.children, func, path);
if (findChildren.length) return findChildren;
}
path.pop();
}
return [];
}
// 全屏
export function toggleFullscreen(elem, fullscreen) {
if (!elem) return;
if (fullscreen) {
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen();
} else if (elem.mozRequestFullScreen) {
elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen();
}
} else if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
export function immutableSet(obj, path, val) {
const oldVal = _.get(obj, path);
if (oldVal === val) {
return obj;
}
if (path.indexOf('.') === -1) {
const newObj = _.clone(obj);
return _.set(newObj, path, val);
}
const paths = path.split('.');
const lastPath = paths.pop();
const parentPath = paths.join('.');
const parentValue = _.clone(_.get(obj, parentPath)) || {};
_.set(parentValue, lastPath, val);
return immutableSet(obj, parentPath, parentValue);
}
import { useState, useEffect } from 'react';
import { toggleFullscreen } from './index';
export function useLoadingReq(req) {
const [loading, setLoading] = useState(false);
return [
loading,
async (...rest) => {
setLoading(true);
try {
const res = await req(...rest);
setLoading(false);
return res;
} catch (error) {
setLoading(false);
throw error;
}
},
];
}
export function useStateWithProp(prop) {
const [state, setState] = useState(prop);
useEffect(() => {
if (state !== prop) {
setState(prop);
}
}, [prop]);
return [state, setState];
}
export function useFullscreen(ref) {
const [current, setCurrent] = useState();
const [fullscreen, setFullscreen] = useState(false);
useEffect(() => {
if (typeof ref === 'string') {
const raf = () => {
const ele = document.getElementById(ref);
if (ele) {
setCurrent(ele);
} else {
window.requestAnimationFrame(raf);
}
};
raf();
} else if (ref.current) {
setCurrent(ref.current);
}
}, []);
useEffect(() => {
if (current) {
const callback = () => {
let newFullscreen = false;
if (document.fullscreenElement) {
newFullscreen = true;
}
setFullscreen(newFullscreen);
};
current.addEventListener('fullscreenchange', callback);
return () => {
current.removeEventListener('fullscreenchange', callback);
};
}
}, [current]);
return {
fullscreen,
toggleFullscreen: () => {
const newFullscreen = !fullscreen;
toggleFullscreen(current, newFullscreen);
},
};
}