@ant-design/icons#FolderFilled TypeScript Examples

The following examples show how to use @ant-design/icons#FolderFilled. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: useGetVizIcon.tsx    From datart with Apache License 2.0 6 votes vote down vote up
function useGetVizIcon() {
  const chartManager = ChartManager.instance();
  const chartIcons = chartManager.getAllChartIcons();

  return useCallback(
    ({ relType, avatar, subType }) => {
      switch (relType) {
        case 'DASHBOARD':
          return subType !== null ? (
            renderIcon(subType === 'free' ? 'CombinedShape' : 'kanban')
          ) : (
            <FundFilled />
          );
        case 'DATACHART':
          return avatar ? renderIcon(chartIcons[avatar]) : <BarChartOutlined />;
        default:
          return p => (p.expanded ? <FolderOpenFilled /> : <FolderFilled />);
      }
    },
    [chartIcons],
  );
}
Example #2
Source File: panel-body.tsx    From XFlow with MIT License 5 votes vote down vote up
FolderIcon = ({ expanded }: { expanded: boolean }) => {
  return expanded ? <FolderOpenFilled /> : <FolderFilled />
}
Example #3
Source File: index.tsx    From dashboard with Apache License 2.0 5 votes vote down vote up
DepartmentTreeSelect: React.FC<DepartmentTreeSelectProps> = (props) => {
  const {value, onChange, options: allDepartments, ...rest} = props;
  const [departmentSelectionVisible, setDepartmentSelectionVisible] = useState(false);
  const allDepartmentMap = _.keyBy<DepartmentOption>(allDepartments, 'ext_id');
  return (
    <>
      <Select
        {...rest}
        mode="multiple"
        placeholder="请选择部门"
        allowClear={true}
        value={value}
        open={false}
        maxTagCount={rest.maxTagCount || 2}
        tagRender={(tagProps) => {
          // @ts-ignore
          const department: DepartmentOption = allDepartmentMap[tagProps.value];
          if (!department) {
            return <></>
          }

          return (
            <span className={'ant-select-selection-item'}>
              <div className="flex-col align-left">
                <FolderFilled
                  style={{
                    color: '#47a7ff',
                    fontSize: 20,
                    marginRight: 6,
                    verticalAlign: -6,
                  }}
                />
                {department?.name}
              </div>
              <span
                className="ant-select-selection-item-remove"
                style={{marginLeft: 3}}
                onClick={(e) => {
                  e.preventDefault();
                  e.stopPropagation();
                  // @ts-ignore
                  onChange(
                    value.filter(
                      // @ts-ignore
                      (extDepartmentID: string) => extDepartmentID !== department?.ext_id,
                    ),
                  );
                }}
              >
                <CloseOutlined/>
              </span>
            </span>
          );
        }}
        onClear={() => {
          // @ts-ignore
          onChange([]);
        }}
        onClick={() => {
          setDepartmentSelectionVisible(!departmentSelectionVisible);
        }}
      />

      <DepartmentSelectionModal
        visible={departmentSelectionVisible}
        setVisible={setDepartmentSelectionVisible}
        defaultCheckedDepartments={value?.map((id: string) => allDepartmentMap[id])}
        onFinish={(values) => {
          // @ts-ignore
          onChange(values.map((item) => item.ext_id));
        }}
        allDepartments={allDepartments}
      />
    </>
  );
}
Example #4
Source File: index.tsx    From Aragorn with MIT License 4 votes vote down vote up
FileManage = () => {
  const {
    state: {
      uploaderProfiles,
      configuration: { defaultUploaderProfileId }
    }
  } = useAppContext();

  const [windowHeight, setWindowHeight] = useState(window.innerHeight);

  useEffect(() => {
    function handleResize() {
      setWindowHeight(window.innerHeight);
    }
    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  const { id } = useParams<{ id: string }>();

  const [hasFileManageFeature, setHasFileManageFeature] = useState(false);

  const [uploaderProfile, setUploaderProfile] = useState({} as UploaderProfile);

  useEffect(() => {
    const currentId = id || defaultUploaderProfileId;
    setCurrentProfile(currentId as string);
  }, []);

  useEffect(() => {
    if (uploaderProfile?.id) {
      getList();
    }
  }, [uploaderProfile]);

  const [list, setList] = useState([] as ListFile[]);
  const [listLoading, setListLoading] = useState(false);

  const getList = (directoryPath?: string) => {
    setListLoading(true);
    ipcRenderer.send('file-list-get', uploaderProfile.id, directoryPath);
  };

  const [dirPath, setDirPath] = useState([] as string[]);

  useEffect(() => {
    function handleListGetReply(_, res?: FileListResponse) {
      setListLoading(false);
      if (res === undefined) {
        setHasFileManageFeature(false);
        setList([]);
        message.info(`${uploaderProfile.uploaderName}暂不支持文件管理功能`);
        return;
      }
      setHasFileManageFeature(true);
      if (res.success) {
        setList(res.data);
      } else {
        message.error(`文件列表获取失败 ${res.desc || ''}`);
      }
    }

    function handleFileDeleteReply(_, res?: DeleteFileResponse) {
      if (res === undefined) {
        return;
      }
      if (res.success) {
        message.success({ content: '文件删除成功', key: 'file-manage-delete' });
        getList(dirPath.join('/'));
      } else {
        message.error({ content: `文件删除失败 ${res.desc || ''}`, key: 'file-manage-delete' });
      }
    }

    function handleFileUploadReply() {
      getList(dirPath.join('/'));
    }

    function handleDirectoryCreateReply(_, res?: CreateDirectoryResponse) {
      if (res === undefined) {
        return;
      }
      if (res.success) {
        message.success('目录创建成功');
        setModalVisible(false);
        getList(dirPath.join('/'));
      } else {
        message.error(`目录创建失败 ${res.desc || ''}`);
      }
    }

    function handleExportReplay(_, res) {
      setExportLoading(false);
      if (res) {
        shell.showItemInFolder(res);
        setRowKeys([]);
        setSelectRows([]);
      }
    }

    ipcRenderer.on('file-list-get-reply', handleListGetReply);
    ipcRenderer.on('file-delete-reply', handleFileDeleteReply);
    ipcRenderer.on('file-upload-reply', handleFileUploadReply);
    ipcRenderer.on('directory-create-reply', handleDirectoryCreateReply);
    ipcRenderer.on('export-reply', handleExportReplay);

    return () => {
      ipcRenderer.removeListener('file-list-get-reply', handleListGetReply);
      ipcRenderer.removeListener('file-delete-reply', handleFileDeleteReply);
      ipcRenderer.removeListener('file-upload-reply', handleFileUploadReply);
      ipcRenderer.removeListener('directory-create-reply', handleDirectoryCreateReply);
      ipcRenderer.removeListener('export-reply', handleExportReplay);
    };
  }, [uploaderProfile, dirPath]);

  const handleNameClick = (record: ListFile) => {
    if (record.type === 'directory') {
      const newPath = [...dirPath, formatFileName(record.name)];
      setDirPath(newPath);
      getList(newPath.join('/'));
    } else {
      clipboard.writeText(record.url as string);
      message.success('链接已复制到粘贴板');
    }
  };

  const handlePathClick = (index: number) => {
    if (index === -1) {
      setDirPath([]);
      getList();
    } else {
      const newPath = dirPath.slice(0, index + 1);
      setDirPath(newPath);
      getList(newPath.join('/'));
    }
  };

  const setCurrentProfile = (uploaderProfileId: string) => {
    setDirPath([]);
    const uploaderProfile = uploaderProfiles.find(item => item.id === uploaderProfileId);
    setUploaderProfile(uploaderProfile as UploaderProfile);
  };

  const formatFileName = (name: string) => {
    if (dirPath.length > 0) {
      const pathPrefix = dirPath.join('/') + '/';
      return name.split(pathPrefix).pop() || '';
    } else {
      return name;
    }
  };

  const [selectRowKeys, setRowKeys] = useState([] as string[]);
  const [selectRows, setSelectRows] = useState([] as ListFile[]);

  const handleTableRowChange = (selectedRowKeys, selectedRows: ListFile[]) => {
    setRowKeys(selectedRowKeys);
    setSelectRows(selectedRows);
  };

  const handleRefresh = () => {
    getList(dirPath.join('/'));
  };

  const handleBatchDelete = () => {
    Modal.confirm({
      title: '确认删除',
      onOk: () => {
        const names = selectRows.map(item => [...dirPath, formatFileName(item.name)].join('/'));
        message.info({ content: '正在删除,请稍后...', key: 'file-manage-delete' });
        ipcRenderer.send('file-delete', uploaderProfile.id, names);
      }
    });
  };

  const handleDelete = (record: ListFile) => {
    let name = record.name;
    Modal.confirm({
      title: '确认删除',
      content: name,
      onOk: () => {
        let name = record.name;
        if (record.type === 'directory') {
          name = `${[...dirPath, record.name].join('/')}/`;
        } else {
          name = [...dirPath, formatFileName(record.name)].join('/');
        }
        message.info({ content: '正在删除,请稍后...', key: 'file-manage-delete' });
        ipcRenderer.send('file-delete', uploaderProfile.id, [name]);
      }
    });
  };

  const uploadRef = useRef<HTMLInputElement>(null);

  const handleFileUpload = (event: React.FormEvent<HTMLInputElement>) => {
    const fileList = event.currentTarget.files || [];
    const filesPath = Array.from(fileList).map(file => file.path);
    const pathPrefix = dirPath.join('/');
    ipcRenderer.send('file-upload', uploaderProfile.id, filesPath, pathPrefix);
    event.currentTarget.value = '';
  };

  const [modalVisible, setModalVisible] = useState(false);

  const [form] = Form.useForm();

  const handleCreateDirectory = () => {
    form.validateFields().then(values => {
      ipcRenderer.send('directory-create', uploaderProfile.id, values?.directoryPath || '');
    });
  };

  const handleDownload = (record: ListFile) => {
    ipcRenderer.send('file-download', record.name, record.url);
  };

  const [exportLoading, setExportLoading] = useState(false);

  const handleExport = () => {
    const data = selectRows.map(item => {
      const fileNameArr = item.name.split('.');
      fileNameArr.pop();
      return {
        name: fileNameArr.join('.'),
        url: item.url
      };
    });
    setExportLoading(true);
    ipcRenderer.send('export', data);
  };

  const columns: ColumnsType<ListFile> = [
    {
      title: '文件名',
      dataIndex: 'name',
      ellipsis: true,
      render: (val: string, record: ListFile) => (
        <div style={{ display: 'flex', alignItems: 'center' }}>
          {record.type === 'directory' ? (
            <FolderFilled style={{ fontSize: 16 }} />
          ) : (
            <FileOutlined style={{ fontSize: 16 }} />
          )}
          {record.type === 'directory' ? (
            <a
              title={val}
              onClick={() => handleNameClick(record)}
              className="table-filename"
              style={{ marginLeft: 10, overflow: 'hidden', textOverflow: 'ellipsis' }}
            >
              {formatFileName(val)}
            </a>
          ) : (
            <Popover
              placement="topLeft"
              content={() =>
                /(jpg|png|gif|jpeg)$/.test(val) ? (
                  <Image
                    style={{ maxWidth: 500 }}
                    src={record.url}
                    fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg=="
                  />
                ) : (
                  val
                )
              }
              trigger="hover"
            >
              <a
                title={val}
                onClick={() => handleNameClick(record)}
                className="table-filename"
                style={{ marginLeft: 10, overflow: 'hidden', textOverflow: 'ellipsis' }}
              >
                {formatFileName(val)}
              </a>
            </Popover>
          )}
        </div>
      )
    },
    {
      title: '文件大小',
      dataIndex: 'size',
      ellipsis: true,
      width: 120,
      render: val => (val ? filesize(val) : '-')
    },
    {
      title: '更新时间',
      dataIndex: 'lastModified',
      ellipsis: true,
      width: 200,
      render: val => (val ? dayjs(val).format('YYYY-MM-DD HH:mm:ss') : '-')
    },
    {
      title: '操作',
      width: 120,
      render: (_, record) => (
        <Space>
          {record.type !== 'directory' && (
            <>
              <DownloadOutlined onClick={() => handleDownload(record)} />
              <CopyOutlined onClick={() => handleNameClick(record)} />
            </>
          )}
          <DeleteOutlined onClick={() => handleDelete(record)} />
        </Space>
      )
    }
  ];

  return (
    <div className="storage-page">
      <header>
        <span>文件管理</span>
        <Divider />
      </header>
      <Space style={{ marginBottom: 10 }}>
        <Select style={{ minWidth: 120 }} value={uploaderProfile?.id} onChange={setCurrentProfile}>
          {uploaderProfiles.map(item => (
            <Select.Option key={item.name} value={item.id}>
              {item.name}
            </Select.Option>
          ))}
        </Select>
        <Button
          title="上传"
          icon={<UploadOutlined />}
          disabled={!hasFileManageFeature}
          type="primary"
          onClick={() => {
            uploadRef.current?.click();
          }}
        />
        <Button title="刷新" icon={<ReloadOutlined />} disabled={!hasFileManageFeature} onClick={handleRefresh} />
        <Button
          title="创建文件夹"
          icon={<FolderAddOutlined />}
          disabled={!hasFileManageFeature}
          onClick={() => {
            setModalVisible(true);
          }}
        />
        <Button
          title="导出"
          icon={<ExportOutlined />}
          disabled={selectRows.length === 0}
          onClick={handleExport}
          loading={exportLoading}
        />
        <Button title="删除" icon={<DeleteOutlined />} disabled={selectRows.length === 0} onClick={handleBatchDelete} />
      </Space>
      <Breadcrumb style={{ marginBottom: 10 }}>
        <Breadcrumb.Item>
          <a onClick={() => handlePathClick(-1)}>全部文件</a>
        </Breadcrumb.Item>
        {dirPath.map((item, index) => (
          <Breadcrumb.Item key={item}>
            <a onClick={() => handlePathClick(index)}>{item}</a>
          </Breadcrumb.Item>
        ))}
      </Breadcrumb>
      <div className="table-wrapper">
        <Table
          size="small"
          rowKey="name"
          scroll={{ y: windowHeight - 270 }}
          dataSource={list}
          columns={columns}
          pagination={{
            size: 'small',
            defaultPageSize: 100,
            pageSizeOptions: ['50', '100', '200'],
            hideOnSinglePage: true
          }}
          loading={listLoading}
          rowSelection={{
            onChange: handleTableRowChange,
            selectedRowKeys: selectRowKeys,
            getCheckboxProps: record => ({ disabled: record?.type === 'directory' })
          }}
        />
      </div>
      <input ref={uploadRef} type="file" multiple hidden onChange={handleFileUpload} />
      <Modal
        title="创建目录"
        visible={modalVisible}
        onCancel={() => setModalVisible(false)}
        onOk={handleCreateDirectory}
        destroyOnClose={true}
      >
        <Form form={form} preserve={false}>
          <Form.Item
            label="目录名称"
            name="directoryPath"
            rules={[{ required: true }, { pattern: domainPathRegExp, message: '目录名不能以 / 开头或结尾' }]}
          >
            <Input autoFocus />
          </Form.Item>
        </Form>
      </Modal>
    </div>
  );
}
Example #5
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
DepartmentSelectionModal: React.FC<DepartmentSelectionProps> = (props) => {
  const {visible, setVisible, defaultCheckedDepartments, onFinish, allDepartments} = props;
  const [departments, setDepartments] = useState<DepartmentOption[]>([]);
  const [departmentNodes, setDepartmentNodes] = useState<TreeNode[]>([]); // 一维的节点
  const [departmentTree, setDepartmentTree] = useState<TreeNode[]>([]); // 多维的树节点
  const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>(
    _.filter<DepartmentOption>(defaultCheckedDepartments) || [],
  );
  const [keyword, setKeyword] = useState<string>('');
  const [checkAll, setCheckAll] = useState<boolean>(false);
  const [expandAll, setExpandAll] = useState<boolean>(false);
  const [checkedNodeKeys, setCheckedNodeKeys] = useState<string[]>([]);
  const [expandedNodeKeys, setExpandedNodeKeys] = useState<string[]>(['1']);
  const allDepartmentMap = _.keyBy(allDepartments, 'ext_id');

  const onCheckAllChange = (e: CheckboxChangeEvent) => {
    let items: DepartmentOption[];
    if (e.target.checked) {
      items = _.uniqWith<DepartmentOption>(
        [...departments, ...selectedDepartments],
        (a, b) => a.ext_id === b.ext_id,
      );
    } else {
      items = _.differenceWith(selectedDepartments, departments, (a, b) => a.ext_id === b.ext_id);
    }
    setSelectedDepartments(items);
    setCheckAll(e.target.checked);
  };

  const onNodesCheck = (checked: { checked: string[]; halfChecked: string[] }) => {
    const checkedExtDepartmentIDs: number[] = [];
    let selectedExtDepartmentIDs = selectedDepartments.map((item) => item.ext_id);
    let checkedKeys = [...checked.checked];

    // 找出本次uncheck的key,根据这些key的ext_id去删除相关checkedKey
    const uncheckedKeys = _.difference(checkedNodeKeys, checkedKeys);
    _.forEach<string>(uncheckedKeys, (key: string) => {
      // @ts-ignore
      checkedKeys = checkedKeys.filter<string>((checkedKey) => {
        return !checkedKey.includes(key);
      });
    });

    // 记录当前所有checked的key
    checkedKeys.forEach((key) => {
      checkedExtDepartmentIDs.push(Number(key));
      selectedExtDepartmentIDs.push(Number(key));
    });

    // 计算需要删除的extDepartmentID
    // @ts-ignore
    const shouldDeleteExtDepartmentIDs = _.difference(
      _.map(departments, 'ext_id'),
      checkedExtDepartmentIDs,
    );
    selectedExtDepartmentIDs = _.difference(
      _.uniq(selectedExtDepartmentIDs),
      _.uniq(shouldDeleteExtDepartmentIDs),
    );

    const items = selectedExtDepartmentIDs.map((selectedExtDepartmentID) => {
      return allDepartmentMap[selectedExtDepartmentID];
    });

    setCheckAll(departments.length === items.length);
    setSelectedDepartments(items);
  };

  const nodeRender = (node: DataNode): ReactNode => {
    return (
      <>
        <FolderFilled
          style={{
            color: '#47a7ff',
            fontSize: 20,
            marginRight: 6,
            verticalAlign: -6,
          }}
        />
        {node.title}
      </>
    );
  };

  useEffect(() => {
    setSelectedDepartments(_.filter<DepartmentOption>(defaultCheckedDepartments) || []);
    setKeyword('');
  }, [defaultCheckedDepartments, visible]);

  // 监听选中部门变化,计算checked的树节点
  useEffect(() => {
    const allDepartmentNodeKeys = _.map(departmentNodes, 'key');
    // 计算当前选中的部门,命中的key
    const matchedKeys: string[] = [];
    allDepartmentNodeKeys.forEach((key: string) => {
      selectedDepartments.forEach((department) => {
        if (key === `${department.ext_id}`) {
          matchedKeys.push(key);
        }
      });
    });
    setCheckedNodeKeys(matchedKeys);
  }, [selectedDepartments]);

  // 关键词变化的时候
  useEffect(() => {
    let filteredDepartments: DepartmentOption[] = [];

    allDepartments.forEach((item) => {
      if (keyword.trim() === '' || item.label.includes(keyword.trim())) {
        filteredDepartments.push(item);
      }
    })

    // 把搜索结果的父级节点放进结果集,方便构造树
    const pushParentDepartment = (item: DepartmentOption) => {
      if (item.ext_parent_id === 0) {
        filteredDepartments.push(item);
        return;
      }
      const parent = allDepartmentMap[item.ext_parent_id];
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      filteredDepartments.push(parent);
      pushParentDepartment(parent);
    };

    filteredDepartments.forEach((item) => {
      pushParentDepartment(item);
    })

    filteredDepartments = _.uniq<DepartmentOption>(filteredDepartments);
    setDepartments(filteredDepartments);

    const {nodes, tree} = buildDepartmentTree(filteredDepartments);

    // 这里同步更新node节点和选中key值
    let checkedKeys: string[] = [];
    nodes.forEach((node) => {
      selectedDepartments.forEach((department) => {
        if (node.key === `${department.ext_id}`) {
          checkedKeys.push(node.key);
        }
      });
    });
    checkedKeys = _.uniq<string>(checkedKeys);
    setCheckedNodeKeys(checkedKeys);

    setCheckAll(false);
    setDepartmentNodes(nodes);
    setDepartmentTree(tree);
  }, [allDepartments, keyword]);

  // @ts-ignore
  return (
    <Modal
      width={665}
      className={'dialog from-item-label-100w'}
      visible={visible}
      zIndex={1001}
      onCancel={() => setVisible(false)}
      onOk={() => {
        if (onFinish) {
          onFinish(selectedDepartments);
        }
        setVisible(false);
      }}
    >
      <h2 className="dialog-title"> 选择部门 </h2>
      <div className={styles.addDepartmentDialogContent}>
        <div className={styles.container}>
          <div className={styles.left}>
            <p className={styles.toolTop} style={{marginBottom: 0}}>
              <Search
                className={styles.searchInput}
                enterButton={'搜索'}
                prefix={<SearchOutlined/>}
                placeholder="请输入部门名称"
                allowClear
                value={keyword}
                onChange={(e) => {
                  setKeyword(e.target.value);
                }}
              />
            </p>
            <p style={{marginBottom: 0}}>
              <Checkbox checked={checkAll} onChange={onCheckAllChange}>
                全部部门({departments.length}):
              </Checkbox>
              <Button
                type={'link'}
                onClick={() => {
                  const currentStatus = !expandAll;
                  if (currentStatus) {
                    setExpandedNodeKeys(_.map(departmentNodes, 'key'));
                  } else {
                    setExpandedNodeKeys(['0']);
                  }
                  setExpandAll(currentStatus);
                }}
                style={{marginRight: 30}}
              >
                {!expandAll ? '展开全部' : '收起全部'}
              </Button>
            </p>
            <div className={styles.allDepartment}>
              {departmentTree.length === 0 && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>}
              <Tree
                className={styles.departmentTree}
                autoExpandParent={false}
                checkStrictly={true}
                checkedKeys={checkedNodeKeys}
                defaultExpandedKeys={checkedNodeKeys}
                expandedKeys={expandedNodeKeys}
                // @ts-ignore
                onExpand={(expandedKeys: string[]) => {
                  setExpandedNodeKeys(expandedKeys);
                }}
                height={300}
                switcherIcon={<CaretDownFilled style={{color: '#47a7ff'}}/>}
                checkable={true}
                multiple={true}
                treeData={departmentTree}
                // @ts-ignore
                onCheck={onNodesCheck}
                titleRender={nodeRender}
              />
            </div>
          </div>
          <div className={styles.right}>
            <p>
              已选部门({selectedDepartments.length}):
              <Button
                type={'link'}
                onClick={() => {
                  setSelectedDepartments([]);
                  setCheckAll(false);
                }}
              >
                清空
              </Button>
            </p>
            <ul className={styles.allDepartmentList}>
              {selectedDepartments.map((department) => {
                if (!department) {
                  return <></>
                }

                return (
                  <li
                    key={department.ext_id}
                    onClick={() => {
                      setSelectedDepartments(
                        selectedDepartments.filter((item) => item.ext_id !== department.ext_id),
                      );
                    }}
                  >
                    <div className={styles.avatarAndName}>
                      <div className="flex-col align-left">
                        <FolderFilled
                          style={{
                            color: '#47a7ff',
                            fontSize: 20,
                            marginRight: 6,
                            verticalAlign: -6,
                          }}
                        />
                        {department.name}
                      </div>
                    </div>
                    <CloseOutlined/>
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </div>
    </Modal>
  );
}
Example #6
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
StaffTreeSelectionModal: React.FC<StaffSelectionProps> = (props) => {
  const { visible, setVisible, defaultCheckedStaffs, onFinish, allStaffs } = props;
  const [staffs, setStaffs] = useState<StaffOption[]>([]);
  const [staffNodes, setStaffNodes] = useState<TreeNode[]>([]); // 一维的节点
  const [staffTree, setStaffTree] = useState<TreeNode[]>([]); // 多维的树节点
  const [selectedStaffs, setSelectedStaffs] = useState<StaffOption[]>(defaultCheckedStaffs || []);
  const [keyword, setKeyword] = useState<string>('');
  const [checkAll, setCheckAll] = useState<boolean>(false);
  const [expandAll, setExpandAll] = useState<boolean>(false);
  const [checkedNodeKeys, setCheckedNodeKeys] = useState<string[]>([]);
  const [expandedNodeKeys, setExpandedNodeKeys] = useState<string[]>([rootNode]);
  const allStaffMap = _.keyBy(allStaffs, 'ext_id');

  const onCheckAllChange = (e: CheckboxChangeEvent) => {
    let items: StaffOption[];
    if (e.target.checked) {
      items = _.uniqWith<StaffOption>(
        [...staffs, ...selectedStaffs],
        (a, b) => a.ext_id === b.ext_id,
      );
    } else {
      // @ts-ignore
      items = _.differenceWith<StaffParam>(selectedStaffs, staffs, (a, b) => a.ext_id === b.ext_id);
    }
    setSelectedStaffs(items);
    setCheckAll(e.target.checked);
  };

  const onNodesCheck = (checked: string[]) => {
    const checkedExtStaffIDs: string[] = [];
    let selectedExtStaffIDs = _.map(selectedStaffs, 'ext_id');
    let checkedKeys = [...checked];

    // 找出本次uncheck的key,根据这些key的ext_id去删除相关checkedKey
    const uncheckedKeys = _.difference(checkedNodeKeys, checkedKeys);
    _.forEach<string>(uncheckedKeys, (key: string) => {
      const [, , nodeID] = key.split(separator);
      // eslint-disable-next-line no-param-reassign
      // @ts-ignore
      checkedKeys = checkedKeys.filter<string>((checkedKey) => {
        return !checkedKey.includes(`${separator}${nodeID}`);
      });
    });

    // 记录当前所有checked的key
    checkedKeys.forEach((key) => {
      const [nodeType, , nodeID] = key.split(separator);
      if (nodeType === 'node') {
        checkedExtStaffIDs.push(nodeID);
        selectedExtStaffIDs.push(nodeID);
      }
    });

    // 计算需要删除的extStaffID
    // @ts-ignore
    const shouldDeleteExtStaffIDs = _.difference(_.map(staffs, 'ext_id'), checkedExtStaffIDs);
    selectedExtStaffIDs = _.difference(
      _.uniq(selectedExtStaffIDs),
      _.uniq(shouldDeleteExtStaffIDs),
    );

    const items = selectedExtStaffIDs.map((selectedExtStaffID) => {
      return allStaffMap[selectedExtStaffID];
    });

    setCheckAll(staffs.length === items.length);
    setSelectedStaffs(items);
  };

  const nodeRender = (node: DataNode): ReactNode => {
    const [nodeType, , extStaffID] = node.key.toString().split(separator);
    if (nodeType === 'node') {
      const staff = allStaffMap[extStaffID];
      if (staff) {
        return (
          <div className={styles.staffTitleNode}>
            <img src={FormatWeWorkAvatar(staff?.avatar_url, 60)} className={styles.avatar} />
            <div className={styles.text}>
              <span className={styles.title}>{staff?.name}</span>
              <em
                style={{
                  color: RoleColorMap[staff?.role_type],
                  borderColor: RoleColorMap[staff?.role_type],
                }}
              >
                {RoleMap[staff?.role_type] || '普通员工'}
              </em>
            </div>
          </div>
        );
      }
    }
    return (
      <>
        <FolderFilled
          style={{
            color: '#47a7ff',
            fontSize: 20,
            marginRight: 6,
            verticalAlign: -3,
          }}
        />
        {node.title}
      </>
    );
  };

  useEffect(() => {
    setSelectedStaffs(defaultCheckedStaffs || []);
    setKeyword('');
  }, [defaultCheckedStaffs, visible]);

  // 监听选中员工变化,计算checked的树节点
  useEffect(() => {
    const allStaffNodeKeys = _.map(staffNodes, 'key');
    // 计算当前选中的员工,命中的key
    const matchedKeys: string[] = [];
    allStaffNodeKeys.forEach((key: string) => {
      selectedStaffs.forEach((staff) => {
        if (key.includes(`${separator}${staff?.ext_id}`)) {
          matchedKeys.push(key);
        }
      });
    });
    setCheckedNodeKeys(matchedKeys);
  }, [selectedStaffs, staffNodes]);

  // 关键词变化的时候
  useEffect(() => {
    const filteredStaffs = allStaffs.filter((item) => {
      // 搜索部门名称
      let isDepartmentMatch = false;
      item?.departments?.forEach((department) => {
        if (department.name.includes(keyword)) {
          isDepartmentMatch = true;
        }
      });
      return keyword === '' || isDepartmentMatch || item.label.includes(keyword);
    });
    setStaffs(filteredStaffs);
    const { nodes, tree } = buildStaffTree(filteredStaffs);

    // 这里同步更新node节点和选中key值
    let checkedKeys: string[] = [];
    nodes.forEach((node) => {
      selectedStaffs.forEach((staff) => {
        if (node.nodeKey.includes(`${separator}${staff?.ext_id}`)) {
          checkedKeys.push(node.key);
        }
      });
    });
    checkedKeys = _.uniq<string>(checkedKeys);
    setCheckedNodeKeys(checkedKeys);

    setCheckAll(false);
    setStaffNodes(nodes);
    setStaffTree(tree);
  }, [allStaffs, keyword]);

  // @ts-ignore
  return (
    <Modal
      width={665}
      className={'dialog from-item-label-100w'}
      visible={visible}
      zIndex={1001}
      onCancel={() => setVisible(false)}
      onOk={() => {
        if (onFinish) {
          onFinish(selectedStaffs);
        }
        setVisible(false);
      }}
    >
      <h2 className='dialog-title'> 选择员工 </h2>
      <div className={styles.addStaffDialogContent}>
        <div className={styles.container}>
          <div className={styles.left}>
            <p className={styles.toolTop} style={{ marginBottom: 0 }}>
              <Search
                className={styles.searchInput}
                enterButton={'搜索'}
                prefix={<SearchOutlined />}
                placeholder='请输入员工昵称'
                allowClear
                value={keyword}
                onChange={(e) => {
                  setKeyword(e.target.value);
                }}
              />
            </p>
            <p style={{ marginBottom: 0 }}>
              <Checkbox checked={checkAll} onChange={onCheckAllChange}>
                全部成员({staffs.length}):
              </Checkbox>
              <Button
                type={'link'}
                onClick={() => {
                  const currentStatus = !expandAll;
                  if (currentStatus) {
                    setExpandedNodeKeys(
                      _.map(
                        staffNodes.filter((staffNode) => staffNode.type === 'group'),
                        'key',
                      ),
                    );
                  } else {
                    setExpandedNodeKeys([rootNode]);
                  }
                  setExpandAll(currentStatus);
                }}
                style={{ marginRight: 30 }}
              >
                {!expandAll ? '展开部门' : '收起部门'}
              </Button>
            </p>
            <div className={styles.allStaff}>
              {staffTree.length === 0 && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
              <Tree
                className={styles.staffTree}
                autoExpandParent={true}
                checkedKeys={checkedNodeKeys}
                defaultExpandedKeys={checkedNodeKeys}
                expandedKeys={expandedNodeKeys}
                // @ts-ignore
                onExpand={(expandedKeys: string[]) => {
                  setExpandedNodeKeys(expandedKeys);
                }}
                height={300}
                switcherIcon={<CaretDownFilled style={{ color: '#47a7ff' }} />}
                checkable={true}
                multiple={true}
                treeData={staffTree}
                // @ts-ignore
                onCheck={onNodesCheck}
                titleRender={nodeRender}
              />
            </div>
          </div>
          <div className={styles.right}>
            <p>
              已选成员({selectedStaffs.length}):
              <Button
                type={'link'}
                onClick={() => {
                  setSelectedStaffs([]);
                  setCheckAll(false);
                }}
              >
                清空
              </Button>
            </p>
            <ul className={styles.allStaffList}>
              {selectedStaffs.map((staff) => {
                if (!staff?.ext_id) {
                  return '';
                }

                return (
                  <li
                    key={staff?.ext_id}
                    onClick={() => {
                      setSelectedStaffs(
                        selectedStaffs.filter((item) => item?.ext_id !== staff?.ext_id),
                      );
                    }}
                  >
                    <div className={styles.avatarAndName}>
                      <img src={staff?.avatar_url} className={styles.avatar} />
                      <div className='flex-col align-left'>
                        <span>{staff?.name}</span>
                      </div>
                    </div>
                    <CloseOutlined />
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </div>
    </Modal>
  );
}
Example #7
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
ContactWayList: React.FC = () => {
  const [currentGroup, setCurrentGroup] = useState<ContactWayGroupItem>({});
  const [itemDetailVisible, setItemDetailVisible] = useState(false);
  const [currentItem, setCurrentItem] = useState<ContactWayItem>({});
  const [selectedItems, setSelectedItems] = useState<ContactWayItem[]>([]);
  const [filterGroupID, setFilterGroupID] = useState('0');
  const [groupItems, setGroupItems] = useState<ContactWayGroupItem[]>([]);
  const [groupItemsTimestamp, setGroupItemsTimestamp] = useState(Date.now);
  const [createGroupVisible, setCreateGroupVisible] = useState(false);
  const [batchUpdateVisible, setBatchUpdateVisible] = useState(false);
  const [editGroupVisible, setEditGroupVisible] = useState(false);
  const [allStaffs, setAllStaffs] = useState<StaffOption[]>([]);
  const actionRef = useRef<ActionType>();

  function showDeleteGroupConfirm(item: ContactWayGroupItem) {
    Modal.confirm({
      title: `删除分组`,
      content: `是否确认删除「${item.name}」分组?`,
      // icon: <ExclamationCircleOutlined/>,
      okText: '删除',
      okType: 'danger',
      cancelText: '取消',
      onOk() {
        return HandleRequest({ids: [item.id]}, DeleteGroup, () => {
          setGroupItemsTimestamp(Date.now);
        });
      },
    });
  }

  useEffect(() => {
    QuerySimpleStaffs({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        setAllStaffs(
          res?.data?.items?.map((item: SimpleStaffInterface) => {
            return {
              label: item.name,
              value: item.ext_id,
              ...item,
            };
          }) || [],
        );
      } else {
        message.error(res.message);
      }
    });
  }, []);

  useEffect(() => {
    QueryGroup({page_size: 1000, sort_field: 'sort_weight', sort_type: 'asc'})
      .then((resp) => {
        if (resp && resp.data && resp.data.items) {
          setGroupItems(resp.data.items);
        }
      })
      .catch((err) => {
        message.error(err);
      });
  }, [groupItemsTimestamp]);

  const columns: ProColumns<ContactWayItem>[] = [
    {
      title: 'ID',
      dataIndex: 'id',
      valueType: 'text',
      hideInTable: true,
      hideInSearch: true,
      fixed:'left',
    },
    {
      title: '渠道码',
      dataIndex: 'qr_code',
      valueType: 'image',
      hideInSearch: true,
      width: 80,
      fixed:'left',
      render: (dom, item) => {
        return (
          <div className={'qrcodeWrapper'}>
            <img
              src={item.qr_code}
              onClick={() => {
                setItemDetailVisible(true);
                setCurrentItem(item);
              }}
              className={'qrcode clickable'}
              alt={item.name}
            />
          </div>
        );
      },
    },
    {
      title: '名称',
      dataIndex: 'name',
      valueType: 'text',
      fixed:'left',
    },
    {
      title: '使用员工',
      dataIndex: 'staffs',
      valueType: 'text',
      hideInSearch: true,
      width: 210,
      render: (_, item) => {
        let staffs: any[] = [];
        item.schedules?.forEach((schedule) => {
          if (schedule.staffs) {
            staffs = [...staffs, ...schedule.staffs];
          }
        });
        if (item.schedule_enable === True) {
          staffs = uniqWith(staffs, (a, b) => a.ext_staff_id === b.ext_staff_id);
          return <CollapsedStaffs limit={2} staffs={staffs}/>;
        }
        return <CollapsedStaffs limit={2} staffs={item.staffs}/>;
      },
    },
    {
      title: '使用员工',
      dataIndex: 'ext_staff_ids',
      valueType: 'text',
      hideInTable: true,
      renderFormItem: () => {
        return <StaffTreeSelect options={allStaffs}/>;
      },
    },
    {
      title: '备份员工',
      dataIndex: 'backup_staffs',
      valueType: 'text',
      hideInSearch: true,
      width: 210,
      render: (_, item) => {
        return <CollapsedStaffs limit={2} staffs={item.backup_staffs}/>;
      },
    },
    {
      title: '标签',
      dataIndex: 'customer_tags',
      valueType: 'text',
      ellipsis: true,
      hideInSearch: true,
      width: 210,
      render: (_, item) => {
        return <CollapsedTags limit={3} tags={item.customer_tags}/>;
      },
    },
    {
      title: '添加人次',
      dataIndex: 'add_customer_count',
      valueType: 'digit',
      hideInSearch: true,
      sorter: true,
      showSorterTooltip: false,
      width: 120,
      tooltip: '统计添加渠道码的人次,若客户重复添加将会记录多条数据',
    },
    {
      title: '创建时间',
      dataIndex: 'created_at',
      valueType: 'dateRange',
      sorter: true,
      filtered: true,
      render: (dom, item) => {
        return (
          <div
            dangerouslySetInnerHTML={{
              __html: moment(item.created_at).format('YYYY-MM-DD HH:mm').split(' ').join('<br />'),
            }}
          />
        );
      },
    },
    {
      title: '操作',
      width: 180,
      valueType: 'option',
      render: (_, item) => [
        <a
          key='detail'
          onClick={() => {
            setItemDetailVisible(true);
            setCurrentItem(item);
          }}
        >
          详情
        </a>,
        <a
          key='download'
          onClick={() => {
            if (item?.qr_code) {
              FileSaver.saveAs(item?.qr_code, `${item.name}.png`);
            }
          }}
        >
          下载
        </a>,
        <Dropdown
          key='more'
          overlay={
            <Menu>
              <Menu.Item
                key='edit'
                onClick={() => {
                  history.push(`/staff-admin/customer-growth/contact-way/edit?id=${item.id}`);
                }}
              >
                修改
              </Menu.Item>
              <Menu.Item
                key='copy'
                onClick={() => {
                  history.push(`/staff-admin/customer-growth/contact-way/copy?id=${item.id}`);
                }}
              >
                复制
              </Menu.Item>
              {item.ext_creator_id === localStorage.getItem(LSExtStaffAdminID) && (
                <Menu.Item
                  key='delete'
                  onClick={() => {
                    Modal.confirm({
                      title: `删除渠道码`,
                      content: `是否确认删除「${item.name}」渠道码?`,
                      okText: '删除',
                      okType: 'danger',
                      cancelText: '取消',
                      onOk() {
                        return HandleRequest({ids: [item.id]}, Delete, () => {
                          actionRef.current?.clearSelected?.();
                          actionRef.current?.reload?.();
                        });
                      },
                    });
                  }}
                >删除</Menu.Item>
              )}
            </Menu>
          }
          trigger={['hover']}
        >
          <a style={{display: 'flex', alignItems: 'center'}}>
            编辑
            <CaretDownOutlined style={{fontSize: '8px', marginLeft: '3px'}}/>
          </a>
        </Dropdown>,
      ],
    },
  ];

  // @ts-ignore
  // @ts-ignore
  return (
    <PageContainer
      fixedHeader
      header={{
        title: '渠道活码列表',
        subTitle: (
          <a
            target={'_blank'}
            className={styles.tipsLink}
            // href={'https://www.openscrm.cn/wiki/contact-way'}
          >
            什么是渠道活码?
          </a>
        ),
      }}
      extra={[
        <Button
          key='create'
          type='primary'
          icon={<PlusOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
          onClick={() => {
            history.push('/staff-admin/customer-growth/contact-way/create');
          }}
        >
          新建活码
        </Button>,
      ]}
    >
      <ProTable<ContactWayItem>
        actionRef={actionRef}
        className={'table'}
        scroll={{x: 'max-content'}}
        columns={columns}
        rowKey='id'
        pagination={{
          pageSizeOptions: ['5', '10', '20', '50', '100'],
          pageSize: 5,
        }}
        toolBarRender={false}
        bordered={false}
        tableAlertRender={false}
        rowSelection={{
          onChange: (_, items) => {
            setSelectedItems(items);
          },
        }}
        tableRender={(_, dom) => (
          <div className={styles.mixedTable}>
            <div className={styles.leftPart}>
              <div className={styles.header}>
                <Button
                  key='1'
                  className={styles.button}
                  type='text'
                  onClick={() => setCreateGroupVisible(true)}
                  icon={<PlusSquareFilled style={{color: 'rgb(154,173,193)', fontSize: 15}}/>}
                >
                  新建分组
                </Button>
              </div>
              <Menu
                onSelect={(e) => {
                  setFilterGroupID(e.key as string);
                }}
                defaultSelectedKeys={['0']}
                mode='inline'
                className={styles.menuList}
              >
                <Menu.Item
                  icon={<FolderFilled style={{fontSize: '16px', color: '#138af8'}}/>}
                  key='0'
                >
                  全部
                </Menu.Item>
                {groupItems.map((item) => (
                  <Menu.Item
                    icon={<FolderFilled style={{fontSize: '16px', color: '#138af8'}}/>}
                    key={item.id}
                  >
                    <div className={styles.menuItem}>
                      {item.name}
                      <span className={styles.count}
                            style={{marginRight: item.is_default === True ? 16 : 0}}>{item.count}</span>
                    </div>
                    {item.is_default === False && (
                      <Dropdown
                        className={'more-actions'}
                        overlay={
                          <Menu
                            onClick={(e) => {
                              e.domEvent.preventDefault();
                              e.domEvent.stopPropagation();
                            }}
                          >
                            <Menu.Item
                              onClick={() => {
                                setCurrentGroup(item);
                                setEditGroupVisible(true);
                              }}
                              key='edit'
                            >
                              修改名称
                            </Menu.Item>
                            <Menu.Item
                              onClick={() => {
                                showDeleteGroupConfirm(item);
                              }}
                              key='delete'
                            >
                              删除分组
                            </Menu.Item>
                          </Menu>
                        }
                        trigger={['hover']}
                      >
                        <MoreOutlined style={{color: '#9b9b9b', fontSize: 18}}/>
                      </Dropdown>
                    )}
                  </Menu.Item>
                ))}
              </Menu>
            </div>
            <div className={styles.rightPart}>
              <div className={styles.tableWrap}>{dom}</div>
            </div>
          </div>
        )}
        params={{
          group_id: filterGroupID !== '0' ? filterGroupID : '',
        }}
        request={async (params, sort, filter) => {
          return ProTableRequestAdapter(params, sort, filter, Query);
        }}
        dateFormatter='string'
      />

      {selectedItems?.length > 0 && (
        // 底部选中条目菜单栏
        <FooterToolbar>
          <span>
            已选择 <a style={{fontWeight: 600}}>{selectedItems.length}</a> 项 &nbsp;&nbsp;
            <span></span>
          </span>
          <Divider type='vertical'/>
          <Button
            type='link'
            onClick={() => {
              actionRef.current?.clearSelected?.();
            }}
          >
            取消选择
          </Button>
          <Button onClick={() => setBatchUpdateVisible(true)}>批量分组</Button>
          <Button
            icon={<CloudDownloadOutlined/>}
            type={'primary'}
            onClick={() => {
              Modal.confirm({
                title: `批量下载渠道码`,
                content: `是否批量下载所选「${selectedItems.length}」个渠道码?`,
                okText: '下载',
                cancelText: '取消',
                onOk: async () => {
                  const zip = new JSZip();
                  // eslint-disable-next-line no-restricted-syntax
                  for (const item of selectedItems) {
                    if (item?.qr_code) {
                      // eslint-disable-next-line no-await-in-loop
                      const img = (await fetch(item?.qr_code)).blob();
                      zip.file(`${item.name}_${item.id}.png`, img);
                    }
                  }
                  const content = await zip.generateAsync({type: 'blob'});
                  FileSaver.saveAs(content, `渠道活码_${moment().format('YYYY_MM_DD')}.zip`);
                  actionRef.current?.clearSelected?.();
                  return true;
                },
              });
            }}
          >
            批量下载
          </Button>
          <Button
            icon={<DeleteOutlined/>}
            onClick={async () => {
              Modal.confirm({
                title: `删除渠道码`,
                content: `是否批量删除所选「${selectedItems.length}」个渠道码?`,
                okText: '删除',
                okType: 'danger',
                cancelText: '取消',
                onOk() {
                  return HandleRequest(
                    {ids: selectedItems.map((item) => item.id)},
                    Delete,
                    () => {
                      actionRef.current?.clearSelected?.();
                      actionRef.current?.reload?.();
                    },
                  );
                },
              });
            }}
            danger={true}
          >
            批量删除
          </Button>
        </FooterToolbar>
      )}

      <ModalForm
        width={468}
        className={'dialog from-item-label-100w'}
        layout={'horizontal'}
        visible={batchUpdateVisible}
        onVisibleChange={setBatchUpdateVisible}
        onFinish={async (values) => {
          return await HandleRequest(
            {ids: selectedItems.map((item) => item.id), ...values},
            BatchUpdate,
            () => {
              actionRef.current?.clearSelected?.();
              actionRef.current?.reload?.();
              setGroupItemsTimestamp(Date.now);
            },
          );
        }}
      >
        <h2 className='dialog-title'> 批量修改渠道码 </h2>
        <ProFormSelect
          // @ts-ignore
          options={groupItems.map((groupItem) => {
            return {key: groupItem.id, label: groupItem.name, value: groupItem.id};
          })}
          labelAlign={'left'}
          name='group_id'
          label='新分组'
          placeholder='请选择分组'
          rules={[
            {
              required: true,
              message: '请选择新分组',
            },
          ]}
        />
      </ModalForm>

      <ModalForm
        width={400}
        className={'dialog from-item-label-100w'}
        layout={'horizontal'}
        visible={createGroupVisible}
        onVisibleChange={setCreateGroupVisible}
        onFinish={async (params) =>
          HandleRequest({...currentGroup, ...params}, CreateGroup, () => {
            setGroupItemsTimestamp(Date.now);
          })
        }
      >
        <h2 className='dialog-title'> 新建分组 </h2>
        <ProFormText
          name='name'
          label='分组名称'
          tooltip='最长为 24 个汉字'
          placeholder='请输入分组名称'
          rules={[
            {
              required: true,
              message: '请填写分组名称',
            },
          ]}
        />
      </ModalForm>

      <ModalForm
        className={'dialog from-item-label-100w'}
        layout={'horizontal'}
        width={'500px'}
        visible={editGroupVisible}
        onVisibleChange={setEditGroupVisible}
        onFinish={async (params) =>
          HandleRequest({...currentGroup, ...params}, UpdateGroup, () => {
            setGroupItemsTimestamp(Date.now);
          })
        }
      >
        <h2 className='dialog-title'> 修改名称 </h2>
        <ProFormText
          colon={true}
          name='name'
          label='分组名称'
          tooltip='最长为 24 个汉字'
          placeholder='请输入分组名称'
          initialValue={currentGroup.name}
          rules={[
            {
              required: true,
              message: '请填写分组名称',
            },
          ]}
        />
      </ModalForm>

      <Modal
        className={styles.detailDialog}
        width={'800px'}
        visible={itemDetailVisible}
        onCancel={() => setItemDetailVisible(false)}
        footer={null}
      >
        <h2 className='dialog-title' style={{textAlign: "center", fontSize: 19}}> 渠道码详情 </h2>
        <Row>
          <Col span={8} className={styles.leftPart}>
            <img src={currentItem.qr_code}/>
            <h3>{currentItem.name}</h3>
            <Button
              type={'primary'}
              onClick={() => {
                if (currentItem?.qr_code) {
                  FileSaver.saveAs(currentItem?.qr_code, `${currentItem.name}.png`);
                }
              }}
            >
              下载渠道码
            </Button>
            <Button
              onClick={() => {
                history.push(
                  `/staff-admin/customer-growth/contact-way/edit?id=${currentItem.id}`,
                );
              }}
            >
              修改
            </Button>
          </Col>
          <Col span={16} className={styles.rightPart}>
            <div className={styles.section}>
              <div className={styles.titleWrapper}>
                <div className={styles.divider}/>
                <span className={styles.title}>基本设置</span>
              </div>
              <div className={styles.formItem}>
                <span className={styles.title}>创建时间:</span>
                <span className='date'>
                  {moment(currentItem.created_at).format('YYYY-MM-DD HH:mm')}
                </span>
              </div>
              <div className={styles.formItem}>
                <span className={styles.title}>绑定员工:</span>
                {currentItem.staffs?.map((staff) => (
                  <Tag
                    key={staff.id}
                    className={styles.staffTag}
                    style={{opacity: staff.online === False ? '0.5' : '1'}}
                  >
                    <img className={styles.icon} src={staff.avatar_url} alt={staff.name}/>
                    <span className={styles.text}>{staff.name}</span>
                  </Tag>
                ))}
              </div>
              <div className={styles.formItem}>
                <span className={styles.title}>备份员工:</span>
                {currentItem.backup_staffs?.map((staff) => (
                  <Tag
                    key={staff.id}
                    className={styles.staffTag}
                    style={{opacity: staff.online === False ? '0.5' : '1'}}
                  >
                    <img className={styles.icon} src={staff.avatar_url} alt={staff.name}/>
                    <span className={styles.text}>{staff.name}</span>
                  </Tag>
                ))}
              </div>
              <p className={styles.formItem}>
                <span className={styles.title}>自动通过好友:</span>
                {currentItem.auto_skip_verify_enable === True && (
                  <span>
                    {currentItem.skip_verify_start_time && '~'}
                    {currentItem.skip_verify_end_time}自动通过
                  </span>
                )}
                {currentItem.auto_skip_verify_enable === False && <span>未开启</span>}
              </p>
              <p className={styles.formItem}>
                <span className={styles.title}>客户标签:</span>
                {currentItem.customer_tags?.map((tag) => (
                  <Tag key={tag.id} className={styles.staffTag}>
                    <span className={styles.text}>{tag.name}</span>
                  </Tag>
                ))}
              </p>
            </div>
          </Col>
        </Row>
      </Modal>
    </PageContainer>
  );
}
Example #8
Source File: createModalForm.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
CreateModalForm: React.FC<CreateModalFormProps> = (props) => {
  const {allDepartments, initialValues} = props;
  const departmentMap = _.keyBy<DepartmentOption>(allDepartments, "ext_id");
  const [departmentSelectionVisible, setDepartmentSelectionVisible] = useState(false);
  const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>([]);
  const [deletedTagExtIDs, setDeletedTagExtIDs] = useState<string[]>([]);
  const formRef = useRef<FormInstance>();
  const minOrder = props.minOrder ? props.minOrder : 10000;
  const maxOrder = props.maxOrder ? props.maxOrder : 100000;
  const itemDataToFormData = (values: CustomerTagGroupItem) => {
    const params: any = {...values}
    params.is_global = (values.department_list === undefined || values.department_list === [] || values.department_list?.includes(0)) ? True : False;
    return params;
  }

  useEffect(() => {
    if (initialValues?.department_list?.includes(0)) {
      setSelectedDepartments([])
    } else {
      setSelectedDepartments(initialValues?.department_list?.map((ext_id) => departmentMap[ext_id]) || [])
    }
    formRef?.current?.setFieldsValue(itemDataToFormData(initialValues || {}));
  }, [initialValues])

  return (
    <>
      <Modal
        {...props}
        width={568}
        className={'dialog from-item-label-100w'}
        visible={props.visible}
        onOk={() => {
          formRef.current?.submit();
        }}
        onCancel={() => {
          props.setVisible(false);
        }}
      >
        <ProForm
          submitter={{
            render: false,
          }}
          initialValues={itemDataToFormData(initialValues || {})}
          formRef={formRef}
          layout={'horizontal'}
          onFinish={async (values) => {
            const params: CustomerTagGroupItem = {
              ...props.initialValues,
              ...values,
              department_list: selectedDepartments.map((item) => item.ext_id),
            };

            if (values.is_global === True) {
              params.department_list = [0];
            }

            if (props.type === 'create') {
              if (values.order_type === 'max') {
                params.order = maxOrder + 1;
              }

              if (values.order_type === 'min') {
                params.order = minOrder - 1 >= 0 ? minOrder - 1 : 0;
              }
            }

            if (props.type === 'edit' && deletedTagExtIDs.length > 0) {
              params.remove_ext_tag_ids = deletedTagExtIDs;
            }

            await props.onFinish(params);
            setDeletedTagExtIDs([]);
          }}
        >
          <h3 className="dialog-title" style={{fontSize: 18}}>
            {' '}
            {props.type === 'edit' ? '修改标签组' : '新建标签组'}{' '}
          </h3>
          <ProFormText
            name="name"
            label="标签组名称"
            width={'md'}
            placeholder="请输入标签组名称"
            rules={[
              {
                required: true,
                message: '标签组名称必填',
              },
            ]}
          />

          <ProFormRadio.Group
            name="is_global"
            label="可见范围"
            options={[
              {
                label: '全部员工',
                value: True,
              },
              {
                label: '部门可用',
                value: False,
              },
            ]}
          />

          <ProFormDependency name={['is_global']}>
            {({is_global}) => {
              // 部门可用
              if (is_global === Disable) {
                return (
                  <>
                    <Row>
                      <ProForm.Item label={'选择可用部门'}>
                        <Button
                          icon={<PlusOutlined/>}
                          onClick={() => setDepartmentSelectionVisible(true)}
                        >
                          添加部门
                        </Button>
                      </ProForm.Item>
                    </Row>

                    <Row>
                      <Space direction={'horizontal'} wrap={true} style={{marginBottom: 6}}>
                        {selectedDepartments?.length > 0 && selectedDepartments.map((item, index) => {
                          if (!item?.id) {
                            return <div key={index}></div>
                          }
                          return (
                            <div key={item.id} className={'department-item'}>
                              <Badge
                                count={
                                  <CloseCircleOutlined
                                    onClick={() => {
                                      setSelectedDepartments(
                                        selectedDepartments.filter(
                                          (department) => department.id !== item.id,
                                        ),
                                      );
                                    }}
                                    style={{color: 'rgb(199,199,199)'}}
                                  />
                                }
                              >
                              <span className={'container'}>
                                <FolderFilled
                                  style={{
                                    color: '#47a7ff',
                                    fontSize: 20,
                                    marginRight: 6,
                                    verticalAlign: -6,
                                  }}
                                />
                                {item.name}
                              </span>
                              </Badge>
                            </div>
                          )
                        })}
                      </Space>
                    </Row>
                  </>
                );
              }

              // 全局可用
              return <></>;
            }}
          </ProFormDependency>

          {props.type === 'create' && (
            <ProFormRadio.Group
              name="order_type"
              label="默认排序"
              initialValue={'max'}
              options={[
                {
                  label: '排最前面',
                  value: 'max',
                },
                {
                  label: '排最后面',
                  value: 'min',
                },
              ]}
            />
          )}

          <ProFormList
            label={'标签名称'}
            name="tags"
            actionRender={(field: FormListFieldData, action: FormListOperation) => {
              const currentKey = field.name;
              const lastKey = formRef.current?.getFieldValue('tags').length - 1;
              return [
                <Tooltip key={'moveUp'} title="上移">
                  <UpCircleOutlined
                    className={'ant-pro-form-list-action-icon'}
                    onClick={() => {
                      if (currentKey - 1 >= 0) {
                        action.move(currentKey, currentKey - 1);
                      } else {
                        action.move(currentKey, lastKey);
                      }
                    }}
                  />
                </Tooltip>,
                <Tooltip key={'moveDown'} title="下移">
                  <DownCircleOutlined
                    className={'ant-pro-form-list-action-icon'}
                    onClick={() => {
                      if (currentKey + 1 <= lastKey) {
                        action.move(currentKey, currentKey + 1);
                      } else {
                        action.move(currentKey, 0);
                      }
                    }}
                  />
                </Tooltip>,
                <Tooltip key={'copy'} title="复制">
                  <CopyOutlined
                    className={'ant-pro-form-list-action-icon'}
                    onClick={() => {
                      action.add(formRef.current?.getFieldValue('tags')[currentKey]);
                    }}
                  />
                </Tooltip>,
                <Tooltip key={'remove'} title="删除">
                  <DeleteOutlined
                    className={'ant-pro-form-list-action-icon'}
                    onClick={() => {
                      if (formRef.current?.getFieldValue('tags')[currentKey]?.ext_id) {
                        setDeletedTagExtIDs([
                          ...deletedTagExtIDs,
                          formRef.current?.getFieldValue('tags')[currentKey].ext_id,
                        ]);
                      }
                      action.remove(currentKey);
                    }}
                  />
                </Tooltip>,
              ];
            }}
            creatorButtonProps={{
              type: 'default',
              style: {width: '128px'},
              position: 'bottom',
              creatorButtonText: '添加标签',
            }}
            creatorRecord={{
              name: '',
            }}
            rules={[
              {
                // @ts-ignore
                required: true,
                message: '标签名称必填',
              },
            ]}
          >
            <ProFormText
              name="name"
              width={'sm'}
              fieldProps={{
                allowClear: false,
                style: {
                  // width: '230px',
                },
              }}
              placeholder="请输入标签名称"
              rules={[
                {
                  required: true,
                  message: '标签名称必填',
                },
              ]}
            />
          </ProFormList>
        </ProForm>
      </Modal>

      <DepartmentSelectionModal
        visible={departmentSelectionVisible}
        setVisible={setDepartmentSelectionVisible}
        defaultCheckedDepartments={selectedDepartments}
        onFinish={(values) => {
          setSelectedDepartments(values);
        }}
        allDepartments={props.allDepartments}
      />
    </>
  );
}
Example #9
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
CustomerTagGroupList: React.FC = () => {
  const [currentItem, setCurrentItem] = useState<CustomerTagGroupItem>({});
  const [tagGroups, setTagGroups] = useState<CustomerTagGroupItem[]>([]);
  const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
  const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
  const [syncLoading, setSyncLoading] = useState<boolean>(false);
  const [actionLoading, setActionLoading] = useState<boolean>(false);
  const [inputLoading, setInputLoading] = useState<boolean>(false);
  const [minOrder, setMinOrder] = useState<number>(10000);
  const [maxOrder, setMaxOrder] = useState<number>(100000);
  const [currentInputTagGroupExtID, setCurrentInputTagGroupExtID] = useState<string>();
  const [allDepartments, setAllDepartments] = useState<DepartmentOption[]>([]);
  const [allDepartmentMap, setAllDepartmentMap] = useState<Dictionary<DepartmentOption>>({});
  const queryFilterFormRef = useRef<FormInstance>();

  useEffect(() => {
    QueryDepartment({page_size: 5000}).then((res) => {
      if (res.code === 0) {
        const departments =
          res?.data?.items?.map((item: DepartmentInterface) => {
            return {
              label: item.name,
              value: item.ext_id,
              ...item,
            };
          }) || [];
        setAllDepartments(departments);
        setAllDepartmentMap(_.keyBy<DepartmentOption>(departments, 'ext_id'));

      } else {
        message.error(res.message);
      }
    });
    queryFilterFormRef.current?.submit();
  }, []);

  // @ts-ignore
  // @ts-ignore
  return (
    <PageContainer
      fixedHeader
      header={{
        title: '客户标签管理',
      }}
      extra={[
        <Button
          key='create'
          type='primary'
          icon={<PlusOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
          onClick={() => {
            setCreateModalVisible(true);
          }}
        >
          添加标签组
        </Button>,

        <Button
          key={'sync'}
          type='dashed'
          icon={<SyncOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
          loading={syncLoading}
          onClick={async () => {
            setSyncLoading(true);
            const res: CommonResp = await Sync();
            if (res.code === 0) {
              setSyncLoading(false);
              message.success('同步成功');
              queryFilterFormRef.current?.submit();
            } else {
              setSyncLoading(false);
              message.error(res.message);
            }
          }}
        >
          同步企业微信标签
        </Button>,
      ]}
    >
      <ProCard className={styles.queryFilter}>
        <QueryFilter
          formRef={queryFilterFormRef}
          onReset={() => {
            queryFilterFormRef.current?.submit();
          }}
          onFinish={async (params: any) => {
            setActionLoading(true);
            const res: CommonResp = await Query({
              ...params,
              page_size: 5000,
              sort_field: 'order',
              sort_type: 'desc',
            });
            setActionLoading(false);
            if (res.code === 0) {
              setTagGroups(res.data.items);
              if (res.data?.items[0]) {
                setMaxOrder(res.data.items[0]?.order);
              }
              if (res.data?.items.length >= 1 && res.data?.items[res.data?.items.length - 1]) {
                let min = res.data?.items[res.data?.items.length - 1];
                min = min - 1 >= 0 ? min - 1 : 0;
                setMinOrder(min);
              }
            } else {
              message.error('查询标签失败');
              setTagGroups([]);
            }
          }}
        >
          <Form.Item label='可用部门' name='ext_department_ids'>
            <DepartmentTreeSelect
              onChange={() => {
                queryFilterFormRef.current?.submit();
              }}
              options={allDepartments}
            />
          </Form.Item>

          <ProFormText width={'md'} name='name' label='搜索' placeholder='请输入关键词'/>

        </QueryFilter>
      </ProCard>

      <ProCard style={{marginTop: 12}} bodyStyle={{paddingTop: 0}} gutter={0}>
        <Spin spinning={actionLoading}>
          {(!tagGroups || tagGroups.length === 0) && <Empty style={{marginTop: 36, marginBottom: 36}}/>}
          {tagGroups && tagGroups.length > 0 && (
            <ReactSortable<any>
              handle={'.draggable-button'}
              className={styles.tagGroupList}
              list={tagGroups}
              setList={setTagGroups}
              swap={true}
              onEnd={async (e) => {
                // @ts-ignore
                const from = tagGroups[e.newIndex];
                // @ts-ignore
                const to = tagGroups[e.oldIndex];
                const res = await ExchangeOrder({id: from.id, exchange_order_id: to.id});
                if (res.code !== 0) {
                  message.error(res.message)
                }
              }}
            >
              {tagGroups.map((tagGroup) => (
                <Row className={styles.tagGroupItem} data-id={tagGroup.id} key={tagGroup.ext_id}>
                  <Col md={4} className={styles.tagName}>
                    <h4>{tagGroup.name}</h4>
                  </Col>
                  <Col md={16} className={styles.tagList}>
                    <Row>
                      可见范围:
                      {tagGroup.department_list && !tagGroup.department_list.includes(0) ? (
                        <Space direction={'horizontal'} wrap={true} style={{marginBottom: 6}}>
                          {tagGroup.department_list.map((id) => (
                            <div key={id}>
                            <span>
                              <FolderFilled
                                style={{
                                  color: '#47a7ff',
                                  fontSize: 20,
                                  marginRight: 6,
                                  verticalAlign: -6,
                                }}
                              />
                              {allDepartmentMap[id]?.name}
                            </span>
                            </div>
                          ))}
                        </Space>
                      ) : (
                        <span>全部员工可见</span>
                      )}
                    </Row>
                    <Row style={{marginTop: 12}}>
                      <Space direction={'horizontal'} wrap={true}>
                        <Button
                          icon={<PlusOutlined/>}
                          onClick={() => {
                            setCurrentInputTagGroupExtID(tagGroup.ext_id);
                          }}
                        >
                          添加
                        </Button>

                        {currentInputTagGroupExtID === tagGroup.ext_id && (
                          <Input
                            autoFocus={true}
                            disabled={inputLoading}
                            placeholder='逗号分隔,回车保存'
                            onBlur={() => setCurrentInputTagGroupExtID('')}
                            onPressEnter={async (e) => {
                              setInputLoading(true);
                              const res = await CreateTag({
                                names: e.currentTarget.value
                                  .replace(',', ',')
                                  .split(',')
                                  .filter((val) => val),
                                ext_tag_group_id: tagGroup.ext_id || '',
                              });
                              if (res.code === 0) {
                                setCurrentInputTagGroupExtID('');
                                tagGroup.tags?.unshift(...res.data);
                              } else {
                                message.error(res.message);
                              }
                              setInputLoading(false);
                            }}
                          />
                        )}
                        {tagGroup.tags?.map((tag) => (
                          <Tag className={styles.tagItem} key={tag.id}>
                            {tag.name}
                          </Tag>
                        ))}
                      </Space>
                    </Row>
                  </Col>
                  <Col md={4} className={styles.groupAction}>
                    <Tooltip title="拖动可实现排序" trigger={['click']}>
                      <Button
                        className={'draggable-button'}
                        icon={<DragOutlined
                          style={{cursor: 'grabbing'}}
                        />}
                        type={'text'}
                      >
                        排序
                      </Button>
                    </Tooltip>

                    <Button
                      icon={<EditOutlined/>}
                      type={'text'}
                      onClick={() => {
                        setCurrentItem(tagGroup);
                        setEditModalVisible(true);
                      }}
                    >
                      修改
                    </Button>
                    <Button
                      icon={<DeleteOutlined/>}
                      type={'text'}
                      onClick={() => {
                        Modal.confirm({
                          title: `删除标签分组`,
                          content: `是否确认删除「${tagGroup.name}」分组?`,
                          okText: '删除',
                          okType: 'danger',
                          cancelText: '取消',
                          onOk() {
                            return HandleRequest({ext_ids: [tagGroup.ext_id]}, Delete, () => {
                              queryFilterFormRef.current?.submit();
                            });
                          },
                        });
                      }}
                    >
                      删除
                    </Button>
                  </Col>
                </Row>
              ))}
            </ReactSortable>
          )}
        </Spin>
      </ProCard>

      <CreateModalForm
        // 创建标签
        type={'create'}
        minOrder={minOrder}
        maxOrder={maxOrder}
        allDepartments={allDepartments}
        setVisible={setCreateModalVisible}
        initialValues={{tags: [{name: ''}], department_list: [0]}}
        visible={createModalVisible}
        onFinish={async (values) => {
          await HandleRequest(values, Create, () => {
            queryFilterFormRef.current?.submit();
            setCreateModalVisible(false);
          });
        }}
      />

      <CreateModalForm
        // 修改标签
        type={'edit'}
        destroyOnClose={true}
        minOrder={minOrder}
        maxOrder={maxOrder}
        allDepartments={allDepartments}
        setVisible={setEditModalVisible}
        visible={editModalVisible}
        initialValues={currentItem}
        onFinish={async (values) => {
          await HandleRequest(values, Update, () => {
            queryFilterFormRef.current?.submit();
            setEditModalVisible(false);
          });
        }}
      />
    </PageContainer>
  );
}
Example #10
Source File: EnterpriseScript.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
EnterpriseScript: (props: any, ref: any) => JSX.Element = (props, ref) => {
  const actionRef = useRef<ActionType>();
  const formRef = useRef({} as any);
  const [keyWord, setKeyWord] = useState('')
  const [groupId, setGroupId] = useState('')
  const [department_id, setDepartmentId] = useState<number[]>([])
  const [allDepartments, setAllDepartments] = useState<DepartmentOption[]>([]);
  const [allDepartmentMap, setAllDepartmentMap] = useState<Dictionary<DepartmentOption>>({});
  const [targetScriptGroup, setTargetScriptGroup] = useState<Partial<ScriptGroup.Item>>({})
  const [groupModalVisible, setGroupModalVisible] = useState(false);
  const groupModalRef = useRef<any>({});
  const scriptModalRef = useRef<any>({});
  const [groupItemsTimestamp, setGroupItemsTimestamp] = useState(Date.now);
  const [scriptModalVisible, setScriptModalVisible] = useState(false);
  const [targetScript, setTargetScript] = useState<Partial<ScriptGroup.Item>>({})
  const [groupItems, setGroupItems] = useState<Partial<ScriptGroup.Item>[]>([]);
  const [allGroupMap, setAllGroupMap] = useState<Dictionary<ScriptGroup.Item>>({});
  const [selectedItems, setSelectedItems] = useState<Script.Item[]>([]);

  useImperativeHandle(ref, () => {
    return {
      createEnterpriseScript: () => {
        setTargetScript({})
        scriptModalRef.current.open({reply_details: [{content_type: 2}]})
      },
    }
  })
  useEffect(() => {
    QueryEnterpriseScriptGroups({page_size: 5000}).then(res => {
      if (res?.code === 0) {
        setGroupItems(res?.data?.items || [])
        setAllGroupMap(_.keyBy<ScriptGroup.Item>(res?.data?.items || [], 'id'));
      }
    }).catch((err) => {
      message.error(err);
    });
  }, [groupItemsTimestamp])

  useEffect(() => {
    QueryDepartmentList({page_size: 5000}).then((res) => {
      if (res?.code === 0) {
        const departments =
          res?.data?.items?.map((item: DepartmentInterface) => {
            return {
              label: item?.name,
              value: item?.ext_id,
              ...item,
            };
          }) || [];
        setAllDepartments(departments);
        setAllDepartmentMap(_.keyBy<DepartmentOption>(departments, 'ext_id'));
      } else {
        message.error(res.message);
      }
    });
  }, []);

  // 后端数据转为前端组件FormItem -- name
  const transferParams = (paramsFromBackEnd: any) => {
    const newReplyDetails = []
    for (let i = 0; i < paramsFromBackEnd.reply_details.length; i += 1) {
      const typeObjectKey = typeEnums[paramsFromBackEnd.reply_details[i].content_type]
      const replyDetailItem: FrontEndReplyDetailParams = {
        content_type: paramsFromBackEnd.reply_details[i].content_type, id: paramsFromBackEnd.reply_details[i].id
      }
      const quickReplyContent = paramsFromBackEnd.reply_details[i].quick_reply_content
      if (typeObjectKey === 'text') {
        replyDetailItem.text_content = quickReplyContent.text.content
      }
      if (typeObjectKey === 'image') {
        replyDetailItem.image_title = quickReplyContent.image.title
        replyDetailItem.image_size = quickReplyContent.image.size
        replyDetailItem.image_picurl = quickReplyContent.image.picurl
      }
      if (typeObjectKey === 'link') {
        replyDetailItem.link_title = quickReplyContent.link.title
        replyDetailItem.link_desc = quickReplyContent.link.desc
        replyDetailItem.link_picurl = quickReplyContent.link.picurl
        replyDetailItem.link_url = quickReplyContent.link.url
      }
      if (typeObjectKey === 'pdf') {
        replyDetailItem.pdf_title = quickReplyContent.pdf.title
        replyDetailItem.pdf_size = quickReplyContent.pdf.size
        replyDetailItem.pdf_fileurl = quickReplyContent.pdf.fileurl
      }
      if (typeObjectKey === 'video') {
        replyDetailItem.video_title = quickReplyContent.video.title
        replyDetailItem.video_size = quickReplyContent.video.size
        replyDetailItem.video_picurl = quickReplyContent.video.picurl
      }
      newReplyDetails.push(replyDetailItem)
    }
    return {...paramsFromBackEnd, reply_details: newReplyDetails}
  }


  const columns: ProColumns<Script.Item>[] = [
    {
      title: '话术内容',
      dataIndex: 'keyword',
      width: '18%',
      hideInSearch: false,
      render: (dom: any, item: any) => {
        return (
          <ScriptContentPreView script={item}/>
        )
      },
    },
    {
      title: '标题',
      dataIndex: 'name',
      key:'name',
      valueType: 'text',
      hideInSearch: true,
    },
    {
      title: '发送次数',
      dataIndex: 'send_count',
      key: 'name',
      valueType: 'digit',
      hideInSearch: true,
      width: 100,
    },
    {
      title: '所属分组',
      width: '14%',
      dataIndex: 'group_id',
      key: 'group_id',
      valueType: 'text',
      hideInSearch: true,
      render: (dom: any) => {
        return (
          <span>{allGroupMap[dom]?.name || '-'}</span>
        )
      },
    },
    {
      title: '创建人',
      dataIndex: 'staff_name',
      key: 'staff_name',
      hideInSearch: true,
      render: (dom, item) => (
        <div className={'tag-like-staff-item'}>
          <img className={'icon'} src={item.avatar}/>
          <span className={'text'}>{dom}</span>
        </div>
      )
    },
    {
      title: '创建时间',
      dataIndex: 'created_at',
      key: 'created_at',
      valueType: 'dateTime',
      hideInSearch: true,
      render: (dom, item) => {
        return (
          <div
            dangerouslySetInnerHTML={{
              __html: moment(item.created_at)
                .format('YYYY-MM-DD HH:mm')
                .split(' ')
                .join('<br />'),
            }}
          />
        );
      },
    },
    {
      title: '类型',
      dataIndex: 'quick_reply_type',
      key: 'quick_reply_type',
      valueType: 'text',
      hideInSearch: true,
      render: (dom: any) => {
        return <span>{typeEnums[dom]}</span>
      }
    },
    {
      title: '可用部门',
      dataIndex: 'department_id',
      key: 'department_id',
      valueType: 'text',
      hideInSearch: false,
      hideInTable: true,
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      renderFormItem: (schema, config, form) => {
        return (
          <DepartmentTreeSelect
            options={allDepartments}
          />
        )
      },
    },
    {
      title: '操作',
      valueType: 'text',
      width: '10%',
      hideInSearch: true,
      render: (dom) => {
        return (
          <Space>
            <Button
              type={'link'}
              onClick={() => {
                // @ts-ignore
                const target = transferParams(dom)
                setTargetScript(target)
                // @ts-ignore
                scriptModalRef.current.open(target)
              }}
            >修改</Button>
            <Button
              type={'link'}
              onClick={() => {
                Modal.confirm({
                  title: `删除话术`,
                  // @ts-ignore
                  content: `是否确认删除「${dom?.name}」话术?`,
                  okText: '删除',
                  okType: 'danger',
                  cancelText: '取消',
                  onOk() {
                    // @ts-ignore
                    return HandleRequest({ids: [dom?.id]}, DeleteEnterpriseScriptList, () => {
                      actionRef?.current?.reload()
                    })
                  },
                });
              }}
            >
              删除
            </Button>
          </Space>
        )
      }
    },
  ]
  const getUseableDepartment = (departments: number[]) => {
    return departments.map((id) => {
      return <span key={id}>
           {id === 0 ? '全部员工可见' : allDepartmentMap[id]?.label} &nbsp;
        </span>
    })
  }

  return (
    <>
      <ProTable
        onSubmit={() => {
          setKeyWord(formRef.current.getFieldValue('keyword'))
          setDepartmentId(formRef.current.getFieldValue('department_id'))
        }}
        onReset={() => {
          setKeyWord(formRef?.current?.getFieldValue('keyword'))
          setDepartmentId(formRef?.current?.getFieldValue('department_id'))
        }}
        actionRef={actionRef}
        formRef={formRef}
        className={'table'}
        scroll={{x: 'max-content'}}
        columns={columns}
        rowKey={(scriptItem) => scriptItem.id}
        pagination={{
          pageSizeOptions: ['5', '10', '20', '50', '100'],
          pageSize: 5,
        }}
        toolBarRender={false}
        bordered={false}
        tableAlertRender={false}
        rowSelection={{
          onChange: (__, items) => {
            setSelectedItems(items);
          },
        }}
        tableRender={(block, dom) => (
          <div className={styles.mixedTable}>
            <div className={styles.leftPart}>
              <div className={styles.header}>
                <Button
                  key="1"
                  className={styles.button}
                  type="text"
                  onClick={() => {
                    setTargetScriptGroup({})
                    groupModalRef.current.open({})
                  }}
                  icon={<PlusSquareFilled style={{color: 'rgb(154,173,193)', fontSize: 15}}/>}
                >
                  新建分组
                </Button>
              </div>
              <Menu
                onSelect={(e) => {
                  setGroupId(e.key as string)
                }}
                defaultSelectedKeys={['']}
                mode="inline"
                className={styles.menuList}
              >
                <Menu.Item
                  icon={<FolderFilled style={{fontSize: '16px', color: '#138af8'}}/>}
                  onClick={() => setTargetScriptGroup({})}
                  key=""
                >
                  全部
                </Menu.Item>
                {groupItems.map((item) => (
                  <Menu.Item
                    icon={<FolderFilled style={{fontSize: '16px', color: '#138af8'}}/>}
                    key={item.id}
                    onClick={() => {
                      setTargetScriptGroup(item)
                    }}
                  >
                    {item.name}
                    <Dropdown
                      className={'more-actions'}
                      overlay={
                        <Menu
                          onClick={(e) => {
                            e.domEvent.preventDefault();
                            e.domEvent.stopPropagation();
                          }}
                        >
                          <Menu.Item
                            onClick={() => {
                              setTargetScriptGroup(item)
                              groupModalRef.current.open(item)
                            }}
                            key="edit"
                          >
                            <a type={'link'}>修改分组</a>

                          </Menu.Item>
                          <Menu.Item
                            key="delete"
                          >
                            <a
                              type={'link'}
                              onClick={() => {
                                Modal.confirm({
                                  title: `删除分组`,
                                  // @ts-ignore
                                  content: `是否确认删除「${item?.name}」分组?`,
                                  okText: '删除',
                                  okType: 'danger',
                                  cancelText: '取消',
                                  onOk() {
                                    // @ts-ignore
                                    return HandleRequest({ids: [item.id]}, DeleteEnterpriseScriptGroups, () => {
                                      setGroupItemsTimestamp(Date.now)
                                    })
                                  },
                                });
                              }}
                            >
                              删除分组
                            </a>
                          </Menu.Item>
                        </Menu>
                      }
                      trigger={['hover']}
                    >
                      <MoreOutlined style={{color: '#9b9b9b', fontSize: 18}}/>
                    </Dropdown>

                  </Menu.Item>
                ))}

              </Menu>
            </div>
            <div className={styles.rightPart}>
              {
                targetScriptGroup?.id
                &&
                <div className={styles.aboveTableWrap}>
                  可见范围&nbsp;<Tooltip title={'范围内的员工可在企业微信【侧边栏】使用话术'}><QuestionCircleOutlined /></Tooltip>:
                  {allGroupMap[targetScriptGroup.id]?.departments ? getUseableDepartment(allGroupMap[targetScriptGroup.id]?.departments) : '全部员工可见'}
                  &nbsp;&nbsp;&nbsp;
                  <a onClick={() => groupModalRef.current.open(allGroupMap[targetScriptGroup!.id!])}>修改</a>
                </div>
              }
              <div className={styles.tableWrap}>{dom}</div>
            </div>
          </div>
        )}
        params={{
          group_id: groupId || '',
          department_ids: department_id || [],
          keyword: keyWord || ''
        }}
        request={async (params, sort, filter) => {
          return ProTableRequestAdapter(params, sort, filter, QueryEnterpriseScriptList);
        }}
        dateFormatter="string"
      />

      {selectedItems?.length > 0 && (
        // 底部选中条目菜单栏
        <FooterToolbar>
          <span>
            已选择 <a style={{fontWeight: 600}}>{selectedItems.length}</a> 项 &nbsp;&nbsp;
          </span>
          <Divider type='vertical'/>
          <Button
            type='link'
            onClick={() => {
              actionRef.current?.clearSelected?.();
            }}
          >
            取消选择
          </Button>
          <Button
            icon={<DeleteOutlined/>}
            onClick={async () => {
              Modal.confirm({
                title: `删除渠道码`,
                content: `是否批量删除所选「${selectedItems.length}」个渠道码?`,
                okText: '删除',
                okType: 'danger',
                cancelText: '取消',
                onOk() {
                  return HandleRequest(
                    {ids: selectedItems.map((item) => item.id)},
                    DeleteEnterpriseScriptList,
                    () => {
                      actionRef.current?.clearSelected?.();
                      actionRef.current?.reload?.();
                    },
                  );
                },
              });
            }}
            danger={true}
          >
            批量删除
          </Button>
        </FooterToolbar>
      )}
      {/* 新增&修改分组 */}
      <GroupModal
        initialValues={targetScriptGroup}
        allDepartments={allDepartments}
        ref={groupModalRef}
        visible={groupModalVisible}
        setVisible={setGroupModalVisible}
        onFinish={async (values, action) => {
          if (action === 'create') {
            await HandleRequest({...values, sub_groups: []}, CreateEnterpriseScriptGroups, () => {
              setGroupItemsTimestamp(Date.now)
              groupModalRef.current.close()
            })
          } else {
            await HandleRequest({...values, sub_groups: []}, UpdateEnterpriseScriptGroups, () => {
              setGroupItemsTimestamp(Date.now)
              groupModalRef.current.close()
            })
          }
        }}
      />
      {/* 新增&修改话术 */}
      <ScriptModal
        allDepartments={allDepartments}
        initialValues={targetScript}
        ref={scriptModalRef}
        visible={scriptModalVisible}
        setVisible={setScriptModalVisible}
        setPropsGroupsTimestamp={setGroupItemsTimestamp}
        onCancel={() => {
          setGroupItemsTimestamp(Date.now)
        }}
        onFinish={async (values, action) => {
          if (action === 'create') {
            await HandleRequest({...values}, CreateEnterpriseScriptList, () => {
              setGroupItemsTimestamp(Date.now)
              actionRef?.current?.reload()
              scriptModalRef.current.close()
            })
          } else {
            const {reply_details, id, group_id, name, deleted_ids} = values
            await HandleRequest({reply_details, id, group_id, name, deleted_ids}, UpdateEnterpriseScriptList, () => {
              setGroupItemsTimestamp(Date.now)
              actionRef?.current?.reload()
              scriptModalRef.current.close()
            })
          }
        }}
      />
    </>
  )
}
Example #11
Source File: GroupModal.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
GroupModal = (props: GroupModalProps, ref: any) => {
  const {initialValues, visible, setVisible, allDepartments} = props
  const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>([]);
  const [departmentSelectionVisible, setDepartmentSelectionVisible] = useState(false);
  const formRef = React.useRef<FormInstance>();
  const departmentMap = _.keyBy<DepartmentOption>(allDepartments, "ext_id");
  const itemDataToFormData = (values: ScriptGroup.Item) => {
    const params: any = {...values}
    params.is_global = values.departments?.includes(0) ? True : False;
    return params;
  }

  useImperativeHandle(ref, () => {
    return {
      open: (item?: ScriptGroup.Item) => {
        setTimeout(() => {
          formRef.current?.setFieldsValue(itemDataToFormData(item as ScriptGroup.Item))
          if (item?.departments?.includes(0)) {
            setSelectedDepartments([])
          } else {
            setSelectedDepartments(item?.departments?.map((ext_id) => departmentMap[ext_id]) || [])
          }
        }, 100)
        setVisible(true)
      },
      close: () => {
        formRef.current?.resetFields()
        setSelectedDepartments([])
        setVisible(false)
      }
    }
  })
  return (
    <>
      <Modal
        {...props}
        width={568}
        className={'dialog'}
        visible={visible}
        onOk={() => {
          if(formRef.current?.getFieldValue('is_global')===True){
            formRef.current?.submit();
          }
          if(formRef.current?.getFieldValue('is_global')===False){
            if(selectedDepartments?.length>0){
              formRef.current?.submit();
            }else{
              message.warning('请选择部门')
            }
          }
        }}
        onCancel={() => {
          setVisible(false)
          formRef.current?.resetFields()
        }}
      >
        <ProForm
          submitter={{
            render: false,
          }}
          formRef={formRef}
          // initialValues={initialValues}
          layout={'horizontal'}
          onFinish={async (values) => {
            const params: any = {
              ...initialValues,
              ...values,
              departments: selectedDepartments.map((item) => item.ext_id),
            };
            if (values.is_global === True) {
              params.departments = [0];
            }

            await props.onFinish(params, params.id ? 'update' : 'create');
          }}
        >
          <h3 className="dialog-title" style={{fontSize: 18}}>
            {initialValues?.id ? '修改分组' : '添加分组'}
          </h3>
          <ProFormText
            name="name"
            label="分组名称"
            width={'md'}
            placeholder="请输入分组名称"
            rules={[
              {
                required: true,
                message: '分组名称必填',
              },
            ]}
          />

          <ProFormRadio.Group
            name="is_global"
            label="可见范围"
            initialValue={True}
            options={[
              {
                label: '全部员工',
                value: True,
              },
              {
                label: '部门可用',
                value: False,
              },
            ]}
          />

          <ProFormDependency name={['is_global']}>
            {({is_global}) => {
              // 部门可用
              if (is_global === Disable) {
                return (
                  <>
                    <Row>
                      <ProForm.Item label={'选择可用部门'}>
                        <Button
                          icon={<PlusOutlined/>}
                          onClick={() => setDepartmentSelectionVisible(true)}
                        >
                          添加部门
                        </Button>
                      </ProForm.Item>
                    </Row>

                    <Row>
                      <Space direction={'horizontal'} wrap={true} style={{marginBottom: 6}}>
                        {selectedDepartments?.length > 0 && selectedDepartments.map((item, index) => {
                          if (!item?.id) {
                            return <div key={index}/>
                          }
                          return (
                            <div key={item.id} className={'department-item'}>
                              <Badge
                                count={
                                  <CloseCircleOutlined
                                    onClick={() => {
                                      setSelectedDepartments(
                                        selectedDepartments.filter(
                                          (department) => department.id !== item.id,
                                        ),
                                      );
                                    }}
                                    style={{color: 'rgb(199,199,199)'}}
                                  />
                                }
                              >
                                  <span className={'container'}>
                                      <FolderFilled
                                        style={{
                                          color: '#47a7ff',
                                          fontSize: 20,
                                          marginRight: 6,
                                          verticalAlign: -6,
                                        }}
                                      />
                                    {item.name}
                                  </span>
                              </Badge>
                            </div>
                          )
                        })}
                      </Space>
                    </Row>
                  </>
                );
              }
              return <></>;
            }}
          </ProFormDependency>


        </ProForm>
      </Modal>
      <DepartmentSelectionModal
        visible={departmentSelectionVisible}
        setVisible={setDepartmentSelectionVisible}
        defaultCheckedDepartments={selectedDepartments}
        onFinish={(values) => {
          setSelectedDepartments(values);
        }}
        allDepartments={props.allDepartments}
      />
    </>

  )
}
Example #12
Source File: ScriptModal.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
ScriptModal: (props: ScriptModalProps, ref: any) => JSX.Element = (props, ref) => {
  const formRef = useRef<FormInstance>();
  const [fileInfoAry, setFileInfoAry] = useState<{ fileName: string, fileSize: string, key: number }[]>([]) // PDF的文件信息数组
  const groupModalRef = useRef<any>({});
  const [groupModalVisible, setGroupModalVisible] = useState(false);
  const [groupItems, setGroupItems] = useState<Partial<ScriptGroup.Item>[]>([]);
  const [groupItemsTimestamp, setGroupItemsTimestamp] = useState(Date.now);
  const [deletedIds,setDeletedIds] = useState<number[]>([])

  React.useImperativeHandle(ref, () => {
    return {
      open: (item: Partial<Script.Item>) => {
        setGroupItemsTimestamp(Date.now)
        setTimeout(() => {
          formRef?.current?.setFieldsValue(item)
        }, 100)
        props.setVisible(true)
      },
      close: () => {
        formRef?.current?.resetFields()
        props.setVisible(false)
      }
    }
  })

  useEffect(() => {
    QueryEnterpriseScriptGroups({page_size: 5000}).then(res => {
      if (res?.code === 0 && res?.data) {
        setGroupItems(res?.data?.items || [])
      }
    }).catch((err) => {
      message.error(err);
    });
  }, [groupItemsTimestamp])

  // 转为传给后端的数据
  const transferParams = (params: any) => {
    const newReplyDetails = []
    for (let i = 0; i < params.reply_details.length; i += 1) {
      const typeObjectKey = typeEnums[params.reply_details[i].content_type]
      const replyDetailItem = {content_type: params.reply_details[i].content_type, id: params.reply_details[i].id || '',quick_reply_content: {}}
      const item = params.reply_details[i]
      if (typeObjectKey === 'text') {
        const quickReplyContent = {
          text: {
            'content': item.text_content
          }
        }
        replyDetailItem.quick_reply_content = quickReplyContent
        newReplyDetails.push(replyDetailItem)
      }
      if (typeObjectKey === 'image') {
        const quickReplyContent = {
          image: {
            'title': item.image_title,
            'size': item.image_size,
            'picurl': item.image_picurl
          }
        }
        replyDetailItem.quick_reply_content = quickReplyContent
        newReplyDetails.push(replyDetailItem)
      }
      if (typeObjectKey === 'link') {
        const quickReplyContent = {
          link: {
            'title': item.link_title,
            'picurl': item.link_picurl,
            'desc': item.link_desc,
            'url': item.link_url
          }
        }
        replyDetailItem.quick_reply_content = quickReplyContent
        newReplyDetails.push(replyDetailItem)
      }
      if (typeObjectKey === 'pdf') {
        const quickReplyContent = {
          pdf: {
            'title': item.pdf_title,
            'size': item.pdf_size,
            'fileurl': item.pdf_fileurl
          }
        }
        replyDetailItem.quick_reply_content = quickReplyContent
        newReplyDetails.push(replyDetailItem)
      }
      if (typeObjectKey === 'video') {
        const quickReplyContent = {
          video: {
            'title': item.video_title,
            'size': item.video_size,
            'picurl': item.video_picurl
          }
        }
        replyDetailItem.quick_reply_content = quickReplyContent
        newReplyDetails.push(replyDetailItem)
      }
    }
    return {...params, reply_details: newReplyDetails, id: props.initialValues?.id,deleted_ids:deletedIds||[]}
  }

  return (
    <div className={styles.scriptModalContainer}>
      <Modal
        {...props}
        width={640}
        className={'dialog from-item-label-100w'}
        visible={props.visible}
        onOk={() => {
          setFileInfoAry([])
          formRef?.current?.submit();
        }}
        onCancel={() => {
          props.setVisible(false)
          formRef?.current?.resetFields()
          props.onCancel()
          setFileInfoAry([])
        }}
      >
        <ProForm
          formRef={formRef}
          layout={'horizontal'}
          submitter={{
            resetButtonProps: {
              style: {
                display: 'none',
              },
            },
            submitButtonProps: {
              style: {
                display: 'none',
              },
            },
          }}
          onFinish={async (values) => {
            const params: any = transferParams(values)
            if (values.is_global === True) {
              params.departments = [0];
            }
            await props.onFinish(params, props.initialValues?.id ? 'update' : 'create');
          }}

        >
          <h3 className="dialog-title" style={{fontSize: 18}}>
            {props.initialValues?.id ? '修改话术' : '新建话术'}
          </h3>

          <ProForm.Item
            name={'group_id'}
            label="分组"
            rules={[{required: true, message: '请选择分组!'}]}
          >
            <Select
              style={{width: 400}}
              placeholder="请选择分组"
              dropdownRender={menu => (
                <div>
                  {menu}
                  <Divider style={{margin: '4px 0'}}/>
                  <div style={{display: 'flex', flexWrap: 'nowrap', padding: 8}}>
                    <a
                      style={{flex: 'none', display: 'block', cursor: 'pointer'}}
                      onClick={() => {
                        groupModalRef.current.open({sub_group: [{name: ''}]})
                      }}
                    >
                      <PlusOutlined/> 新建分组
                    </a>
                  </div>
                </div>
              )}
            >
              {
                groupItems?.map(item => (
                  <Option key={item.id} value={String(item.id)}>
                    <FolderFilled style={{fontSize: '16px', color: '#138af8', marginRight: 8}}/>
                    {item.name}
                  </Option>
                ))
              }
            </Select>
          </ProForm.Item>

          <ProForm.Item name="name" label="标题" rules={[{required: true, message: '请输入标题!'}]} >
            <Input placeholder="仅内部可见,方便整理和查看" style={{width: 400}} maxLength={10}/>
          </ProForm.Item>

          <ProFormList
            name="reply_details"
            creatorButtonProps={{
              type: 'default',
              style: {width: '128px', color: '#58adfc', borderColor: '#58adfc', marginLeft: '20px', marginTop: '-30px'},
              position: 'bottom',
              creatorButtonText: '添加内容',
            }}
            creatorRecord={{
              content_type: 2,
            }}
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            itemRender={({listDom}, {field, record, operation}) => {
              const currentKey = field.name;
              // const boundKey = field.fieldKey;
              return (
                <div className={styles.dynamicFormContainer}>
                  <div>
                    <div className={styles.radioBox}>
                      <ProFormRadio.Group
                        name="content_type"
                        initialValue={'word'}
                        label=""
                        options={[
                          {
                            label: '文字',
                            value: 2,
                          },
                          {
                            label: '图片',
                            value: 3,
                          },
                          {
                            label: '网页',
                            value: 4,
                          },
                          {
                            label: 'PDF',
                            value: 5,
                          },
                          {
                            label: '视频',
                            value: 6,
                          },
                        ]}
                      />
                    </div>
                    <ProForm.Item name="id" label="" style={{display: 'none'}}>
                      <Input />
                    </ProForm.Item>
                    <div className={styles.tabContent}>
                      <ProFormDependency name={['content_type']}>
                        {({content_type}) => {
                          if (content_type === 2) {
                            return (
                              <ProFormTextArea
                                name="text_content"
                                label={'话术内容'}
                                placeholder="请输入话术内容"
                                rules={[{required: true, message: '请输入话术内容!'}]}
                                fieldProps={{showCount: true, maxLength: 1300, allowClear: true}}
                              />
                            );
                          }
                          if (content_type === 3) {
                            return <div>
                              <ProForm.Item name="image_title" label="图片名称"
                                            rules={[{required: true, message: '请输入图片名称!'}]}>
                                <Input placeholder="图片名称可用于搜索" style={{width: 328}}/>
                              </ProForm.Item>

                              <ProForm.Item name="image_size" label="" style={{display: 'none'}}>
                                <Input placeholder="仅内部可见,方便整理和查看"/>
                              </ProForm.Item>

                              <ProForm.Item
                                name='image_picurl'
                                label={'上传图片'}
                              >
                                {/* 上传图片 */}
                                <Uploader
                                  fileType={'formImage'}
                                  customRequest={async (req) => {
                                    try {
                                      const file = req.file as RcFile;
                                      const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
                                      if (getUploadUrlRes.code !== 0) {
                                        message.error('获取上传地址失败');
                                        return;
                                      }
                                      const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
                                        method: 'PUT',
                                        body: file
                                      });
                                      if (uploadRes.clone().ok) {
                                        const reply_details = formRef?.current?.getFieldValue('reply_details');
                                        reply_details[currentKey].image_picurl = getUploadUrlRes?.data?.download_url;
                                        reply_details[currentKey].image_title = file.name;
                                        reply_details[currentKey].image_size = String(file.size);
                                        formRef?.current?.setFieldsValue({...reply_details})
                                        return;
                                      }
                                      message.error('上传图片失败');
                                      return;
                                    } catch (e) {
                                      message.error('上传图片失败');
                                    }
                                  }}
                                />
                              </ProForm.Item>
                            </div>
                          }
                          if (content_type === 4) {// 解析链接
                            return (
                              <div>
                                <ProFormText
                                  name='link_url'
                                  label='链接地址'
                                  placeholder="请输入链接,链接地址以http或https开头"
                                  fieldProps={{
                                    addonAfter: (
                                      <Tooltip title="点击抓取远程链接,自动填充标题,描述,图片">
                                        <div
                                          onClick={async () => {
                                            const res = await ParseURL(formRef?.current?.getFieldValue('reply_details')[currentKey].link_url)
                                            if (res.code !== 0) {
                                              message.error(res.message);
                                            } else {
                                              message.success('解析链接成功');
                                              const reply_details = formRef?.current?.getFieldValue('reply_details');
                                              reply_details[currentKey].link_title = res.data.title;// 链接标题
                                              reply_details[currentKey].link_desc = res.data.desc; // 链接描述
                                              reply_details[currentKey].link_picurl = res.data.img_url; // 图片
                                              formRef?.current?.setFieldsValue({reply_details})
                                            }
                                          }}
                                          style={{
                                            cursor: "pointer",
                                            width: 32,
                                            height: 30,
                                            display: 'flex',
                                            alignItems: 'center',
                                            justifyContent: 'center'
                                          }}>
                                          <SyncOutlined/>
                                        </div>
                                      </Tooltip>
                                    )
                                  }}
                                  rules={[
                                    {
                                      required: true,
                                      message: '请输入链接地址',
                                    },
                                    {
                                      type: 'url',
                                      message: '请填写正确的的URL,必须是http或https开头',
                                    },
                                  ]}
                                />
                                <ProFormText
                                  name='link_title'
                                  label='链接标题'
                                  width='md'
                                  rules={[
                                    {
                                      required: true,
                                      message: '请输入链接标题',
                                    },
                                  ]}
                                />
                                <ProFormTextArea
                                  name='link_desc'
                                  label='链接描述'
                                  width='md'
                                />
                                <ProForm.Item
                                  label='链接封面'
                                  name='link_picurl'
                                  rules={[
                                    {
                                      required: true,
                                      message: '请上传链接图片!',
                                    },
                                  ]}
                                >
                                  <Uploader
                                    fileType={'formImage'}
                                    customRequest={async (req) => {
                                      try {
                                        const file = req.file as RcFile;
                                        const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
                                        if (getUploadUrlRes.code !== 0) {
                                          message.error('获取上传地址失败');
                                          return;
                                        }
                                        const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
                                          method: 'PUT',
                                          body: file
                                        });
                                        if (uploadRes.clone().ok) {
                                          const reply_details = formRef?.current?.getFieldValue('reply_details');
                                          reply_details[currentKey].link_picurl = getUploadUrlRes?.data?.download_url;
                                          formRef?.current?.setFieldsValue({...reply_details})
                                          return;
                                        }
                                        message.error('上传图片失败');
                                        return;
                                      } catch (e) {
                                        message.error('上传图片失败');
                                      }
                                    }}
                                  />
                                </ProForm.Item>
                              </div>
                            )
                          }
                          if (content_type === 5) {
                            return (
                              <div>
                                <ProForm.Item name="pdf_title" label="" style={{display: 'none'}}>
                                  <Input/>
                                </ProForm.Item>
                                <ProForm.Item name="pdf_size" label="" style={{display: 'none'}}>
                                  <Input/>
                                </ProForm.Item>
                                <div className={styles.pdfUploadBox}>
                                  <ProForm.Item name={'pdf_fileurl'}>
                                    <Uploader
                                      fileType='PDF'
                                      fileInfoAry={fileInfoAry}
                                      setFileInfoAry={setFileInfoAry}
                                      currentKey={currentKey}
                                      initialFileInfo={{
                                        // 修改PDF时要回显的文件信息
                                        fileName: formRef?.current?.getFieldValue('reply_details')[currentKey].pdf_title,
                                        fileSize: formRef?.current?.getFieldValue('reply_details')[currentKey].pdf_size,
                                        key: currentKey
                                      }}
                                      customRequest={async (req) => {
                                        try {
                                          const file = req.file as RcFile;
                                          const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
                                          if (getUploadUrlRes.code !== 0) {
                                            message.error('获取上传地址失败');
                                            return;
                                          }
                                          // 上传
                                          const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
                                            method: 'PUT',
                                            body: file
                                          });
                                          // 下载
                                          if (uploadRes.clone().ok) {
                                            const reply_details = formRef?.current?.getFieldValue('reply_details');
                                            reply_details[currentKey].pdf_fileurl = getUploadUrlRes?.data?.download_url;
                                            reply_details[currentKey].pdf_title = file.name;
                                            reply_details[currentKey].pdf_size = String(file.size);
                                            formRef?.current?.setFieldsValue({reply_details})
                                            return;
                                          }
                                          message.error('上传PDF失败');
                                          return;
                                        } catch (e) {
                                          message.error('上传PDF失败');
                                        }
                                      }}
                                    />
                                  </ProForm.Item>
                                </div>
                              </div>
                            );
                          }
                          if (content_type === 6) {
                            return (
                              <>
                                <Row>
                                  <div style={{display: 'none'}}>
                                    <ProFormText name="video_title"/>
                                    <ProFormText name="video_size"/>
                                  </div>
                                  <div className={styles.videoUplaodBox}></div>
                                  <ProForm.Item name={'video_picurl'}>
                                    <Uploader
                                      fileType='视频'
                                      customRequest={async (req) => {
                                        try {
                                          const file = req.file as RcFile;
                                          const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
                                          if (getUploadUrlRes.code !== 0) {
                                            message.error('获取上传地址失败');
                                            return;
                                          }
                                          // 上传
                                          const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
                                            method: 'PUT',
                                            body: file
                                          });
                                          // 下载
                                          if (uploadRes.clone().ok) {
                                            const reply_details = formRef?.current?.getFieldValue('reply_details');
                                            reply_details[currentKey].video_picurl = getUploadUrlRes?.data?.download_url;
                                            reply_details[currentKey].video_title = file.name;
                                            reply_details[currentKey].video_size = String(file.size);
                                            formRef?.current?.setFieldsValue({reply_details})
                                            return;
                                          }
                                          message.error('上传视频失败');
                                          return;
                                        } catch (e) {
                                          message.error('上传视频失败');
                                        }
                                      }}
                                    />
                                  </ProForm.Item>
                                </Row>
                              </>
                            );
                          }
                          return <></>;
                        }}
                      </ProFormDependency>
                    </div>
                  </div>
                  <div>
                    {
                      formRef?.current?.getFieldValue('reply_details').length>1 &&  <Tooltip key={'remove'} title="删除">
                        <DeleteTwoTone
                          style={{paddingTop: 8}}
                          className={'ant-pro-form-list-action-icon'}
                          onClick={() => {
                            const temp = [...fileInfoAry]
                            for (let i = 0; i < temp.length; i += 1) {
                              if (temp[i].key === currentKey) {
                                temp.splice(i, 1)
                              }
                            }
                            setFileInfoAry(temp)
                            if(formRef?.current?.getFieldValue('reply_details')?.[currentKey].id){
                              setDeletedIds([...deletedIds,formRef?.current?.getFieldValue('reply_details')[currentKey].id])
                            }
                            operation.remove(currentKey);
                          }}

                        />
                      </Tooltip>
                    }
                  </div>
                </div>
              )
            }}
          >
          </ProFormList>
        </ProForm>
      </Modal>
      <GroupModal
        allDepartments={props.allDepartments as DepartmentOption[]}
        ref={groupModalRef}
        visible={groupModalVisible}
        setVisible={setGroupModalVisible}
        onFinish={async (values, action) => {
          if (action === 'create') {
            await HandleRequest({...values, sub_groups: values.sub_group || []}, CreateEnterpriseScriptGroups, () => {
              setGroupItemsTimestamp(Date.now)
              groupModalRef.current.close()
            })
          } else {
            await HandleRequest({...values, sub_groups: values.sub_group || []}, UpdateEnterpriseScriptGroups, () => {
              setGroupItemsTimestamp(Date.now)
              groupModalRef.current.close()
            })
          }
        }}
      />
    </div>

  )
}
Example #13
Source File: DepartmentTree.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
DepartMentTreeComp = ({ callback }: { callback: (selectedkey: string) => void }) => {
  const [allDepartments, setAllDepartments] = useState<DepartmentList.Item[]>([]);
  const [departments, setDepartments] = useState<DepartmentOption[]>([]);
  const [departmentNodes, setDepartmentNodes] = useState<TreeNode[]>([]); // 一维的节点
  const [departmentTree, setDepartmentTree] = useState<TreeNode[]>([]); // 多维的树节点
  const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>([]);
  const [keyword] = useState<string>('');
  const [expandAll, setExpandAll] = useState<boolean>(false);
  const [checkedNodeKeys, setCheckedNodeKeys] = useState<string[]>([]);
  const [expandedNodeKeys, setExpandedNodeKeys] = useState<string[]>(['1']);
  const allDepartmentMap = _.keyBy(allDepartments, 'ext_id');

  useEffect(() => {
    QueryDepartmentList({ page_size: 5000 })
      .then((res: any) => {
        if (res?.code === 0 && res?.data && res?.data.items) {
          setAllDepartments(res?.data.items);
          setExpandedNodeKeys(['1']);
        }
      })
      .catch((err) => {
        message.error(err);
      });
  }, []);

  const onNodesCheck = (checked: { checked: string[]; halfChecked: string[] }) => {
    const checkedExtDepartmentIDs: number[] = [];
    let selectedExtDepartmentIDs = selectedDepartments.map((item) => item.ext_id);
    let checkedKeys = [...checked.checked];

    // 找出本次uncheck的key,根据这些key的ext_id去删除相关checkedKey
    const uncheckedKeys = _.difference(checkedNodeKeys, checkedKeys);
    _.forEach<string>(uncheckedKeys, (key: string) => {
      // @ts-ignore
      checkedKeys = checkedKeys.filter<string>((checkedKey) => {
        return !checkedKey.includes(key);
      });
    });

    // 记录当前所有checkedkey
    checkedKeys.forEach((key) => {
      checkedExtDepartmentIDs.push(Number(key));
      selectedExtDepartmentIDs.push(Number(key));
    });

    // 计算需要删除的extDepartmentID
    // @ts-ignore
    const shouldDeleteExtDepartmentIDs = _.difference(
      _.map(departments, 'ext_id'),
      checkedExtDepartmentIDs,
    );
    selectedExtDepartmentIDs = _.difference(
      _.uniq(selectedExtDepartmentIDs),
      _.uniq(shouldDeleteExtDepartmentIDs),
    );

    const items = selectedExtDepartmentIDs.map((selectedExtDepartmentID) => {
      return allDepartmentMap[selectedExtDepartmentID];
    });

    // @ts-ignore
    setSelectedDepartments(items);
  };

  const nodeRender = (node: DataNode): ReactNode => {
    return (
      <div
        onClick={() => {
          // eslint-disable-next-line @typescript-eslint/no-unused-expressions
          callback && callback(String(node.key))
        }}
        style={{ padding: '4px 6px', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow:'ellipsis'}}
      >
        <FolderFilled
          style={{
            color: '#47a7ff',
            fontSize: 20,
            marginRight: 6,
            verticalAlign: -6,
          }}
        />
        <span>
          {/* node.title */}
          {
            // @ts-ignore
            node.title.length>14? <span>{node.title.slice(0,13)}...</span>:<span>{node.title}</span>
          }
          ({node.staff_num})
        </span>
      </div>
    );
  };
  // 监听选中部门变化,计算checked的树节点
  useEffect(() => {
    const allDepartmentNodeKeys = _.map(departmentNodes, 'key');
    // 计算当前选中的部门,命中的key
    const matchedKeys: string[] = [];
    allDepartmentNodeKeys.forEach((key: string) => {
      selectedDepartments.forEach((department) => {
        if (key === `${department.ext_id}`) {
          matchedKeys.push(key);
        }
      });
    });
    setCheckedNodeKeys(matchedKeys);
  }, [selectedDepartments]);

  // 关键词变化的时候
  useEffect(() => {
    const filteredDepartments = allDepartments.filter((item) => {
      return keyword === '' || item.label.includes(keyword);
    });
    // @ts-ignore
    setDepartments(filteredDepartments);
    const { nodes, tree } = buildDepartmentTree(filteredDepartments);
    // 这里同步更新node节点和选中keylet checkedKeys: string[] = [];
    nodes.forEach((node) => {
      selectedDepartments.forEach((department) => {
        if (node.key === `${department.ext_id}`) {
          checkedKeys.push(node.key);
        }
      });
    });
    checkedKeys = _.uniq<string>(checkedKeys);
    setCheckedNodeKeys(checkedKeys);
    setDepartmentNodes(nodes);
    setDepartmentTree(tree);
  }, [allDepartments, keyword]);

  return (
    <div >
      <div className={styles.header}>
        <span className={styles.departmentTitle}>部门信息</span>
        <a
          type={'link'}
          onClick={() => {
            const currentStatus = !expandAll;
            if (currentStatus) {
              setExpandedNodeKeys(_.map(departmentNodes, 'key'));
            } else {
              setExpandedNodeKeys(['0']);
            }
            setExpandAll(currentStatus);
          }}
        >
          {!expandAll ? '展开全部' : '收起全部'}
        </a>
      </div>
      <div className={styles.treeContainer}>
        <Tree
          autoExpandParent={false}
          checkStrictly={true}
          checkedKeys={checkedNodeKeys}
          expandedKeys={expandedNodeKeys}
          // @ts-ignore
          onExpand={(expandedKeys: string[]) => {
            setExpandedNodeKeys(expandedKeys);
          }}
          height={300}
          switcherIcon={<CaretDownFilled style={{ color: '#47a7ff' }} />}
          multiple={true}
          treeData={departmentTree}
          // @ts-ignore
          onCheck={onNodesCheck}
          titleRender={nodeRender}
        />
      </div>
    </div>
  );
}
Example #14
Source File: index.tsx    From datart with Apache License 2.0 4 votes vote down vote up
Sidebar = memo(
  ({ isDragging, width, sliderVisible, handleSliderVisible }: SidebarProps) => {
    const history = useHistory();
    const dispatch = useDispatch();
    const { showSaveForm } = useContext(SaveFormContext);
    const orgId = useSelector(selectOrgId);
    const selectViewTree = useMemo(makeSelectViewTree, []);
    const viewsData = useSelector(selectViews);
    const t = useI18NPrefix('view.sidebar');

    const getIcon = useCallback(
      ({ isFolder }: ViewSimpleViewModel) =>
        isFolder ? (
          p => (p.expanded ? <FolderOpenFilled /> : <FolderFilled />)
        ) : (
          <CodeFilled />
        ),
      [],
    );
    const getDisabled = useCallback(
      ({ deleteLoading }: ViewSimpleViewModel) => deleteLoading,
      [],
    );

    const treeData = useSelector(state =>
      selectViewTree(state, { getIcon, getDisabled }),
    );

    const { filteredData: filteredTreeData, debouncedSearch: treeSearch } =
      useDebouncedSearch(treeData, (keywords, d) =>
        d.title.toLowerCase().includes(keywords.toLowerCase()),
      );
    const archived = useSelector(selectArchived);
    const recycleList = useMemo(
      () =>
        archived?.map(({ id, name, parentId, isFolder, deleteLoading }) => ({
          key: id,
          title: name,
          parentId,
          icon: isFolder ? <FolderOutlined /> : <FileOutlined />,
          isFolder,
          disabled: deleteLoading,
        })),
      [archived],
    );
    const { filteredData: filteredListData, debouncedSearch: listSearch } =
      useDebouncedSearch(recycleList, (keywords, d) =>
        d.title.toLowerCase().includes(keywords.toLowerCase()),
      );

    const add = useCallback(
      ({ key }) => {
        switch (key) {
          case 'view':
            history.push(
              `/organizations/${orgId}/views/${`${UNPERSISTED_ID_PREFIX}${uuidv4()}`}`,
            );
            break;
          case 'folder':
            showSaveForm({
              type: CommonFormTypes.Add,
              visible: true,
              simple: true,
              parentIdLabel: t('parent'),
              onSave: (values, onClose) => {
                let index = getInsertedNodeIndex(values, viewsData);

                dispatch(
                  saveFolder({
                    folder: {
                      ...values,
                      parentId: values.parentId || null,
                      index,
                    },
                    resolve: onClose,
                  }),
                );
              },
            });
            break;
          default:
            break;
        }
      },
      [dispatch, history, orgId, showSaveForm, viewsData, t],
    );

    const titles = useMemo(
      () => [
        {
          key: 'list',
          title: t('title'),
          search: true,
          add: {
            items: [
              { key: 'view', text: t('addView') },
              { key: 'folder', text: t('addFolder') },
            ],
            callback: add,
          },
          more: {
            items: [
              {
                key: 'recycle',
                text: t('recycle'),
                prefix: <DeleteOutlined className="icon" />,
              },
              {
                key: 'collapse',
                text: t(sliderVisible ? 'open' : 'close'),
                prefix: sliderVisible ? (
                  <MenuUnfoldOutlined className="icon" />
                ) : (
                  <MenuFoldOutlined className="icon" />
                ),
              },
            ],
            callback: (key, _, onNext) => {
              switch (key) {
                case 'recycle':
                  onNext();
                  break;
                case 'collapse':
                  handleSliderVisible(!sliderVisible);
                  break;
              }
            },
          },
          onSearch: treeSearch,
        },
        {
          key: 'recycle',
          title: t('recycle'),
          back: true,
          search: true,
          onSearch: listSearch,
        },
      ],
      [add, treeSearch, listSearch, t, handleSliderVisible, sliderVisible],
    );

    return (
      <Wrapper
        sliderVisible={sliderVisible}
        className={sliderVisible ? 'close' : ''}
        isDragging={isDragging}
        width={width}
      >
        {sliderVisible ? (
          <MenuUnfoldOutlined className="menuUnfoldOutlined" />
        ) : (
          <></>
        )}
        <ListNavWrapper defaultActiveKey="list">
          <ListPane key="list">
            <ListTitle {...titles[0]} />
            <FolderTree treeData={filteredTreeData} />
          </ListPane>
          <ListPane key="recycle">
            <ListTitle {...titles[1]} />
            <Recycle list={filteredListData} />
          </ListPane>
        </ListNavWrapper>
      </Wrapper>
    );
  },
)