You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 12 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /*
  2. * @Author: 赵伟
  3. * @Date: 2024-04-11 16:31:18
  4. * @Description: 选择数据集、模型、镜像
  5. */
  6. import KFIcon from '@/components/KFIcon';
  7. import KFModal from '@/components/KFModal';
  8. import { CommonTabKeys } from '@/enums';
  9. import { ResourceFileData } from '@/pages/Dataset/config';
  10. import { type MirrorVersionData } from '@/pages/Mirror/Info';
  11. import { to } from '@/utils/promise';
  12. import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd';
  13. import { Input, Tabs, Tree } from 'antd';
  14. import React, { useEffect, useMemo, useRef, useState } from 'react';
  15. import { ResourceSelectorType, selectorTypeConfig } from './config';
  16. import styles from './index.less';
  17. export { ResourceSelectorType, selectorTypeConfig };
  18. // 选择数据集、模型、镜像的返回类型
  19. export type ResourceSelectorResponse = {
  20. activeTab: CommonTabKeys; // 是我的还是公开的
  21. id: string; // 数据集\模型 id
  22. name: string; // 数据集\模型 name
  23. identifier: string; // 数据集\模型 identifier
  24. owner: string; // 数据集\模型 owner
  25. version: string; // 数据集\模型 version
  26. path: string; // 数据集\模型 版本路径
  27. } & MirrorVersionData;
  28. export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
  29. /** 类型,数据集、模型、镜像 */
  30. type: ResourceSelectorType;
  31. /** 默认展开的节点 */
  32. defaultExpandedKeys?: React.Key[];
  33. /** 默认展开的节点 */
  34. defaultCheckedKeys?: React.Key[];
  35. /** 默认激活的 Tab */
  36. defaultActiveTab?: CommonTabKeys;
  37. /**
  38. * 确认回调
  39. * @param params 选择的数据
  40. */
  41. onOk?: (params: ResourceSelectorResponse | undefined) => void;
  42. }
  43. type TreeRef = GetRef<typeof Tree<TreeDataNode>>;
  44. // 更新树形结构的 children
  45. const updateChildren = (parentId: string, children: TreeDataNode[]) => {
  46. return (node: TreeDataNode) => {
  47. if (node.key === parentId) {
  48. return {
  49. ...node,
  50. children,
  51. };
  52. }
  53. return node;
  54. };
  55. };
  56. // 得到数据集\模型\镜像 id 和下属版本号
  57. const getIdAndVersion = (versionKey: string) => {
  58. const index = versionKey.indexOf('-');
  59. const id = versionKey.slice(0, index);
  60. const version = versionKey.slice(index + 1);
  61. return {
  62. id,
  63. version,
  64. };
  65. };
  66. /** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */
  67. function ResourceSelectorModal({
  68. type,
  69. defaultExpandedKeys = [],
  70. defaultCheckedKeys = [],
  71. defaultActiveTab = CommonTabKeys.Private,
  72. onOk,
  73. ...rest
  74. }: ResourceSelectorModalProps) {
  75. const [activeTab, setActiveTab] = useState<CommonTabKeys>(defaultActiveTab);
  76. const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
  77. const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
  78. const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
  79. const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]);
  80. const [files, setFiles] = useState<ResourceFileData[]>([]);
  81. const [versionDesc, setVersionDesc] = useState<string | undefined>(undefined);
  82. const [versionPath, setVersionPath] = useState('');
  83. const [searchText, setSearchText] = useState('');
  84. const [firstLoadList, setFirstLoadList] = useState(false);
  85. const [firstLoadVersions, setFirstLoadVersions] = useState(false);
  86. const treeRef = useRef<TreeRef>(null);
  87. const config = selectorTypeConfig[type];
  88. // 搜索
  89. const treeData = useMemo(
  90. () =>
  91. originTreeData.filter((v) =>
  92. (v.title as string).toLowerCase()?.includes(searchText.toLowerCase()),
  93. ),
  94. [originTreeData, searchText],
  95. );
  96. useEffect(() => {
  97. // 获取数据集\模型\镜像列表
  98. const getTreeData = async () => {
  99. const isPublic = activeTab === CommonTabKeys.Private ? false : true;
  100. const [res] = await to(config.getList(isPublic));
  101. if (res) {
  102. setOriginTreeData(res);
  103. // 恢复上一次的 Expand 操作
  104. setFirstLoadList(true);
  105. } else {
  106. setOriginTreeData([]);
  107. }
  108. };
  109. setExpandedKeys([]);
  110. setCheckedKeys([]);
  111. setLoadedKeys([]);
  112. setFiles([]);
  113. setVersionDesc(undefined);
  114. setVersionPath('');
  115. setSearchText('');
  116. getTreeData();
  117. }, [activeTab, config]);
  118. useEffect(() => {
  119. // 恢复上一次的 Expand 操作
  120. // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys
  121. // fisrtLoadList 标志位
  122. const restoreLastExpand = () => {
  123. if (firstLoadList && Array.isArray(defaultExpandedKeys) && defaultExpandedKeys.length > 0) {
  124. setExpandedKeys(defaultExpandedKeys);
  125. // 延时滑动到 defaultExpandedKeys,不然不会加载 defaultExpandedKeys,不然不会加载版本
  126. setTimeout(() => {
  127. treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' });
  128. }, 100);
  129. }
  130. };
  131. restoreLastExpand();
  132. }, [firstLoadList, defaultExpandedKeys]);
  133. // 获取数据集\模型\镜像版本列表
  134. const getVersions = async (parentId: string, parentNode: any) => {
  135. const [res, error] = await to(config.getVersions(parentId, parentNode));
  136. if (res) {
  137. // 更新 treeData children
  138. setOriginTreeData((prev) => prev.map(updateChildren(parentId, res)));
  139. // 缓存 loadedKeys
  140. const index = loadedKeys.find((v) => v === parentId);
  141. if (!index) {
  142. setLoadedKeys((prev) => prev.concat(parentId));
  143. }
  144. // 恢复上一次的 Check 操作,需要延时以便 TreeData 更新完
  145. setTimeout(() => {
  146. restoreLastCheck(parentId, res);
  147. }, 100);
  148. } else {
  149. setExpandedKeys([]);
  150. return Promise.reject(error);
  151. }
  152. };
  153. // 获取版本下的文件
  154. const getFiles = async (parentId: string, parentNode: any) => {
  155. const [res] = await to(config.getFiles(parentId, parentNode));
  156. if (res) {
  157. setVersionPath(res.path);
  158. setFiles(res.content);
  159. setVersionDesc(res.versionDesc);
  160. } else {
  161. setVersionPath('');
  162. setFiles([]);
  163. setVersionDesc(undefined);
  164. }
  165. };
  166. // 展开时,动态加载 tree children
  167. const onLoadData = ({ key, children, ...rest }: TreeDataNode) => {
  168. if (children) {
  169. return Promise.resolve();
  170. } else {
  171. return getVersions(key as string, rest);
  172. }
  173. };
  174. // 扩展
  175. const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => {
  176. const lastKeys = expandedKeysValue.slice(-1);
  177. setExpandedKeys(lastKeys);
  178. };
  179. // 选中
  180. const onCheck: TreeProps['onCheck'] = (checkedKeysValue, { checkedNodes }) => {
  181. const lastKeys = (checkedKeysValue as React.Key[]).slice(-1);
  182. setCheckedKeys(lastKeys);
  183. if (lastKeys.length && checkedNodes.length) {
  184. const last = lastKeys[0] as string;
  185. const lastNode = checkedNodes[checkedNodes.length - 1];
  186. getFiles(last, lastNode);
  187. } else {
  188. setVersionPath('');
  189. setFiles([]);
  190. setVersionDesc(undefined);
  191. }
  192. };
  193. // 恢复上一次的 Check 操作
  194. // 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口
  195. // fisrtLoadVersions 标志位
  196. const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => {
  197. if (!firstLoadVersions && Array.isArray(defaultCheckedKeys) && defaultCheckedKeys.length > 0) {
  198. const last = defaultCheckedKeys[0] as string;
  199. const { id } = getIdAndVersion(last);
  200. // 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致
  201. if (id === parentId) {
  202. setCheckedKeys(defaultCheckedKeys);
  203. const parentNode = versions.find((v) => v.key === last);
  204. getFiles(last, parentNode);
  205. setFirstLoadVersions(true);
  206. setTimeout(() => {
  207. treeRef?.current?.scrollTo({
  208. key: defaultCheckedKeys[0],
  209. align: 'bottom',
  210. });
  211. }, 100);
  212. }
  213. }
  214. };
  215. // 提交
  216. const handleOk = () => {
  217. if (checkedKeys.length > 0) {
  218. const last = checkedKeys[0] as string;
  219. const { id, version } = getIdAndVersion(last);
  220. const treeNode = treeData.find((v) => v.key === id) as any;
  221. const name = (treeNode?.title ?? '') as string;
  222. const identifier = (treeNode?.identifier ?? '') as string;
  223. const owner = (treeNode?.owner ?? '') as string;
  224. const childNode = treeNode.children.filter((v: TreeDataNode) => v.key === last)[0];
  225. const res =
  226. type === ResourceSelectorType.Mirror
  227. ? {
  228. activeTab: activeTab,
  229. ...childNode,
  230. }
  231. : {
  232. activeTab: activeTab,
  233. id,
  234. name,
  235. path: versionPath,
  236. version,
  237. identifier,
  238. owner,
  239. };
  240. onOk?.(res);
  241. } else {
  242. onOk?.(undefined);
  243. }
  244. };
  245. const title = `选择${config.name}`;
  246. const palceholder = `请输入${config.name}名称`;
  247. const fileLen = files.length > 0 ? `(${files.length})` : '';
  248. const fileTitle =
  249. type === ResourceSelectorType.Mirror ? '镜像地址' : `${config.name}版本文件${fileLen}`;
  250. const tabItems = config.tabItems;
  251. const titleImg = config.modalIcon;
  252. return (
  253. <KFModal {...rest} title={title} image={titleImg} onOk={handleOk} width={920} destroyOnClose>
  254. <div>
  255. <Tabs
  256. activeKey={activeTab}
  257. items={tabItems}
  258. onChange={(e) => setActiveTab(e as CommonTabKeys)}
  259. className={styles['model-tabs']}
  260. />
  261. <div className={styles['model-selector']}>
  262. <div className={styles['model-selector__left']}>
  263. <Input
  264. className={styles['model-selector__left__search']}
  265. placeholder={palceholder}
  266. allowClear
  267. variant="borderless"
  268. value={searchText}
  269. onChange={(e) => setSearchText(e.target.value)}
  270. prefix={
  271. <KFIcon
  272. type="icon-sousuo"
  273. color="rgba(22,100,255,0.4)"
  274. style={{ height: '15px' }}
  275. />
  276. }
  277. // prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />}
  278. />
  279. <Tree
  280. ref={treeRef}
  281. rootStyle={{ backgroundColor: 'transparent' }}
  282. loadData={onLoadData}
  283. treeData={treeData}
  284. onCheck={onCheck}
  285. checkedKeys={checkedKeys}
  286. multiple={false}
  287. selectable={false}
  288. height={324}
  289. loadedKeys={loadedKeys}
  290. expandedKeys={expandedKeys}
  291. onExpand={onExpand}
  292. checkable
  293. titleRender={(nodeData) => {
  294. return (
  295. <span
  296. className={styles['model-selector__left__tree-title']}
  297. style={{ width: nodeData.isLeaf ? '370px' : '420px' }}
  298. >
  299. {nodeData.title as string}
  300. </span>
  301. );
  302. }}
  303. />
  304. </div>
  305. <div className={styles['model-selector__right']}>
  306. <div style={{ height: '50%' }}>
  307. <div className={styles['model-selector__right__title']}>{fileTitle}</div>
  308. <div className={styles['model-selector__right__files']}>
  309. {files.map((v) => (
  310. <div key={v.url} className={styles['model-selector__right__files__file']}>
  311. {v.file_name}
  312. </div>
  313. ))}
  314. </div>
  315. </div>
  316. {versionDesc && (
  317. <div style={{ height: '50%' }}>
  318. <div
  319. className={styles['model-selector__right__title']}
  320. >{`${config.name}版本描述`}</div>
  321. <div className={styles['model-selector__right__desc']}>{versionDesc}</div>
  322. </div>
  323. )}
  324. </div>
  325. </div>
  326. </div>
  327. </KFModal>
  328. );
  329. }
  330. export default ResourceSelectorModal;