dnd拖拽 - davy-gan/web GitHub Wiki

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);
    },
  };
}
⚠️ **GitHub.com Fallback** ⚠️