@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 |
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 |
FolderIcon = ({ expanded }: { expanded: boolean }) => {
return expanded ? <FolderOpenFilled /> : <FolderFilled />
}
Example #3
Source File: index.tsx From dashboard with Apache License 2.0 | 5 votes |
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 |
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 |
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 |
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 |
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> 项
<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 |
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 |
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 |
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}
</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}>
可见范围 <Tooltip title={'范围内的员工可在企业微信【侧边栏】使用话术'}><QuestionCircleOutlined /></Tooltip>:
{allGroupMap[targetScriptGroup.id]?.departments ? getUseableDepartment(allGroupMap[targetScriptGroup.id]?.departments) : '全部员工可见'}
<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> 项
</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 |
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 |
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 |
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);
});
});
// 记录当前所有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];
});
// @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节点和选中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);
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 |
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>
);
},
)