| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
9ce68b8813 | merge | 10 months ago |
|
|
67861012ee | . | 11 months ago |
|
|
5f478f8214 | merge | 11 months ago |
| @@ -0,0 +1 @@ | |||
| v16.11.0 | |||
| @@ -20,14 +20,20 @@ export default { | |||
| // localhost:8000/api/** -> https://preview.pro.ant.design/api/** | |||
| '/api/': { | |||
| // 要代理的地址 | |||
| target: 'http://172.20.32.181:31213', // 开发环境 | |||
| target: 'http://172.20.32.114:8082/', // 开发环境 | |||
| // target: 'http://172.20.32.197:31213', // 开发环境 | |||
| // target: 'http://172.20.32.98:8082', | |||
| // target: 'http://172.20.32.150:8082', | |||
| // 配置了这个可以从 http 代理到 https | |||
| // 依赖 origin 的功能可能需要这个,比如 cookie | |||
| changeOrigin: true, | |||
| // pathRewrite: { '^/api': '' }, | |||
| pathRewrite: { '^/api': '' }, | |||
| }, | |||
| // '/cp/': { | |||
| // target: 'http://172.20.32.14:9213', // 开发环境 | |||
| // changeOrigin: true, | |||
| // pathRewrite: { '^/cp': '' }, | |||
| // }, | |||
| '/profile/avatar/': { | |||
| target: 'http://172.20.32.185:31213', | |||
| changeOrigin: true, | |||
| @@ -44,7 +44,7 @@ export default [ | |||
| { | |||
| name: 'login', | |||
| path: '/user/login', | |||
| component: './User/Login', | |||
| component: './User/Login/login', | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -147,6 +147,42 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '知识图谱', | |||
| path: '/knowledge', | |||
| routes: [ | |||
| { | |||
| name: '我的图谱', | |||
| path: 'myGraph', | |||
| component: './knowledge/MyGraph/index', | |||
| }, | |||
| { | |||
| name: '我的主体', | |||
| path: 'mySubject', | |||
| component: './knowledge/MySubject/index', | |||
| }, | |||
| { | |||
| name: '在线版本图谱', | |||
| path: 'onlineGraph', | |||
| component: './knowledge/OnlineGraph/index', | |||
| }, | |||
| { | |||
| name: '预览主体', | |||
| path: 'subjectView/:name/:id', | |||
| component: './knowledge/SubjectView/index', | |||
| }, | |||
| { | |||
| name: '预览图谱', | |||
| path: 'graphView', | |||
| component: './knowledge/GraphView/index.tsx', | |||
| }, | |||
| { | |||
| name: '图谱配置', | |||
| path: 'graphCounter', | |||
| component: './knowledge/GraphCounter/index', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: 'AI资产', | |||
| path: '/dataset', | |||
| @@ -0,0 +1,20 @@ | |||
| import { defineMock } from 'umi'; | |||
| export default defineMock({ | |||
| 'GET /api/kg/configuration/list': { | |||
| code: 200, | |||
| msg: '操作成功', | |||
| data: [ | |||
| { | |||
| id: '000000001', | |||
| description:"是个好东西", | |||
| name: '你收到了 14 份新周报', | |||
| updateTime: '2017-08-09', | |||
| effectiveTime: '2017-08-09', | |||
| type: 'notification', | |||
| } | |||
| ], | |||
| }, | |||
| }); | |||
| @@ -0,0 +1,183 @@ | |||
| .graphCounter-page { | |||
| :global { | |||
| .ant-tabs .ant-tabs-tab+.ant-tabs-tab { | |||
| margin-left: 54px; | |||
| } | |||
| } | |||
| height: 100%; | |||
| padding: 0 9px; | |||
| &__container { | |||
| height: 91px; | |||
| padding:14px 30px 0 30px; | |||
| background-image: url(@/assets/img/page-title-bg.png); | |||
| background-repeat: no-repeat; | |||
| background-position: top center; | |||
| background-size: 100% 100%; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| &__box{ | |||
| display: flex; | |||
| align-items: center; | |||
| color:#1d1d20; | |||
| font-size:15px; | |||
| &__update{ | |||
| padding: 7px 10px; | |||
| background:rgba(22, 100, 255, 0.06); | |||
| border:1px solid rgba(22, 100, 255, 0.11); | |||
| border-radius:4px; | |||
| margin-right: 20px; | |||
| } | |||
| } | |||
| } | |||
| &__topCenter{ | |||
| position: relative; | |||
| margin: 10px 0 15px 0; | |||
| width: 100%; | |||
| height: calc(100% - 100px); | |||
| background:#ffffff; | |||
| border-radius:10px; | |||
| box-shadow:0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| } | |||
| &__title{ | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| margin: 30px 0 15px 25px; | |||
| font-weight: 600; | |||
| } | |||
| &__listBox{ | |||
| padding: 0 25px; | |||
| display: flex; | |||
| align-items: center; | |||
| &__itemBox{ | |||
| position: relative; | |||
| width: calc(20% - 15px); | |||
| height:100px; | |||
| background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%); | |||
| border-radius:4px; | |||
| box-shadow:0px 3px 10px rgba(164, 169, 181, 0.12); | |||
| margin-right: 15px; | |||
| &__img{ | |||
| position: absolute; | |||
| left: 30px; | |||
| top: 26px; | |||
| width: 48px; | |||
| height: 48px; | |||
| } | |||
| &__wordTitle{ | |||
| position: absolute; | |||
| top: 26px; | |||
| left: 102px; | |||
| font-weight:600; | |||
| color:#1d1d20; | |||
| font-size:18px; | |||
| } | |||
| &__wordContent{ | |||
| position: absolute; | |||
| top: 54px; | |||
| left: 102px; | |||
| color:#565658; | |||
| font-size:15px; | |||
| } | |||
| } | |||
| } | |||
| &__bottomBox{ | |||
| padding: 23px 35px 23px 25px; | |||
| height: 100%; | |||
| &__leftText{ | |||
| float: left; | |||
| display: flex; | |||
| align-items: start; | |||
| margin-bottom: 25px; | |||
| &__text{ | |||
| width:1430px; | |||
| min-height:70px; | |||
| padding: 25px; | |||
| background:rgba(96, 107, 122, 0.06); | |||
| border:1px solid; | |||
| border-color:rgba(96, 107, 122, 0.2); | |||
| border-radius:0px 10px 10px 10px; | |||
| color:rgba(29, 29, 32, 0.75); | |||
| font-size:15px; | |||
| } | |||
| } | |||
| &__rightText{ | |||
| float: right; | |||
| display: flex; | |||
| align-items: start; | |||
| margin-bottom: 25px; | |||
| &__text{ | |||
| width:791px; | |||
| min-height:70px; | |||
| padding: 25px; | |||
| background:#1664ff; | |||
| border:1px solid; | |||
| border-color:rgba(22, 100, 255, 0.3); | |||
| border-radius:10px 0px 10px 10px; | |||
| color:#fff; | |||
| font-size:15px; | |||
| } | |||
| } | |||
| &__titleBox{ | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 39px; | |||
| &__left{ | |||
| display: flex; | |||
| align-items: center; | |||
| font-weight:600; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| &__word{ | |||
| display: flex; | |||
| align-items: center; | |||
| width:206px; | |||
| height:27px; | |||
| margin-left: 14px; | |||
| background:rgba(22, 100, 255, 0.09); | |||
| border-radius:4px; | |||
| padding: 2px 20px 2px 16px; | |||
| color:#4c5360; | |||
| font-size:14px; | |||
| font-weight: normal; | |||
| &__circle{ | |||
| position: relative; | |||
| width:15px; | |||
| height:15px; | |||
| border-radius: 50%; | |||
| background:#ffffff; | |||
| box-shadow:0px 1px 3px rgba(0, 82, 217, 0.09); | |||
| margin-right: 13px; | |||
| &__inner{ | |||
| position: absolute; | |||
| left: 50%; | |||
| top: 50%; | |||
| transform: translate(-50%,-50%); | |||
| width:9px; | |||
| height:9px; | |||
| border-radius: 50%; | |||
| background:#1664ff; | |||
| box-shadow:0px 1px 3px rgba(22, 100, 255, 0.27); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__right{ | |||
| display: flex; | |||
| align-items: center; | |||
| // width:647px; | |||
| height:39px; | |||
| background:rgba(138, 138, 138, 0.05); | |||
| border-radius:4px; | |||
| padding-left: 30px; | |||
| } | |||
| } | |||
| &__queryparams{ | |||
| margin: 17px 0; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,413 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { getConfigurationList } from '@/services/graph'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { Button, Input, Table, type TablePaginationConfig, type TableProps, Tabs } from 'antd'; | |||
| // import getConfigurationList from 'mock/knowledge'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; | |||
| import styles from './index.less'; | |||
| export type CodeConfigData = { | |||
| id: number; | |||
| code_repo_name: string; | |||
| code_repo_vis: number; | |||
| git_url: string; | |||
| git_branch: string; | |||
| git_user_name: string; | |||
| git_password: string; | |||
| ssh_key: string; | |||
| verify_mode: number; | |||
| create_by: string; | |||
| create_time: string; | |||
| update_by: string; | |||
| update_time: string; | |||
| }; | |||
| export type EditorData = { | |||
| id: number; | |||
| name: string; | |||
| status: string; | |||
| computing_resource: string; | |||
| update_by: string; | |||
| create_time: string; | |||
| url: string; | |||
| }; | |||
| function DatasetAnnotation() { | |||
| const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined); | |||
| const [tableData, setTableData] = useState<EditorData[]>([]); | |||
| const [cacheState, setCacheState] = useCacheState(); | |||
| const [searchParams] = useSearchParams(); | |||
| const tabActive = searchParams.get('tabActive') || ''; | |||
| const [activeTab, setActiveTab] = useState<string>('1'); | |||
| const navigate = useNavigate(); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| const handleTableChange: TableProps<EditorData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| }; | |||
| const [total, setTotal] = useState(0); | |||
| useEffect(() => { | |||
| getList(); | |||
| setActiveTab(tabActive); | |||
| }, []); | |||
| const items = [ | |||
| { | |||
| key: '1', | |||
| label: `元素链接配置`, | |||
| icon: <KFIcon type="icon-moxingjianjie" />, | |||
| children: null, | |||
| }, | |||
| { | |||
| key: '2', | |||
| label: `问答模板配置`, | |||
| icon: <KFIcon type="icon-moxingwenjian" />, | |||
| children: null, | |||
| }, | |||
| { | |||
| key: '3', | |||
| label: `问答体验`, | |||
| icon: <KFIcon type="icon-moxingwenjian" />, | |||
| children: null, | |||
| }, | |||
| ]; | |||
| const createCodeConfig = () => { | |||
| const { close } = openAntdModal(AddCodeConfigModal, { | |||
| opType: OperationType.Create, | |||
| onOk: () => { | |||
| // getDataList(); | |||
| // close(); | |||
| }, | |||
| }); | |||
| }; | |||
| const handleSubjectDelete = () => { | |||
| modalConfirm({ | |||
| title: '确定要删除XXXX吗?', | |||
| content: '删除后本体将不可恢复,是否确认删除?', | |||
| onOk: () => {}, | |||
| }); | |||
| }; | |||
| const getList = () => { | |||
| let params = {}; | |||
| getConfigurationList(params).then((ret) => { | |||
| setTableData(ret.data); | |||
| }); | |||
| }; | |||
| const columnsNow: TableProps<EditorData>['columns'] = [ | |||
| { | |||
| title: '元素链接配置名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '元素链接配置描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '更新时间', | |||
| dataIndex: 'updateTime', | |||
| key: 'updateTime', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '生效时间', | |||
| dataIndex: 'effectiveTime', | |||
| key: 'effectiveTime', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 300, | |||
| key: 'operation', | |||
| render: (_: any, record: EditorData) => ( | |||
| <div> | |||
| <Button type="link" size="small" key="stop" icon={<KFIcon type="icon-tingzhi" />}> | |||
| 查看 | |||
| </Button> | |||
| <Button type="link" size="small" key="stop" icon={<KFIcon type="icon-tingzhi" />}> | |||
| 编辑 | |||
| </Button> | |||
| <Button type="link" size="small" key="stop" icon={<KFIcon type="icon-tingzhi" />}> | |||
| 删除 | |||
| </Button> | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| const columnsFu: TableProps<EditorData>['columns'] = [ | |||
| { | |||
| title: '运行编号', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| }, | |||
| { | |||
| title: '事件类型', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: '10%', | |||
| }, | |||
| { | |||
| title: '本体', | |||
| dataIndex: 'computing_resource', | |||
| key: 'computing_resource', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'update_by', | |||
| key: 'update_by', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '开始时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: '20%', | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '结束时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: '20%', | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 300, | |||
| key: 'operation', | |||
| // render: (_: any, record: EditorData) => ( | |||
| // <div> | |||
| // {record.status === DevEditorStatus.Pending || | |||
| // record.status === DevEditorStatus.Running ? ( | |||
| // <Button | |||
| // type="link" | |||
| // size="small" | |||
| // key="stop" | |||
| // icon={<KFIcon type="icon-tingzhi" />} | |||
| // onClick={() => stopEditor(record.id)} | |||
| // > | |||
| // 停止 | |||
| // </Button> | |||
| // ) : ( | |||
| // <Button | |||
| // type="link" | |||
| // size="small" | |||
| // key="debug" | |||
| // icon={<KFIcon type="icon-tiaoshi" />} | |||
| // onClick={() => startEditor(record.id)} | |||
| // > | |||
| // 启动 | |||
| // </Button> | |||
| // )} | |||
| // {record.status === DevEditorStatus.Running ? ( | |||
| // <Button | |||
| // type="link" | |||
| // size="small" | |||
| // key="jingxiang" | |||
| // icon={<KFIcon type="icon-jingxiang" />} | |||
| // onClick={() => createMirror(record.id)} | |||
| // > | |||
| // 制作镜像 | |||
| // </Button> | |||
| // ) : null} | |||
| // <ConfigProvider | |||
| // theme={{ | |||
| // token: { | |||
| // colorLink: themes['warningColor'], | |||
| // }, | |||
| // }} | |||
| // > | |||
| // <Button | |||
| // type="link" | |||
| // size="small" | |||
| // key="remove" | |||
| // icon={<KFIcon type="icon-shanchu" />} | |||
| // onClick={() => handleEditorDelete(record)} | |||
| // > | |||
| // 删除 | |||
| // </Button> | |||
| // </ConfigProvider> | |||
| // </div> | |||
| // ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['graphCounter-page']}> | |||
| <div className={styles['graphCounter-page__container']}> | |||
| <span>552815033</span> | |||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||
| </div> | |||
| <div className={styles['graphCounter-page__topCenter']}> | |||
| <div className={styles['graphCounter-page__bottomBox']}> | |||
| {activeTab == '1' && ( | |||
| <div> | |||
| <div className={styles['graphCounter-page__bottomBox__queryparams']}> | |||
| <Input.Search | |||
| placeholder="按镜像名称筛选" | |||
| allowClear | |||
| // onSearch={onSearch} | |||
| // onChange={(e) => setInputText(e.target.value)} | |||
| style={{ width: 300 }} | |||
| // value={inputText} | |||
| /> | |||
| <Button | |||
| key="info" | |||
| type="primary" | |||
| style={{ marginLeft: '20px' }} | |||
| // onClick={() => toDetail(record)} | |||
| > | |||
| 查询 | |||
| </Button> | |||
| <Button | |||
| key="info" | |||
| style={{ marginLeft: '20px' }} | |||
| onClick={() => handleSubjectDelete()} | |||
| > | |||
| 批量删除 | |||
| </Button> | |||
| </div> | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columnsNow} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| )} | |||
| {activeTab == '2' && ( | |||
| <div> | |||
| <div className={styles['graphCounter-page__bottomBox__queryparams']}> | |||
| <Input.Search | |||
| placeholder="按镜像名称筛选" | |||
| allowClear | |||
| // onSearch={onSearch} | |||
| // onChange={(e) => setInputText(e.target.value)} | |||
| style={{ width: 300 }} | |||
| // value={inputText} | |||
| /> | |||
| <Button | |||
| key="info" | |||
| type="primary" | |||
| style={{ marginLeft: '20px' }} | |||
| // onClick={() => toDetail(record)} | |||
| > | |||
| 查询 | |||
| </Button> | |||
| <Button | |||
| key="info" | |||
| style={{ marginLeft: '20px' }} | |||
| onClick={() => handleSubjectDelete()} | |||
| > | |||
| 批量删除 | |||
| </Button> | |||
| </div> | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columnsNow} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| )} | |||
| {activeTab == '3' && ( | |||
| <div style={{ height: '100%' }}> | |||
| <div className={styles['graphCounter-page__bottomBox__leftText']}> | |||
| <img | |||
| style={{ width: '40px', marginRight: '20px' }} | |||
| src={require('@/assets/img/computer-img.png')} | |||
| alt="" | |||
| draggable={false} | |||
| /> | |||
| <div className={styles['graphCounter-page__bottomBox__leftText__text']}> | |||
| Heyow too! You sound cute! How can I help you today? Oh, you're curious about my | |||
| smarts, huh? Well, let me assure you, I'm super up-to-date! I update every minute | |||
| and second, so I've got the latest scoop at my digital fingertips! | |||
| </div> | |||
| </div> | |||
| <div className={styles['graphCounter-page__bottomBox__rightText']}> | |||
| <div className={styles['graphCounter-page__bottomBox__rightText__text']}> | |||
| Heyow too! You sound cute! How can I help you today? Oh, you're curious about my | |||
| smarts, huh? Well, let me assure you, I'm super up-to-date! I update every minute | |||
| and second, so I've got the latest scoop at my digital fingertips! | |||
| </div> | |||
| <img | |||
| style={{ width: '40px', marginLeft: '20px' }} | |||
| src={require('@/assets/img/user-answer.png')} | |||
| alt="" | |||
| draggable={false} | |||
| /> | |||
| </div> | |||
| <div className={styles['graphCounter-page__bottomBox__leftText']}> | |||
| <img | |||
| style={{ width: '40px', marginRight: '20px' }} | |||
| src={require('@/assets/img/computer-img.png')} | |||
| alt="" | |||
| draggable={false} | |||
| /> | |||
| <div className={styles['graphCounter-page__bottomBox__leftText__text']}> | |||
| Heyow too! You sound cute! How can I help you today? Oh, you're curious about my | |||
| smarts, huh? Well, let me assure you, I'm super up-to-date! I update every minute | |||
| and second, so I've got the latest scoop at my digital fingertips! | |||
| </div> | |||
| </div> | |||
| <Input.TextArea | |||
| style={{ | |||
| position: 'absolute', | |||
| bottom: '30px', | |||
| left: '40px', | |||
| width: 'calc(100% - 80px)', | |||
| }} | |||
| placeholder="请输入内容" | |||
| autoSize={{ minRows: 6, maxRows: 20 }} | |||
| allowClear | |||
| /> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default DatasetAnnotation; | |||
| @@ -0,0 +1,69 @@ | |||
| .subjectView-container { | |||
| display: flex; | |||
| height: 100%; | |||
| background-color: #fff; | |||
| &__workflow { | |||
| flex: 1 1 0; | |||
| min-width: 0; | |||
| height: 100%; | |||
| &__top { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: end; | |||
| width: 100%; | |||
| height: 52px; | |||
| padding: 0 20px; | |||
| background: #ffffff; | |||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | |||
| &__circle{ | |||
| width:8px; | |||
| height:8px; | |||
| border-radius: 50%; | |||
| margin-right: 10px; | |||
| } | |||
| } | |||
| &__graph { | |||
| width: 100%; | |||
| height: calc(100% - 52px); | |||
| background-color: @background-color; | |||
| background-image: url(@/assets/img/pipeline-canvas-bg.png); | |||
| background-size: 100% 100%; | |||
| } | |||
| } | |||
| } | |||
| :global { | |||
| .pipeline-context-menu { | |||
| width: 78px; | |||
| padding: 10px 0; | |||
| background: #ffffff; | |||
| border-radius: 6px; | |||
| box-shadow: 0px 0px 6px rgba(40, 84, 168, 0.21); | |||
| &__item { | |||
| display: flex; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 34px; | |||
| padding-left: 12px; | |||
| color: @text-color-secondary; | |||
| font-size: 15px; | |||
| cursor: pointer; | |||
| &:hover { | |||
| color: #0d5ef8; | |||
| font-weight: 500; | |||
| background-color: .addAlpha(#8895a8, 0.11) []; | |||
| } | |||
| &__icon { | |||
| width: 1em; | |||
| height: 1em; | |||
| margin-right: 9px; | |||
| fill: currentColor; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,293 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { deleteSubject, getKnowledgeSubject } from '@/services/subject'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, Input, Table, type TablePaginationConfig, type TableProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import AddSubjectConfigModal, { OperationType } from '../components/AddSubjectConfigModal'; | |||
| import styles from './index.less'; | |||
| export type CodeConfigData = { | |||
| id: number; | |||
| code_repo_name: string; | |||
| code_repo_vis: number; | |||
| git_url: string; | |||
| git_branch: string; | |||
| git_user_name: string; | |||
| git_password: string; | |||
| ssh_key: string; | |||
| verify_mode: number; | |||
| create_by: string; | |||
| create_time: string; | |||
| update_by: string; | |||
| update_time: string; | |||
| }; | |||
| export type EditorData = { | |||
| id: number; | |||
| name: string; | |||
| updateTime: string; | |||
| conceptsCount: string; | |||
| update_by: string; | |||
| relationsCount: string; | |||
| description: string; | |||
| }; | |||
| function GraphView() { | |||
| const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined); | |||
| const [tableData, setTableData] = useState<EditorData[]>([]); | |||
| const [cacheState, setCacheState] = useCacheState(); | |||
| const [activeTab, setActiveTab] = useState<string>('1'); | |||
| const navigate = useNavigate(); | |||
| const [inputText, setInputText] = useState<string | undefined>(undefined); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>({ | |||
| current: 1, | |||
| pageSize: 10, | |||
| }); | |||
| const { message } = App.useApp(); | |||
| const handleTableChange: TableProps<EditorData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| }; | |||
| const [total, setTotal] = useState(0); | |||
| useEffect(() => { | |||
| getList(); | |||
| }, []); | |||
| const items = [ | |||
| { | |||
| key: 1, | |||
| label: `图谱版本`, | |||
| icon: <KFIcon type="icon-moxingjianjie" />, | |||
| children: null, | |||
| }, | |||
| { | |||
| key: 2, | |||
| label: `更新日志`, | |||
| icon: <KFIcon type="icon-moxingwenjian" />, | |||
| children: null, | |||
| }, | |||
| ]; | |||
| const createCodeConfig = () => { | |||
| const { close } = openAntdModal(AddSubjectConfigModal, { | |||
| opType: OperationType.Create, | |||
| onOk: () => { | |||
| getList(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| const getList = () => { | |||
| const params: Record<string, any> = { | |||
| pageNum: pagination.current, | |||
| pageSize: pagination.pageSize, | |||
| name: inputText, | |||
| }; | |||
| getKnowledgeSubject(params).then((ret) => { | |||
| console.log(ret.rows); | |||
| setTableData(ret.rows); | |||
| }); | |||
| }; | |||
| // 处理删除 | |||
| const goSubjectEdit = (row) => { | |||
| console.log(row); | |||
| navigate(`/knowledge/subjectView/${row.name}/${row.id}`); | |||
| }; | |||
| const handleSubjectDelete = (row) => { | |||
| modalConfirm({ | |||
| title: '确定要删除该主体吗?', | |||
| content: '删除后主体将不可恢复,是否确认删除?', | |||
| onOk: () => { | |||
| deleteSubject(row.id).then((res) => { | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| setPagination((prev) => ({ | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getList(); | |||
| } | |||
| } | |||
| }); | |||
| }, | |||
| }); | |||
| }; | |||
| const columnsNow: TableProps<EditorData>['columns'] = [ | |||
| { | |||
| title: '序号', | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: '20%', | |||
| render: tableCellRender(false, TableCellValueType.Index, { | |||
| page: pagination.current! - 1, | |||
| pageSize: pagination.pageSize!, | |||
| }), | |||
| }, | |||
| { | |||
| title: '主体名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '20%', | |||
| }, | |||
| { | |||
| title: '更新时间', | |||
| dataIndex: 'updateTime', | |||
| key: 'updateTime', | |||
| width: '20%', | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '概念个数', | |||
| dataIndex: 'conceptsCount', | |||
| key: 'conceptsCount', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '关系个数', | |||
| dataIndex: 'relationsCount', | |||
| key: 'relationsCount', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '主体描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| width: '30%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 300, | |||
| key: 'operation', | |||
| render: (_: any, record: EditorData) => ( | |||
| <div> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="edit" | |||
| onClick={goSubjectEdit} | |||
| icon={<KFIcon type="icon-chakanxiangqing" />} | |||
| > | |||
| 查看 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="run" | |||
| icon={<KFIcon type="icon-bianji" />} | |||
| onClick={() => goSubjectEdit(record)} | |||
| > | |||
| 编辑 | |||
| </Button> | |||
| <Button type="link" size="small" key="run" icon={<KFIcon type="icon-bendishangchuan" />}> | |||
| 导出 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="remove" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => handleSubjectDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['mySubject-page']}> | |||
| <div className={styles['mySubject-page__container']}> | |||
| 主体管理 | |||
| <div className={styles['mySubject-page__container__box']}> | |||
| <img | |||
| className={styles['mySubject-page__container__box__img']} | |||
| src={require('@/assets/img/icon-graph.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span> | |||
| 您还可以创建<span style={{ fontSize: '16px', color: '#1664ff' }}>47</span>个框架{' '} | |||
| </span> | |||
| </div> | |||
| </div> | |||
| <div className={styles['mySubject-page__topCenter']}> | |||
| <div className={styles['mySubject-page__bottomBox']}> | |||
| <div className={styles['mySubject-page__bottomBox__queryparams']}> | |||
| <div className={styles['mySubject-page__bottomBox__queryparams__leftbox']}> | |||
| <Input.Search | |||
| placeholder="按名称筛选" | |||
| allowClear | |||
| onSearch={getList} | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| value={inputText} | |||
| /> | |||
| {/* <Button | |||
| type="primary" | |||
| key="primary" | |||
| style={{ margin: '0 20px' }} | |||
| onClick={() => getList()} | |||
| > | |||
| 查询 | |||
| </Button> */} | |||
| <Button | |||
| style={{ marginLeft: '20px' }} | |||
| key="info" | |||
| onClick={() => handleSubjectDelete()} | |||
| > | |||
| 批量删除 | |||
| </Button> | |||
| </div> | |||
| <div> | |||
| <Button | |||
| onClick={() => createCodeConfig()} | |||
| // onClick={() => toDetail(record)} | |||
| > | |||
| 创建主体 | |||
| </Button> | |||
| <Button key="info" style={{ margin: '0 20px' }}> | |||
| obs导入 | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columnsNow} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default GraphView; | |||
| @@ -0,0 +1,94 @@ | |||
| import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { Graph, INode } from '@antv/g6'; | |||
| import { type MenuProps } from 'antd'; | |||
| // 找到节点所以的上游节点 | |||
| export const findAllParentNodes = (graph: Graph, node: INode) => { | |||
| const parentNodes: INode[] = []; | |||
| let index = -1; | |||
| let targetNode = node; | |||
| while (targetNode) { | |||
| const neighbors: INode[] = graph.getNeighbors(targetNode, 'source'); | |||
| for (const sourceNode of neighbors) { | |||
| // 避免重复,也避免循环 | |||
| const idx = parentNodes.findIndex((item) => sourceNode.getID() === item.getID()); | |||
| if (idx === -1 && sourceNode.getID() !== node.getID()) { | |||
| parentNodes.push(sourceNode); | |||
| } | |||
| } | |||
| targetNode = parentNodes[++index]; | |||
| } | |||
| return parentNodes; | |||
| }; | |||
| // 判断并找到全局参数第一个重复项,有重复项时,全局参数不允许保存 | |||
| export function findFirstDuplicate(params: PipelineGlobalParam[]): string | null { | |||
| const seen = new Set(); | |||
| for (const item of params) { | |||
| if (seen.has(item.param_name)) { | |||
| return item.param_name; | |||
| } | |||
| seen.add(item.param_name); | |||
| } | |||
| return null; | |||
| } | |||
| // 创建参数下拉菜单 | |||
| export function createMenuItems( | |||
| params: PipelineGlobalParam[], | |||
| parentNodes: INode[], | |||
| ): MenuProps['items'] { | |||
| const nodes: MenuProps['items'] = parentNodes.map((item) => { | |||
| const model = item.getModel(); | |||
| const out_parameters = model.out_parameters as string | undefined | null; | |||
| const out_parametersObj = parseJsonText(out_parameters); | |||
| const outParametersList = Object.keys(out_parametersObj ?? {}); | |||
| return { | |||
| key: model.id as string, | |||
| label: model.label as string, | |||
| children: outParametersList.map((key: string) => ({ | |||
| key: key as string, | |||
| label: out_parametersObj[key].label, | |||
| })), | |||
| }; | |||
| }); | |||
| if (params.length > 0) { | |||
| return [ | |||
| { | |||
| key: 'global', | |||
| label: '全局参数', | |||
| children: params.map((item) => ({ | |||
| key: item.param_name, | |||
| label: item.param_name, | |||
| })), | |||
| }, | |||
| ...nodes, | |||
| ]; | |||
| } else { | |||
| return [...nodes]; | |||
| } | |||
| } | |||
| export function getInParameterComponent( | |||
| parameter: PipelineNodeModelParameter, | |||
| ): React.ReactNode | null { | |||
| if (parameter.value) { | |||
| } | |||
| return null; | |||
| } | |||
| // 判断是否允许输入 | |||
| export function canInput(parameter: PipelineNodeModelParameter) { | |||
| const { type, item_type } = parameter; | |||
| return !( | |||
| type === 'ref' && | |||
| (item_type === 'dataset' || | |||
| item_type === 'model' || | |||
| item_type === 'image' || | |||
| item_type === 'code') | |||
| ); | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| .collapse { | |||
| flex: none; | |||
| width: 250px; | |||
| height: 100%; | |||
| :global { | |||
| .ant-collapse { | |||
| height: calc(100% - 60px); | |||
| overflow-y: auto; | |||
| background-color: #fff; | |||
| border-color: transparent !important; | |||
| } | |||
| .ant-collapse > .ant-collapse-item > .ant-collapse-header { | |||
| margin-bottom: 5px; | |||
| padding: 20px 16px 15px 16px; | |||
| background-color: #fff; | |||
| border-color: transparent; | |||
| } | |||
| .ant-collapse > .ant-collapse-item { | |||
| margin: 0 10px; | |||
| border-bottom: 0.5px dashed rgba(20, 49, 179, 0.12); | |||
| border-radius: 0px; | |||
| } | |||
| .ant-collapse .ant-collapse-content { | |||
| padding-bottom: 15px; | |||
| border-top: 1px solid transparent; | |||
| } | |||
| .ant-collapse .ant-collapse-content > .ant-collapse-content-box { | |||
| padding: 0; | |||
| } | |||
| } | |||
| } | |||
| .collapseItem { | |||
| display: flex; | |||
| align-items: center; | |||
| height: 40px; | |||
| padding: 0 16px; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| &:hover { | |||
| color: @primary-color; | |||
| background: rgba(22, 100, 255, 0.08); | |||
| } | |||
| } | |||
| .modelMenusTitle { | |||
| margin-bottom: 10px; | |||
| padding: 12px 25px; | |||
| color: #111111; | |||
| font-size: 16px; | |||
| } | |||
| @@ -0,0 +1,92 @@ | |||
| import { getComponentAll } from '@/services/pipeline/index.js'; | |||
| import { PipelineNodeModel } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Collapse, type CollapseProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import Styles from './index.less'; | |||
| type ModelMenuData = { | |||
| key: string; | |||
| name: string; | |||
| value: PipelineNodeModel[]; | |||
| }; | |||
| type ModelMenuProps = { | |||
| onComponentDragEnd: ( | |||
| data: PipelineNodeModel & { x: number; y: number; label: string; img: string }, | |||
| ) => void; | |||
| }; | |||
| const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { | |||
| const [modelMenusList, setModelMenusList] = useState<ModelMenuData[]>([]); | |||
| const [collapseItems, setCollapseItems] = useState<CollapseProps['items']>([]); | |||
| useEffect(() => { | |||
| getAllComponents(); | |||
| }, []); | |||
| // 获取所有组件 | |||
| const getAllComponents = async () => { | |||
| const [res] = await to(getComponentAll()); | |||
| if (res && res.data) { | |||
| const menus = res.data as ModelMenuData[]; | |||
| setModelMenusList(menus); | |||
| const items = menus.map((item) => { | |||
| return { | |||
| key: item.key, | |||
| label: item.name, | |||
| children: item.value.map((ele) => { | |||
| return ( | |||
| <div | |||
| key={ele.id} | |||
| draggable="true" | |||
| onDragEnd={(e) => { | |||
| dragEnd(e, ele); | |||
| }} | |||
| className={Styles.collapseItem} | |||
| > | |||
| {ele.icon_path && ( | |||
| <img | |||
| style={{ height: '16px', marginRight: '15px' }} | |||
| src={`/assets/images/${ele.icon_path}.png`} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| )} | |||
| {ele.component_label} | |||
| </div> | |||
| ); | |||
| }), | |||
| }; | |||
| }); | |||
| setCollapseItems(items); | |||
| } | |||
| }; | |||
| const dragEnd = (e: React.DragEvent<HTMLDivElement>, data: PipelineNodeModel) => { | |||
| onComponentDragEnd({ | |||
| ...data, | |||
| x: e.clientX, | |||
| y: e.clientY, | |||
| label: data.component_label, | |||
| img: `/assets/images/${data.icon_path}.png`, | |||
| }); | |||
| }; | |||
| const defaultActiveKey = modelMenusList.map((item) => item.key + ''); | |||
| return ( | |||
| <div className={Styles.collapse}> | |||
| <div className={Styles.modelMenusTitle}>组件库</div> | |||
| {/* 这样 defaultActiveKey 才能生效 */} | |||
| {modelMenusList.length > 0 ? ( | |||
| <Collapse | |||
| collapsible="header" | |||
| expandIconPosition="end" | |||
| defaultActiveKey={defaultActiveKey} | |||
| items={collapseItems} | |||
| ></Collapse> | |||
| ) : null} | |||
| </div> | |||
| ); | |||
| }; | |||
| export default ModelMenu; | |||
| @@ -0,0 +1,115 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Button, Typography } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import styles from './index.less'; | |||
| export type CodeConfigData = { | |||
| id: number; //主键 | |||
| name: string; //名称 | |||
| spec: string; //图谱规格 | |||
| createTime: string; //创建时间 | |||
| relationCount: number; //关系数 | |||
| entityCount: number; //实体数 | |||
| }; | |||
| type CodeConfigItemProps = { | |||
| item: CodeConfigData; | |||
| onClick?: (item: CodeConfigData) => void; | |||
| onEdit?: (item: CodeConfigData) => void; | |||
| onRemove?: (item: CodeConfigData) => void; | |||
| }; | |||
| function graphItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps) { | |||
| const [dataList, setDataList] = useState<CodeConfigData[] | undefined>([]); | |||
| const navigate = useNavigate(); | |||
| useEffect(() => {}, []); | |||
| return ( | |||
| <div | |||
| className={styles['myGraph-page__centercontent__box']} | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onClick?.(item); | |||
| }} | |||
| > | |||
| <div className={styles['myGraph-page__centercontent__box__top']}> | |||
| <div className={styles['myGraph-page__centercontent__box__top__left']}> | |||
| <img | |||
| className={styles['myGraph-page__centercontent__box__top__left__img']} | |||
| src={require('@/assets/img/graph-star.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <Typography.Paragraph | |||
| className={styles['myGraph-page__centercontent__box__top__left__name']} | |||
| ellipsis={{ tooltip: item.name }} | |||
| > | |||
| {item.name} | |||
| </Typography.Paragraph> | |||
| </div> | |||
| <div className={styles['myGraph-page__centercontent__box__top__right']}> | |||
| <Button | |||
| type="text" | |||
| shape="circle" | |||
| style={{ marginLeft: 'auto', marginRight: '4px' }} | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onEdit?.(item); | |||
| }} | |||
| > | |||
| <KFIcon type="icon-bianji" font={17} /> | |||
| </Button> | |||
| <Button | |||
| type="text" | |||
| shape="circle" | |||
| style={{ marginRight: '-4px' }} | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onRemove?.(item); | |||
| }} | |||
| > | |||
| <KFIcon type="icon-shanchu" font={17} /> | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| <div className={styles['myGraph-page__centercontent__box__center']}> | |||
| <span> | |||
| <span className={styles['myGraph-page__centercontent__box__center__word']}>图谱规格</span> | |||
| <span className={styles['myGraph-page__centercontent__box__center__word']}> | |||
| {item.spec} | |||
| </span> | |||
| </span> | |||
| <span> | |||
| <span className={styles['myGraph-page__centercontent__box__center__word']}> | |||
| 实体/关系 | |||
| </span> | |||
| <span className={styles['myGraph-page__centercontent__box__center__word']}> | |||
| {item.entityCount / item.relationCount} | |||
| </span> | |||
| </span> | |||
| <span> | |||
| <span className={styles['myGraph-page__centercontent__box__center__word']}>创建时间</span> | |||
| <span className={styles['myGraph-page__centercontent__box__center__word']}> | |||
| {formatDate(item.createTime)} | |||
| </span> | |||
| </span> | |||
| </div> | |||
| <div className={styles['myGraph-page__centercontent__box__bottom']}> | |||
| <div className={styles['myGraph-page__centercontent__box__bottom__left']}> | |||
| <img | |||
| className={styles['myGraph-page__centercontent__box__bottom__left__img']} | |||
| src={require('@/assets/img/graph-correct.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span>可用</span> | |||
| </div> | |||
| <span> | |||
| 试用期剩余<span style={{ color: '#c73131' }}>27</span>天 | |||
| </span> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default graphItem; | |||
| @@ -0,0 +1,181 @@ | |||
| .myGraph-page { | |||
| height: 100%; | |||
| &__container { | |||
| display: flex; | |||
| align-items: center; | |||
| height: 50px; | |||
| padding-left: 30px; | |||
| background-image: url(@/assets/img/page-title-bg.png); | |||
| background-repeat: no-repeat; | |||
| background-position: top center; | |||
| background-size: 100% 100%; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| &__box{ | |||
| display: flex; | |||
| align-items: center; | |||
| width:206px; | |||
| height:27px; | |||
| background:rgba(22, 100, 255, 0.09); | |||
| border-radius:4px; | |||
| color:#4c5360; | |||
| font-size:14px; | |||
| margin-left: 5px; | |||
| &__img{ | |||
| width: 24px; | |||
| height: 24px; | |||
| margin: 0 5px 0 9px; | |||
| } | |||
| } | |||
| } | |||
| &__centercontent{ | |||
| margin-top: 10px; | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| align-items: center; | |||
| &__box{ | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| width: calc(25% - 15px); | |||
| height:229px; | |||
| background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%); | |||
| border:2px solid; | |||
| border-color:#ffffff; | |||
| border-radius:4px; | |||
| box-shadow:0px 3px 10px rgba(164, 169, 181, 0.13); | |||
| margin: 20px 0 0 20px; | |||
| padding: 20px; | |||
| cursor: pointer; | |||
| &:hover { | |||
| border-color: @primary-color; | |||
| } | |||
| &__top{ | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 22px; | |||
| color:#1664ff; | |||
| font-size:16px; | |||
| &__left{ | |||
| display: flex; | |||
| align-items: center; | |||
| &__img{ | |||
| width: 16px; | |||
| height: 16px; | |||
| margin-right: 10px; | |||
| } | |||
| &__name{ | |||
| position: relative; | |||
| margin-right: 20px; | |||
| margin-bottom: 0 !important; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 16px; | |||
| &::after { | |||
| position: absolute; | |||
| top: 14px; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 6px; | |||
| background: linear-gradient( | |||
| to right, | |||
| .addAlpha(@primary-color, 0.4) [] 0, | |||
| .addAlpha(@primary-color, 0) [] 100% | |||
| ); | |||
| content: ''; | |||
| } | |||
| } | |||
| } | |||
| &__right{ | |||
| display: flex; | |||
| align-items: center; | |||
| &__img{ | |||
| width: 20px; | |||
| height: 20px; | |||
| margin-left: 15px; | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| } | |||
| &__center{ | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| width:100%; | |||
| height:114px; | |||
| background:rgba(22, 100, 255, 0.04); | |||
| border-radius:4px; | |||
| padding: 15px; | |||
| &__word{ | |||
| color:#565658; | |||
| font-size:14px; | |||
| margin-right: 15px; | |||
| } | |||
| } | |||
| &__bottom{ | |||
| width: 100%; | |||
| height: 18px; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| color:#808080; | |||
| font-size:13px; | |||
| &__left{ | |||
| display: flex; | |||
| align-items: center; | |||
| &__img{ | |||
| width: 16px; | |||
| height: 16px; | |||
| margin-right: 6px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__addBox{ | |||
| width:350px; | |||
| height:189px; | |||
| background:rgba(22, 100, 255, 0.04); | |||
| border:1px dashed; | |||
| border-color:rgba(22, 100, 255, 0.5); | |||
| border-radius:4px; | |||
| margin: 20px 0 0 40px; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| cursor: pointer; | |||
| &__button{ | |||
| position: relative; | |||
| width:53px; | |||
| height:53px; | |||
| background:rgba(22, 100, 255, 0.05); | |||
| border:1px dotted; | |||
| border-color:#1664ff; | |||
| border-radius: 50%; | |||
| &__horizontal{ | |||
| width: 13px; | |||
| height: 0; | |||
| border-top: 2px solid #1664ff; | |||
| position: absolute; | |||
| top: 50%; | |||
| left: 50%; | |||
| transform: translate(-6.5px, -1px) | |||
| } | |||
| &__vertical{ | |||
| width: 0; | |||
| height: 13px; | |||
| border-left: 2px solid #1664ff; | |||
| position: absolute; | |||
| top: 50%; | |||
| left: 50%; | |||
| transform: translate(-1px,-6.5px) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,181 @@ | |||
| .myGraph-page { | |||
| height: 100%; | |||
| &__container { | |||
| display: flex; | |||
| align-items: center; | |||
| height: 50px; | |||
| padding-left: 30px; | |||
| background-image: url(@/assets/img/page-title-bg.png); | |||
| background-repeat: no-repeat; | |||
| background-position: top center; | |||
| background-size: 100% 100%; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| &__box{ | |||
| display: flex; | |||
| align-items: center; | |||
| width:206px; | |||
| height:27px; | |||
| background:rgba(22, 100, 255, 0.09); | |||
| border-radius:4px; | |||
| color:#4c5360; | |||
| font-size:14px; | |||
| margin-left: 5px; | |||
| &__img{ | |||
| width: 24px; | |||
| height: 24px; | |||
| margin: 0 5px 0 9px; | |||
| } | |||
| } | |||
| } | |||
| &__centercontent{ | |||
| margin-top: 10px; | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| align-items: center; | |||
| &__box{ | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| width: calc(25% - 15px); | |||
| height:229px; | |||
| background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%); | |||
| border:2px solid; | |||
| border-color:#ffffff; | |||
| border-radius:4px; | |||
| box-shadow:0px 3px 10px rgba(164, 169, 181, 0.13); | |||
| margin: 20px 0 0 20px; | |||
| padding: 20px; | |||
| cursor: pointer; | |||
| &:hover { | |||
| border-color: @primary-color; | |||
| } | |||
| &__top{ | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 22px; | |||
| color:#1664ff; | |||
| font-size:16px; | |||
| &__left{ | |||
| display: flex; | |||
| align-items: center; | |||
| &__img{ | |||
| width: 16px; | |||
| height: 16px; | |||
| margin-right: 10px; | |||
| } | |||
| &__name{ | |||
| position: relative; | |||
| margin-right: 20px; | |||
| margin-bottom: 0 !important; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 16px; | |||
| &::after { | |||
| position: absolute; | |||
| top: 14px; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 6px; | |||
| background: linear-gradient( | |||
| to right, | |||
| .addAlpha(@primary-color, 0.4) [] 0, | |||
| .addAlpha(@primary-color, 0) [] 100% | |||
| ); | |||
| content: ''; | |||
| } | |||
| } | |||
| } | |||
| &__right{ | |||
| display: flex; | |||
| align-items: center; | |||
| &__img{ | |||
| width: 20px; | |||
| height: 20px; | |||
| margin-left: 15px; | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| } | |||
| &__center{ | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| width:100%; | |||
| height:114px; | |||
| background:rgba(22, 100, 255, 0.04); | |||
| border-radius:4px; | |||
| padding: 15px; | |||
| &__word{ | |||
| color:#565658; | |||
| font-size:14px; | |||
| margin-right: 15px; | |||
| } | |||
| } | |||
| &__bottom{ | |||
| width: 100%; | |||
| height: 18px; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| color:#808080; | |||
| font-size:13px; | |||
| &__left{ | |||
| display: flex; | |||
| align-items: center; | |||
| &__img{ | |||
| width: 16px; | |||
| height: 16px; | |||
| margin-right: 6px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__addBox{ | |||
| width:350px; | |||
| height:189px; | |||
| background:rgba(22, 100, 255, 0.04); | |||
| border:1px dashed; | |||
| border-color:rgba(22, 100, 255, 0.5); | |||
| border-radius:4px; | |||
| margin: 20px 0 0 40px; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| cursor: pointer; | |||
| &__button{ | |||
| position: relative; | |||
| width:53px; | |||
| height:53px; | |||
| background:rgba(22, 100, 255, 0.05); | |||
| border:1px dotted; | |||
| border-color:#1664ff; | |||
| border-radius: 50%; | |||
| &__horizontal{ | |||
| width: 13px; | |||
| height: 0; | |||
| border-top: 2px solid #1664ff; | |||
| position: absolute; | |||
| top: 50%; | |||
| left: 50%; | |||
| transform: translate(-6.5px, -1px) | |||
| } | |||
| &__vertical{ | |||
| width: 0; | |||
| height: 13px; | |||
| border-left: 2px solid #1664ff; | |||
| position: absolute; | |||
| top: 50%; | |||
| left: 50%; | |||
| transform: translate(-1px,-6.5px) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,124 @@ | |||
| import { deleteKnowledgeGraph, getKnowledgeGraph } from '@/services/graph'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; | |||
| import GraphItem from './components/graphItem'; | |||
| import styles from './index.less'; | |||
| export type CodeConfigData = { | |||
| id: number; //主键 | |||
| name: string; //名称 | |||
| status: number; //状态,0代表不可用,1代表可用 | |||
| spec: string; //图谱规格 | |||
| createTime: string; //创建时间 | |||
| relationCount: number; //关系数 | |||
| entityCount: number; //实体数 | |||
| }; | |||
| // type CodeConfigItemProps = { | |||
| // item: CodeConfigData; | |||
| // onClick?: (item: CodeConfigData) => void; | |||
| // onEdit?: (item: CodeConfigData) => void; | |||
| // onRemove?: (item: CodeConfigData) => void; | |||
| // }; | |||
| function DatasetAnnotation() { | |||
| const [dataList, setDataList] = useState<CodeConfigData[] | undefined>([]); | |||
| const navigate = useNavigate(); | |||
| const createCodeConfig = () => { | |||
| const { close } = openAntdModal(AddCodeConfigModal, { | |||
| opType: OperationType.Create, | |||
| onOk: () => { | |||
| getList(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 删除 | |||
| const handleRemove = (record: CodeConfigData) => { | |||
| console.log(record); | |||
| modalConfirm({ | |||
| title: '确定删除这个图谱?', | |||
| onOk: () => { | |||
| deleteKnowledgeGraph(record.id); | |||
| getList(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 修改 | |||
| const handleEdit = (record: CodeConfigData) => { | |||
| const { close } = openAntdModal(AddCodeConfigModal, { | |||
| opType: OperationType.Update, | |||
| codeConfigData: record, | |||
| onOk: () => { | |||
| getList(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 查看 | |||
| // const handleClick = (record: CodeConfigData) => { | |||
| // const { git_url, git_branch } = record; | |||
| // const url = getGitUrl(git_url, git_branch); | |||
| // window.open(url, '_blank'); | |||
| // }; | |||
| const getList = () => { | |||
| getKnowledgeGraph().then((ret) => { | |||
| console.log(ret.rows); | |||
| setDataList(ret.rows); | |||
| }); | |||
| }; | |||
| const clickGraph = (record: any) => { | |||
| navigate(`/knowledge/onlineGraph?id=${record.id}&version=${record.version}`); | |||
| // navigate({ pathname: `/knowledge/onlineGraph` }); | |||
| }; | |||
| useEffect(() => { | |||
| getList(); | |||
| }, []); | |||
| return ( | |||
| <div className={styles['myGraph-page']}> | |||
| <div className={styles['myGraph-page__container']}> | |||
| 图谱管理 | |||
| <div className={styles['myGraph-page__container__box']}> | |||
| <img | |||
| className={styles['myGraph-page__container__box__img']} | |||
| src={require('@/assets/img/icon-graph.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span> | |||
| 您还可以创建<span style={{ fontSize: '16px', color: '#1664ff' }}>4</span>个图谱{' '} | |||
| </span> | |||
| </div> | |||
| </div> | |||
| <div className={styles['myGraph-page__centercontent']}> | |||
| {dataList && dataList.length != 0 && ( | |||
| <> | |||
| {dataList.map((item) => ( | |||
| <GraphItem | |||
| item={item} | |||
| key={item.id} | |||
| onRemove={handleRemove} | |||
| onEdit={handleEdit} | |||
| onClick={clickGraph} | |||
| ></GraphItem> | |||
| ))} | |||
| </> | |||
| )} | |||
| <div className={styles['myGraph-page__centercontent__addBox']} onClick={createCodeConfig}> | |||
| <div className={styles['myGraph-page__centercontent__addBox__button']}> | |||
| <div | |||
| className={styles['myGraph-page__centercontent__addBox__button__horizontal']} | |||
| ></div> | |||
| <div className={styles['myGraph-page__centercontent__addBox__button__vertical']}></div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default DatasetAnnotation; | |||
| @@ -0,0 +1,192 @@ | |||
| .mySubject-page { | |||
| height: 100%; | |||
| padding: 0 9px; | |||
| &__container { | |||
| display: flex; | |||
| align-items: center; | |||
| height: 50px; | |||
| padding:0 30px; | |||
| background-image: url(@/assets/img/page-title-bg.png); | |||
| background-repeat: no-repeat; | |||
| background-position: top center; | |||
| background-size: 100% 100%; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| &__box{ | |||
| display: flex; | |||
| align-items: center; | |||
| width:206px; | |||
| height:27px; | |||
| background:rgba(22, 100, 255, 0.09); | |||
| border-radius:4px; | |||
| color:#4c5360; | |||
| font-size:14px; | |||
| margin-left: 5px; | |||
| &__img{ | |||
| width: 24px; | |||
| height: 24px; | |||
| margin: 0 5px 0 9px; | |||
| } | |||
| } | |||
| } | |||
| &__topCenter{ | |||
| margin: 10px 0 15px 0; | |||
| width: 100%; | |||
| background:#ffffff; | |||
| border-radius:10px; | |||
| box-shadow:0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| } | |||
| &__title{ | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| margin: 30px 0 15px 25px; | |||
| font-weight: 600; | |||
| } | |||
| &__listBox{ | |||
| padding: 0 25px; | |||
| display: flex; | |||
| align-items: center; | |||
| &__itemBox{ | |||
| position: relative; | |||
| width: calc(20% - 15px); | |||
| height:100px; | |||
| background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%); | |||
| border-radius:4px; | |||
| box-shadow:0px 3px 10px rgba(164, 169, 181, 0.12); | |||
| margin-right: 15px; | |||
| &__img{ | |||
| position: absolute; | |||
| left: 30px; | |||
| top: 26px; | |||
| width: 48px; | |||
| height: 48px; | |||
| } | |||
| &__wordTitle{ | |||
| position: absolute; | |||
| top: 26px; | |||
| left: 102px; | |||
| font-weight:600; | |||
| color:#1d1d20; | |||
| font-size:18px; | |||
| } | |||
| &__wordContent{ | |||
| position: absolute; | |||
| top: 54px; | |||
| left: 102px; | |||
| color:#565658; | |||
| font-size:15px; | |||
| } | |||
| } | |||
| } | |||
| &__centerListBox{ | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 20px 45px; | |||
| &__itemBox{ | |||
| position: relative; | |||
| width: calc(20% - 20px); | |||
| height: 229px; | |||
| margin-right: 20px; | |||
| background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%); | |||
| border-radius:4px; | |||
| box-shadow:0px 3px 10px rgba(164, 169, 181, 0.13); | |||
| &__img{ | |||
| width: 16px; | |||
| height: 16px; | |||
| position: absolute; | |||
| top: 24.5px; | |||
| left: 20px; | |||
| } | |||
| &__title{ | |||
| position: absolute; | |||
| top: 20.1px; | |||
| left: 46px; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| font-weight: 600; | |||
| } | |||
| &__content{ | |||
| position: absolute; | |||
| width: calc(100% - 40px); | |||
| height: 114px; | |||
| top: 61px; | |||
| left: 20px; | |||
| background:rgba(22, 100, 255, 0.04); | |||
| border-radius:4px; | |||
| } | |||
| } | |||
| } | |||
| &__bottomBox{ | |||
| padding: 23px 35px 23px 25px; | |||
| &__titleBox{ | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 39px; | |||
| &__left{ | |||
| display: flex; | |||
| align-items: center; | |||
| font-weight:600; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| &__word{ | |||
| display: flex; | |||
| align-items: center; | |||
| width:206px; | |||
| height:27px; | |||
| margin-left: 14px; | |||
| background:rgba(22, 100, 255, 0.09); | |||
| border-radius:4px; | |||
| padding: 2px 20px 2px 16px; | |||
| color:#4c5360; | |||
| font-size:14px; | |||
| font-weight: normal; | |||
| &__circle{ | |||
| position: relative; | |||
| width:15px; | |||
| height:15px; | |||
| border-radius: 50%; | |||
| background:#ffffff; | |||
| box-shadow:0px 1px 3px rgba(0, 82, 217, 0.09); | |||
| margin-right: 13px; | |||
| &__inner{ | |||
| position: absolute; | |||
| left: 50%; | |||
| top: 50%; | |||
| transform: translate(-50%,-50%); | |||
| width:9px; | |||
| height:9px; | |||
| border-radius: 50%; | |||
| background:#1664ff; | |||
| box-shadow:0px 1px 3px rgba(22, 100, 255, 0.27); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__right{ | |||
| display: flex; | |||
| align-items: center; | |||
| // width:647px; | |||
| height:39px; | |||
| background:rgba(138, 138, 138, 0.05); | |||
| border-radius:4px; | |||
| padding-left: 30px; | |||
| } | |||
| } | |||
| &__queryparams{ | |||
| margin: 17px 0; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| &__leftbox{ | |||
| display: flex; | |||
| align-items: center; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,293 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { deleteSubject, getKnowledgeSubject } from '@/services/subject'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, Input, Table, type TablePaginationConfig, type TableProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import AddSubjectConfigModal, { OperationType } from '../components/AddSubjectConfigModal'; | |||
| import styles from './index.less'; | |||
| export type CodeConfigData = { | |||
| id: number; | |||
| code_repo_name: string; | |||
| code_repo_vis: number; | |||
| git_url: string; | |||
| git_branch: string; | |||
| git_user_name: string; | |||
| git_password: string; | |||
| ssh_key: string; | |||
| verify_mode: number; | |||
| create_by: string; | |||
| create_time: string; | |||
| update_by: string; | |||
| update_time: string; | |||
| }; | |||
| export type EditorData = { | |||
| id: number; | |||
| name: string; | |||
| updateTime: string; | |||
| conceptsCount: string; | |||
| update_by: string; | |||
| relationsCount: string; | |||
| description: string; | |||
| }; | |||
| function DatasetAnnotation() { | |||
| const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined); | |||
| const [tableData, setTableData] = useState<EditorData[]>([]); | |||
| const [cacheState, setCacheState] = useCacheState(); | |||
| const [activeTab, setActiveTab] = useState<string>('1'); | |||
| const navigate = useNavigate(); | |||
| const [inputText, setInputText] = useState<string | undefined>(undefined); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>({ | |||
| current: 1, | |||
| pageSize: 10, | |||
| }); | |||
| const { message } = App.useApp(); | |||
| const handleTableChange: TableProps<EditorData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| }; | |||
| const [total, setTotal] = useState(0); | |||
| useEffect(() => { | |||
| getList(); | |||
| }, []); | |||
| const items = [ | |||
| { | |||
| key: 1, | |||
| label: `图谱版本`, | |||
| icon: <KFIcon type="icon-moxingjianjie" />, | |||
| children: null, | |||
| }, | |||
| { | |||
| key: 2, | |||
| label: `更新日志`, | |||
| icon: <KFIcon type="icon-moxingwenjian" />, | |||
| children: null, | |||
| }, | |||
| ]; | |||
| const createCodeConfig = () => { | |||
| const { close } = openAntdModal(AddSubjectConfigModal, { | |||
| opType: OperationType.Create, | |||
| onOk: () => { | |||
| getList(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| const getList = () => { | |||
| const params: Record<string, any> = { | |||
| pageNum: pagination.current, | |||
| pageSize: pagination.pageSize, | |||
| name: inputText, | |||
| }; | |||
| getKnowledgeSubject(params).then((ret) => { | |||
| console.log(ret.rows); | |||
| setTableData(ret.rows); | |||
| }); | |||
| }; | |||
| // 处理删除 | |||
| const goSubjectEdit = (row) => { | |||
| console.log(row); | |||
| navigate(`/knowledge/subjectView/${row.name}/${row.id}`); | |||
| }; | |||
| const handleSubjectDelete = (row) => { | |||
| modalConfirm({ | |||
| title: '确定要删除该主体吗?', | |||
| content: '删除后主体将不可恢复,是否确认删除?', | |||
| onOk: () => { | |||
| deleteSubject(row.id).then((res) => { | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| setPagination((prev) => ({ | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| } else { | |||
| getList(); | |||
| } | |||
| } | |||
| }); | |||
| }, | |||
| }); | |||
| }; | |||
| const columnsNow: TableProps<EditorData>['columns'] = [ | |||
| { | |||
| title: '序号', | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: '20%', | |||
| render: tableCellRender(false, TableCellValueType.Index, { | |||
| page: pagination.current! - 1, | |||
| pageSize: pagination.pageSize!, | |||
| }), | |||
| }, | |||
| { | |||
| title: '主体名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '20%', | |||
| }, | |||
| { | |||
| title: '更新时间', | |||
| dataIndex: 'updateTime', | |||
| key: 'updateTime', | |||
| width: '20%', | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '概念个数', | |||
| dataIndex: 'conceptsCount', | |||
| key: 'conceptsCount', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '关系个数', | |||
| dataIndex: 'relationsCount', | |||
| key: 'relationsCount', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '主体描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| width: '30%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: 300, | |||
| key: 'operation', | |||
| render: (_: any, record: EditorData) => ( | |||
| <div> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="edit" | |||
| onClick={goSubjectEdit} | |||
| icon={<KFIcon type="icon-chakanxiangqing" />} | |||
| > | |||
| 查看 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="run" | |||
| icon={<KFIcon type="icon-bianji" />} | |||
| onClick={() => goSubjectEdit(record)} | |||
| > | |||
| 编辑 | |||
| </Button> | |||
| <Button type="link" size="small" key="run" icon={<KFIcon type="icon-bendishangchuan" />}> | |||
| 导出 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="remove" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => handleSubjectDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['mySubject-page']}> | |||
| <div className={styles['mySubject-page__container']}> | |||
| 主体管理 | |||
| <div className={styles['mySubject-page__container__box']}> | |||
| <img | |||
| className={styles['mySubject-page__container__box__img']} | |||
| src={require('@/assets/img/icon-graph.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span> | |||
| 您还可以创建<span style={{ fontSize: '16px', color: '#1664ff' }}>47</span>个框架{' '} | |||
| </span> | |||
| </div> | |||
| </div> | |||
| <div className={styles['mySubject-page__topCenter']}> | |||
| <div className={styles['mySubject-page__bottomBox']}> | |||
| <div className={styles['mySubject-page__bottomBox__queryparams']}> | |||
| <div className={styles['mySubject-page__bottomBox__queryparams__leftbox']}> | |||
| <Input.Search | |||
| placeholder="按名称筛选" | |||
| allowClear | |||
| onSearch={getList} | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| value={inputText} | |||
| /> | |||
| {/* <Button | |||
| type="primary" | |||
| key="primary" | |||
| style={{ margin: '0 20px' }} | |||
| onClick={() => getList()} | |||
| > | |||
| 查询 | |||
| </Button> */} | |||
| <Button | |||
| style={{ marginLeft: '20px' }} | |||
| key="info" | |||
| onClick={() => handleSubjectDelete()} | |||
| > | |||
| 批量删除 | |||
| </Button> | |||
| </div> | |||
| <div> | |||
| <Button | |||
| onClick={() => createCodeConfig()} | |||
| // onClick={() => toDetail(record)} | |||
| > | |||
| 创建主体 | |||
| </Button> | |||
| <Button key="info" style={{ margin: '0 20px' }}> | |||
| obs导入 | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columnsNow} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default DatasetAnnotation; | |||
| @@ -0,0 +1,206 @@ | |||
| .onlineGraph-page { | |||
| height: 100%; | |||
| padding: 0 9px; | |||
| &__container { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| height: 50px; | |||
| padding:0 30px; | |||
| background-image: url(@/assets/img/page-title-bg.png); | |||
| background-repeat: no-repeat; | |||
| background-position: top center; | |||
| background-size: 100% 100%; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| &__box{ | |||
| display: flex; | |||
| align-items: center; | |||
| color:#1d1d20; | |||
| font-size:15px; | |||
| &__update{ | |||
| padding: 7px 10px; | |||
| background:rgba(22, 100, 255, 0.06); | |||
| border:1px solid rgba(22, 100, 255, 0.11); | |||
| border-radius:4px; | |||
| margin-right: 20px; | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| } | |||
| &__topCenter{ | |||
| margin: 10px 0 15px 0; | |||
| width: 100%; | |||
| background:#ffffff; | |||
| border-radius:10px; | |||
| box-shadow:0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| } | |||
| &__title{ | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| margin: 30px 0 15px 25px; | |||
| font-weight: 600; | |||
| } | |||
| &__listBox{ | |||
| padding: 0 25px; | |||
| display: flex; | |||
| align-items: center; | |||
| &__itemBox{ | |||
| position: relative; | |||
| width: calc(20% - 15px); | |||
| height:100px; | |||
| background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%); | |||
| border-radius:4px; | |||
| box-shadow:0px 3px 10px rgba(164, 169, 181, 0.12); | |||
| margin-right: 15px; | |||
| &__img{ | |||
| position: absolute; | |||
| left: 30px; | |||
| top: 26px; | |||
| width: 48px; | |||
| height: 48px; | |||
| } | |||
| &__wordTitle{ | |||
| position: absolute; | |||
| top: 26px; | |||
| left: 102px; | |||
| font-weight:600; | |||
| color:#1d1d20; | |||
| font-size:18px; | |||
| } | |||
| &__wordContent{ | |||
| position: absolute; | |||
| top: 54px; | |||
| left: 102px; | |||
| color:#565658; | |||
| font-size:15px; | |||
| } | |||
| } | |||
| } | |||
| &__centerListBox{ | |||
| display: flex; | |||
| align-items: center; | |||
| padding: 20px 45px; | |||
| &__itemBox{ | |||
| position: relative; | |||
| width: calc(20% - 20px); | |||
| height: 229px; | |||
| margin-right: 20px; | |||
| background:linear-gradient(180deg,#f7faff 0%,#ffffff 100%); | |||
| border-radius:4px; | |||
| box-shadow:0px 3px 10px rgba(164, 169, 181, 0.13); | |||
| &__img{ | |||
| width: 16px; | |||
| height: 16px; | |||
| position: absolute; | |||
| top: 24.5px; | |||
| left: 20px; | |||
| } | |||
| &__title{ | |||
| position: absolute; | |||
| top: 20.1px; | |||
| left: 46px; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| font-weight: 600; | |||
| } | |||
| &__content{ | |||
| position: absolute; | |||
| width: calc(100% - 40px); | |||
| height: 114px; | |||
| top: 61px; | |||
| left: 20px; | |||
| background:rgba(22, 100, 255, 0.04); | |||
| border-radius:4px; | |||
| padding: 15px; | |||
| &__center{ | |||
| position: absolute; | |||
| left: 50%; | |||
| top: 50%; | |||
| color:#1664ff; | |||
| font-size:14px; | |||
| transform: translate(-50%,-50%); | |||
| cursor: pointer; | |||
| } | |||
| &__status{ | |||
| color:#565658; | |||
| font-size:14px; | |||
| margin-right: 15px; | |||
| // width: 38px; | |||
| } | |||
| } | |||
| &__button{ | |||
| position: absolute; | |||
| color:#1664ff; | |||
| font-size:13px; | |||
| bottom: 20px; | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| } | |||
| &__bottomBox{ | |||
| padding: 23px 35px 23px 25px; | |||
| &__titleBox{ | |||
| display: flex; | |||
| justify-content: space-between; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 39px; | |||
| &__left{ | |||
| display: flex; | |||
| align-items: center; | |||
| font-weight:600; | |||
| color:#1d1d20; | |||
| font-size:16px; | |||
| &__word{ | |||
| display: flex; | |||
| align-items: center; | |||
| width:206px; | |||
| height:27px; | |||
| margin-left: 14px; | |||
| background:rgba(22, 100, 255, 0.09); | |||
| border-radius:4px; | |||
| padding: 2px 20px 2px 16px; | |||
| color:#4c5360; | |||
| font-size:14px; | |||
| font-weight: normal; | |||
| &__circle{ | |||
| position: relative; | |||
| width:15px; | |||
| height:15px; | |||
| border-radius: 50%; | |||
| background:#ffffff; | |||
| box-shadow:0px 1px 3px rgba(0, 82, 217, 0.09); | |||
| margin-right: 13px; | |||
| &__inner{ | |||
| position: absolute; | |||
| left: 50%; | |||
| top: 50%; | |||
| transform: translate(-50%,-50%); | |||
| width:9px; | |||
| height:9px; | |||
| border-radius: 50%; | |||
| background:#1664ff; | |||
| box-shadow:0px 1px 3px rgba(22, 100, 255, 0.27); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__right{ | |||
| display: flex; | |||
| align-items: center; | |||
| // width:647px; | |||
| height:39px; | |||
| background:rgba(138, 138, 138, 0.05); | |||
| border-radius:4px; | |||
| padding-left: 30px; | |||
| } | |||
| } | |||
| &__queryparams{ | |||
| margin: 17px 0; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,632 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| import { | |||
| deleteKnowledgeGraphVersion, | |||
| fullUpdate, | |||
| getKnowledgeGraphById, | |||
| getKnowledgeGraphVersionList, | |||
| incrementalUpdate, | |||
| rollback, | |||
| } from '@/services/graph'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { | |||
| App, | |||
| Button, | |||
| Input, | |||
| Select, | |||
| Table, | |||
| type TablePaginationConfig, | |||
| type TableProps, | |||
| Tabs, | |||
| } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import AddCodeConfigModal, { OperationType } from '../components/AddUpdateModal'; | |||
| // import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; | |||
| import styles from './index.less'; | |||
| export type CodeConfigData = { | |||
| id: number; | |||
| code_repo_name: string; | |||
| code_repo_vis: number; | |||
| git_url: string; | |||
| git_branch: string; | |||
| git_user_name: string; | |||
| git_password: string; | |||
| ssh_key: string; | |||
| verify_mode: number; | |||
| create_by: string; | |||
| create_time: string; | |||
| update_by: string; | |||
| update_time: string; | |||
| }; | |||
| export type topData = { | |||
| id: number; //主键 | |||
| name: string; //名称 | |||
| status: number; //状态,0代表不可用,1代表可用 | |||
| spec: string; //图谱规格 | |||
| createTime: string; //创建时间 | |||
| updateTime: string; //最后更新时间 | |||
| relationCount: number; //关系数 | |||
| entityCount: number; //实体数 | |||
| }; | |||
| export type EditorData = { | |||
| id: number; | |||
| name: string; | |||
| status: string; | |||
| computing_resource: string; | |||
| update_by: string; | |||
| create_time: string; | |||
| url: string; | |||
| }; | |||
| function DatasetAnnotation() { | |||
| const [dataList, setDataList] = useState<CodeConfigData[] | undefined>(undefined); | |||
| const [tableData, setTableData] = useState<CodeConfigData[] | undefined>(undefined); | |||
| const [topData, setTopData] = useState<topData>([]); | |||
| const [cacheState, setCacheState] = useCacheState(); | |||
| const [searchParams] = useSearchParams(); | |||
| const [activeTab, setActiveTab] = useState<string>('1'); | |||
| const navigate = useNavigate(); | |||
| const { message } = App.useApp(); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| const updateMethodList = ['待更新', '增量更新', '全量更新']; | |||
| const stateList = ['离线', '在线']; | |||
| const graphId = searchParams.get('id') || ''; | |||
| const graphVersion = searchParams.get('version') || ''; | |||
| const handleTableChange: TableProps<EditorData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| }; | |||
| const clickRouter = (val: string) => { | |||
| navigate(`/knowledge/graphCounter?tabActive=${val}`); | |||
| }; | |||
| const getList = () => { | |||
| const params = { | |||
| id: graphId, | |||
| version: graphVersion, | |||
| }; | |||
| getKnowledgeGraphById(params).then((ret) => { | |||
| console.log(ret); | |||
| setTopData(ret.data); | |||
| }); | |||
| }; | |||
| const getTableList = () => { | |||
| const params = { | |||
| id: graphId, | |||
| }; | |||
| getKnowledgeGraphVersionList(params).then((ret) => { | |||
| console.log(ret); | |||
| setTableData(ret.rows); | |||
| setTotal(ret.total); | |||
| }); | |||
| }; | |||
| const incrementalUpdates = (row) => { | |||
| incrementalUpdate({ | |||
| id: row.id, | |||
| }).then((ret) => { | |||
| if (ret.code == 200) { | |||
| getTableList(); | |||
| } | |||
| }); | |||
| }; | |||
| const fullUpdates = (row) => { | |||
| fullUpdate({ | |||
| id: row.id, | |||
| }).then((ret) => { | |||
| if (ret.code == 200) { | |||
| getTableList(); | |||
| } | |||
| }); | |||
| }; | |||
| const singleRollback = (row) => { | |||
| rollback({ | |||
| id: row.id, | |||
| }).then((ret) => { | |||
| if (ret.code == 200) { | |||
| getTableList(); | |||
| } | |||
| }); | |||
| }; | |||
| const [total, setTotal] = useState(0); | |||
| useEffect(() => { | |||
| getList(); | |||
| getTableList(); | |||
| }, []); | |||
| const items = [ | |||
| { | |||
| key: '1', | |||
| label: `图谱版本`, | |||
| icon: <KFIcon type="icon-moxingjianjie" />, | |||
| children: null, | |||
| }, | |||
| // { | |||
| // key: 2, | |||
| // label: `更新日志`, | |||
| // icon: <KFIcon type="icon-moxingwenjian" />, | |||
| // children: null, | |||
| // }, | |||
| ]; | |||
| const addCodeConfig = () => { | |||
| const { close } = openAntdModal(AddCodeConfigModal, { | |||
| opType: OperationType.Create, | |||
| codeConfigData: { | |||
| id: searchParams.get('id') || '', | |||
| }, | |||
| onOk: () => { | |||
| getTableList(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| const handleVersionDelete = (row) => { | |||
| modalConfirm({ | |||
| title: '确定要删除该版本吗?', | |||
| content: '删除后版本将不可恢复,是否确认删除?', | |||
| onOk: () => { | |||
| deleteKnowledgeGraphVersion(row.id).then((res) => { | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||
| // 否则直接刷新这一页的数据 | |||
| // 避免回到第一页 | |||
| if (tableData.length > 1) { | |||
| setPagination((prev) => ({ | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| getTableList(); | |||
| } else { | |||
| getTableList(); | |||
| } | |||
| } | |||
| }); | |||
| }, | |||
| }); | |||
| }; | |||
| const columnsNow: TableProps<EditorData>['columns'] = [ | |||
| { | |||
| title: '版本名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '在线状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: '10%', | |||
| render: (_: any, record: EditorData) => <div>{stateList[record.status]}</div>, | |||
| }, | |||
| { | |||
| title: '更新方式', | |||
| dataIndex: 'updateMethod', | |||
| key: 'updateMethod', | |||
| width: '10%', | |||
| render: (_: any, record: EditorData) => <div>{updateMethodList[record.updateMethod]}</div>, | |||
| }, | |||
| { | |||
| title: '实体个数', | |||
| dataIndex: 'entityCount', | |||
| key: 'entityCount', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '关系个数', | |||
| dataIndex: 'relationCount', | |||
| key: 'relationCount', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'createTime', | |||
| key: 'createTime', | |||
| width: '20%', | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '描述', | |||
| dataIndex: 'remark', | |||
| key: 'remark', | |||
| width: '20%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: '30%', | |||
| key: 'operation', | |||
| render: (_: any, record: EditorData) => ( | |||
| <div> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="jingxiang" | |||
| disabled={record.updateMethod != 0} | |||
| icon={<KFIcon type="icon-chakanxiangqing" />} | |||
| onClick={() => incrementalUpdates(record)} | |||
| > | |||
| 增量更新 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="jingxiang" | |||
| disabled={record.updateMethod != 0} | |||
| icon={<KFIcon type="icon-chakanxiangqing" />} | |||
| onClick={() => fullUpdates(record)} | |||
| > | |||
| 全量更新 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="jingxiang" | |||
| disabled={!(record.status == 0 && record.updateMethod != 0)} | |||
| icon={<KFIcon type="icon-bianji" />} | |||
| onClick={() => singleRollback(record)} | |||
| > | |||
| 回退 | |||
| </Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="remove" | |||
| disabled={record.status !== 0} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => handleVersionDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| {/* <ConfigProvider | |||
| theme={{ | |||
| token: { | |||
| colorLink: themes['warningColor'], | |||
| }, | |||
| }} | |||
| > | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="remove" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => handleEditorDelete(record)} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| </ConfigProvider> */} | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <div className={styles['onlineGraph-page']}> | |||
| <div className={styles['onlineGraph-page__container']}> | |||
| <span>{topData.name}</span> | |||
| <div className={styles['onlineGraph-page__container__box']}> | |||
| <div | |||
| onClick={addCodeConfig} | |||
| className={styles['onlineGraph-page__container__box__update']} | |||
| > | |||
| 导入 | |||
| </div> | |||
| {/* <div | |||
| onClick={allCodeConfig} | |||
| className={styles['onlineGraph-page__container__box__update']} | |||
| > | |||
| 全量更新 | |||
| </div> */} | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__topCenter']}> | |||
| <div className={styles['onlineGraph-page__title']}>在线版本图谱</div> | |||
| <div className={styles['onlineGraph-page__listBox']}> | |||
| {/* {dataList && dataList.length !== 0 && ( */} | |||
| <> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox']}> | |||
| <img | |||
| className={styles['onlineGraph-page__listBox__itemBox__img']} | |||
| src={require('@/assets/img/runningstatus.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}> | |||
| {topData.status == '1' ? '可用' : '不可用'} | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}> | |||
| 运行状态 | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox']}> | |||
| <img | |||
| className={styles['onlineGraph-page__listBox__itemBox__img']} | |||
| src={require('@/assets/img/runningstatus.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}> | |||
| {topData.entityCount} | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}> | |||
| 实体个数(个) | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox']}> | |||
| <img | |||
| className={styles['onlineGraph-page__listBox__itemBox__img']} | |||
| src={require('@/assets/img/runningstatus.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}> | |||
| {topData.relationCount} | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}> | |||
| 关系个数(个) | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox']}> | |||
| <img | |||
| className={styles['onlineGraph-page__listBox__itemBox__img']} | |||
| src={require('@/assets/img/runningstatus.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}> | |||
| {formatDate(topData.updateTime, 'YYYY-MM-DD')} | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}> | |||
| 最后更新时间 | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox']}> | |||
| <img | |||
| className={styles['onlineGraph-page__listBox__itemBox__img']} | |||
| src={require('@/assets/img/runningstatus.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordTitle']}> | |||
| {formatDate(topData.createTime, 'YYYY-MM-DD')} | |||
| </div> | |||
| <div className={styles['onlineGraph-page__listBox__itemBox__wordContent']}> | |||
| 创建时间 | |||
| </div> | |||
| </div> | |||
| </> | |||
| {/* )} */} | |||
| </div> | |||
| <div className={styles['onlineGraph-page__title']}>在线版本图谱</div> | |||
| <div className={styles['onlineGraph-page__centerListBox']}> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox']}> | |||
| <img | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__img']} | |||
| src={require('@/assets/img/graph-star.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox__title']}> | |||
| 知识图谱预览查询 | |||
| </div> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox__content']}> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox__content__center']}> | |||
| 预览图谱 | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox']}> | |||
| <img | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__img']} | |||
| src={require('@/assets/img/graph-star.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox__title']}> | |||
| 知识图谱问答KBQA | |||
| </div> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox__content']}> | |||
| <div style={{ marginBottom: '12px' }}> | |||
| <span | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__content__status']} | |||
| > | |||
| 状态 | |||
| </span> | |||
| <span style={{ color: '#6ac21d', fontSize: '14px' }}>已开通</span> | |||
| </div> | |||
| <div style={{ display: 'flex', alignItems: 'start' }}> | |||
| <div | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__content__status']} | |||
| > | |||
| 状态 | |||
| </div> | |||
| <div style={{ color: '#1d1d20', fontSize: '14px', flex: 1 }}> | |||
| 利用NLP能力理解用户输入的不同类型问题、找到并返回知识图谱中相关的答案。 | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div | |||
| onClick={() => { | |||
| clickRouter('3'); | |||
| }} | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__button']} | |||
| style={{ right: '20px' }} | |||
| > | |||
| 接口信息 | |||
| </div> | |||
| <div | |||
| onClick={() => { | |||
| clickRouter('2'); | |||
| }} | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__button']} | |||
| style={{ right: '101px' }} | |||
| > | |||
| 问答体验 | |||
| </div> | |||
| <div | |||
| onClick={() => { | |||
| clickRouter('1'); | |||
| }} | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__button']} | |||
| style={{ right: '182px' }} | |||
| > | |||
| 问答配置 | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox']}> | |||
| <img | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__img']} | |||
| src={require('@/assets/img/graph-star.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox__title']}> | |||
| 实体链接 | |||
| </div> | |||
| <div className={styles['onlineGraph-page__centerListBox__itemBox__content']}> | |||
| <div style={{ marginBottom: '12px' }}> | |||
| <span | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__content__status']} | |||
| > | |||
| 状态 | |||
| </span> | |||
| <span style={{ color: '#6ac21d', fontSize: '14px' }}>已开通</span> | |||
| </div> | |||
| <div style={{ display: 'flex', alignItems: 'start' }}> | |||
| <div | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__content__status']} | |||
| > | |||
| 状态 | |||
| </div> | |||
| <span style={{ color: '#1d1d20', fontSize: '14px', flex: 1 }}> | |||
| 识别句子中出现的知识图谱中的实体,并返回实体相关信息。 | |||
| </span> | |||
| </div> | |||
| </div> | |||
| <div | |||
| className={styles['onlineGraph-page__centerListBox__itemBox__button']} | |||
| style={{ right: '20px' }} | |||
| > | |||
| 接口信息 | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__bottomBox']}> | |||
| <div className={styles['onlineGraph-page__bottomBox__titleBox']}> | |||
| <div className={styles['onlineGraph-page__bottomBox__titleBox__left']}> | |||
| <span>版本统计</span> | |||
| <div className={styles['onlineGraph-page__bottomBox__titleBox__left__word']}> | |||
| <div | |||
| className={styles['onlineGraph-page__bottomBox__titleBox__left__word__circle']} | |||
| > | |||
| <div | |||
| className={ | |||
| styles['onlineGraph-page__bottomBox__titleBox__left__word__circle__inner'] | |||
| } | |||
| ></div> | |||
| </div> | |||
| 您还可以创建<span style={{ color: '#1664ff', fontSize: '16px' }}>1</span>个版本{' '} | |||
| </div> | |||
| </div> | |||
| <div className={styles['onlineGraph-page__bottomBox__titleBox__right']}> | |||
| <div> | |||
| <span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}> | |||
| 已上线 | |||
| </span> | |||
| <span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '61px' }}>1</span> | |||
| </div> | |||
| <div> | |||
| <span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}> | |||
| 未上线 | |||
| </span> | |||
| <span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '61px' }}>1</span> | |||
| </div> | |||
| <div> | |||
| <span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}> | |||
| 增量更新 | |||
| </span> | |||
| <span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '61px' }}>1</span> | |||
| </div> | |||
| <div> | |||
| <span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}> | |||
| 全量更新 | |||
| </span> | |||
| <span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '61px' }}>1</span> | |||
| </div> | |||
| <div> | |||
| <span style={{ color: '#565658', fontSize: '14px', marginRight: '10px' }}> | |||
| 发布次数 | |||
| </span> | |||
| <span style={{ color: '#1d1d20', fontSize: '15px', marginRight: '20px' }}>1</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||
| <div className={styles['onlineGraph-page__bottomBox__queryparams']}> | |||
| <Input.Search | |||
| placeholder="按镜像名称筛选" | |||
| allowClear | |||
| // onSearch={onSearch} | |||
| // onChange={(e) => setInputText(e.target.value)} | |||
| style={{ width: 300 }} | |||
| // value={inputText} | |||
| /> | |||
| <Select | |||
| allowClear | |||
| style={{ width: '106px', margin: '0px 30px 0 20px' }} | |||
| placeholder="请选择数据集分类" | |||
| options={[]} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| optionFilterProp="name" | |||
| showSearch | |||
| /> | |||
| <Button | |||
| key="primary" | |||
| type="primary" | |||
| // onClick={() => toDetail(record)} | |||
| > | |||
| 查询 | |||
| </Button> | |||
| </div> | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columnsNow} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default DatasetAnnotation; | |||
| @@ -0,0 +1,976 @@ | |||
| import colorB from '@/assets/img/tupu/b-circle.png'; | |||
| import colorC from '@/assets/img/tupu/c-circle.png'; | |||
| import { useStateRef, useVisible } from '@/hooks'; | |||
| import { editKnowledgeSubject, getKnowledgeSubjectById } from '@/services/subject'; | |||
| import { fittingString, parseJsonText, s8 } from '@/utils'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import G6 from '@antv/g6'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { App, Button } from 'antd'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import GlobalParamsDrawer from '../components/GlobalParamsDrawer'; | |||
| import AddSubjectConfigModal, { OperationType } from '../components/subjecDetailEdit'; | |||
| import PropsEdge from '../components/SubjectEdgeDrawer'; | |||
| import Props from '../components/SubjectNodeDrawer'; | |||
| import styles from './index.less'; | |||
| import { findAllParentNodes } from './utils'; | |||
| let graph = null; | |||
| const EditPipeline = () => { | |||
| const navigate = useNavigate(); | |||
| const locationParams = useParams(); //新版本获取路由参数接口 | |||
| console.log(locationParams); | |||
| const graphRef = useRef(); | |||
| const paramsDrawerRef = useRef(); | |||
| const edgeDrawerRef = useRef(); | |||
| const propsRef = useRef(); | |||
| const propsEdgeRef = useRef(); | |||
| const [paramsDrawerOpen, openParamsDrawer, closeParamsDrawer] = useVisible(false); | |||
| const [edgeDrawerOpen, edgeParamsDrawer, closeEdgeDrawer] = useVisible(false); | |||
| const [globalParam, setGlobalParam, globalParamRef] = useStateRef([]); | |||
| const { message } = App.useApp(); | |||
| const [contextFlag, setContextFlag] = useState(false); | |||
| const [subjectName, setSubjectName] = useState(''); | |||
| const colorList = [colorB, colorC]; | |||
| const [subjectDesrc, setSubjectDesrc] = useState(''); | |||
| let sourceAnchorIdx, targetAnchorIdx, dropAnchorIdx; | |||
| let dragSourceNode; | |||
| useEffect(() => { | |||
| initGraph(); | |||
| getFirstWorkflow(locationParams.id); | |||
| console.log(locationParams); | |||
| const changeSize = () => { | |||
| if (!graph || graph.get('destroyed')) return; | |||
| if (!graphRef.current) return; | |||
| graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); | |||
| graph.fitView(); | |||
| }; | |||
| window.addEventListener('resize', changeSize); | |||
| return () => { | |||
| window.removeEventListener('resize', changeSize); | |||
| }; | |||
| }, []); | |||
| // 拖拽结束,添加新节点 | |||
| const onDragEnd = (val) => { | |||
| const { x, y } = val; | |||
| const point = graph.getPointByClient(x, y); | |||
| // 元模型 | |||
| const model = { | |||
| ...val, | |||
| x: point.x, | |||
| y: point.y, | |||
| id: val.component_name + '-' + s8(), | |||
| isCluster: false, | |||
| formError: true, | |||
| }; | |||
| // console.log('model', model); | |||
| graph.addItem('node', model, false); | |||
| }; | |||
| const updateCodeConfig = () => { | |||
| getKnowledgeSubjectById(locationParams.id).then((ret) => { | |||
| console.log(ret); | |||
| if (ret.code == 200) { | |||
| const { close } = openAntdModal(AddSubjectConfigModal, { | |||
| opType: OperationType.Update, | |||
| codeConfigData: { | |||
| name: ret.data.name, | |||
| description: ret.data.description, | |||
| id: ret.data.id, | |||
| }, | |||
| onOk: () => { | |||
| // getList(); | |||
| getKnowledgeSubjectById(locationParams.id).then((res) => { | |||
| getFirstWorkflow(locationParams.id); | |||
| close(); | |||
| }); | |||
| }, | |||
| }); | |||
| } | |||
| }); | |||
| }; | |||
| // 节点数据发生变化 | |||
| const handleFormEdgeChange = (val) => { | |||
| console.log(val); | |||
| if (graph) { | |||
| const data = graph.save(); | |||
| const index = data.edges.findIndex((item) => { | |||
| return item.id === val.id; | |||
| }); | |||
| data.edges[index].label = val.label; | |||
| const zoom = graph.getZoom(); | |||
| // 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置 | |||
| const lastPoint = graph.getCanvasByPoint(0, 0); | |||
| graph.changeData(data); | |||
| graph.render(); | |||
| graph.zoomTo(zoom); | |||
| // 获取重新渲染之后点(0, 0)在画布的位置 | |||
| const newPoint = graph.getCanvasByPoint(0, 0); | |||
| // 移动画布相对位移; | |||
| graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y); | |||
| } | |||
| }; | |||
| const handleFormChange = (val) => { | |||
| console.log(val); | |||
| if (graph) { | |||
| const data = graph.save(); | |||
| const index = data.nodes.findIndex((item) => { | |||
| return item.id === val.id; | |||
| }); | |||
| data.nodes[index] = val; | |||
| const zoom = graph.getZoom(); | |||
| // 在拉取新数据重新渲染页面之前先获取点(0, 0)在画布上的位置 | |||
| const lastPoint = graph.getCanvasByPoint(0, 0); | |||
| graph.changeData(data); | |||
| graph.render(); | |||
| graph.zoomTo(zoom); | |||
| // 获取重新渲染之后点(0, 0)在画布的位置 | |||
| const newPoint = graph.getCanvasByPoint(0, 0); | |||
| // 移动画布相对位移; | |||
| graph.translate(lastPoint.x - newPoint.x, lastPoint.y - newPoint.y); | |||
| } | |||
| }; | |||
| // 保存 | |||
| const savePipeline = async (val) => { | |||
| // const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields()); | |||
| // if (globalParamError) { | |||
| // message.error('全局参数配置有误'); | |||
| // openParamsDrawer(); | |||
| // return; | |||
| // } | |||
| // closeParamsDrawer(); | |||
| // const [propsRes, propsError] = await to(propsRef.current.validateFields()); | |||
| // if (propsError) { | |||
| // message.error('节点必填项必须配置'); | |||
| // return; | |||
| // } | |||
| // propsRef.current.close(); | |||
| // setTimeout(() => { | |||
| // const data = graph.save(); | |||
| // // console.log(data); | |||
| // const errorNode = data.nodes.find((item) => item.formError === true); | |||
| // if (errorNode) { | |||
| // message.error(`【${errorNode.label}】节点必填项必须配置`); | |||
| // const graphNode = graph.findById(errorNode.id); | |||
| // if (graphNode) { | |||
| // openNodeDrawer(graphNode, true); | |||
| // } | |||
| // return; | |||
| // } | |||
| const data = graph.save(); | |||
| console.log(data); | |||
| const params = { | |||
| id: locationParams.id / 1, | |||
| name: subjectName, | |||
| description: subjectDesrc, | |||
| dag: JSON.stringify(data), | |||
| // global_param: JSON.stringify(globalParamRes.global_param), | |||
| }; | |||
| editKnowledgeSubject(params).then((ret) => { | |||
| message.success('保存成功'); | |||
| // setTimeout(() => { | |||
| // if (val) { | |||
| // navigate({ pathname: `/pipeline/template` }); | |||
| // } | |||
| // }, 500); | |||
| }); | |||
| // }, 500); | |||
| }; | |||
| // 渲染数据 | |||
| const getGraphData = (data) => { | |||
| if (graph && data) { | |||
| graph.data(data); | |||
| graph.render(); | |||
| } else { | |||
| setTimeout(() => { | |||
| getGraphData(data); | |||
| }, 500); | |||
| } | |||
| }; | |||
| // 处理并行边,暂时没有用 | |||
| const processParallelEdgesOnAnchorPoint = ( | |||
| edges, | |||
| offsetDiff = 15, | |||
| multiEdgeType = 'cubic-vertical', | |||
| singleEdgeType = undefined, | |||
| loopEdgeType = undefined, | |||
| ) => { | |||
| const len = edges.length; | |||
| const cod = offsetDiff * 2; | |||
| const loopPosition = [ | |||
| 'top', | |||
| 'top-right', | |||
| 'right', | |||
| 'bottom-right', | |||
| 'bottom', | |||
| 'bottom-left', | |||
| 'left', | |||
| 'top-left', | |||
| ]; | |||
| const edgeMap = {}; | |||
| const tags = []; | |||
| const reverses = {}; | |||
| for (let i = 0; i < len; i++) { | |||
| const edge = edges[i]; | |||
| const { source, target, sourceAnchor, targetAnchor } = edge; | |||
| const sourceTarget = `${source}|${sourceAnchor}-${target}|${targetAnchor}`; | |||
| if (tags[i]) continue; | |||
| if (!edgeMap[sourceTarget]) { | |||
| edgeMap[sourceTarget] = []; | |||
| } | |||
| tags[i] = true; | |||
| edgeMap[sourceTarget].push(edge); | |||
| for (let j = 0; j < len; j++) { | |||
| if (i === j) continue; | |||
| const sedge = edges[j]; | |||
| const { | |||
| source: src, | |||
| target: dst, | |||
| sourceAnchor: srcAnchor, | |||
| targetAnchor: dstAnchor, | |||
| } = sedge; | |||
| // 两个节点之间共同的边 | |||
| // 第一条的source = 第二条的target | |||
| // 第一条的target = 第二条的source | |||
| if (!tags[j]) { | |||
| if ( | |||
| source === dst && | |||
| sourceAnchor === dstAnchor && | |||
| target === src && | |||
| targetAnchor === srcAnchor | |||
| ) { | |||
| edgeMap[sourceTarget].push(sedge); | |||
| tags[j] = true; | |||
| reverses[ | |||
| `${src}|${srcAnchor}|${dst}|${dstAnchor}|${edgeMap[sourceTarget].length - 1}` | |||
| ] = true; | |||
| } else if ( | |||
| source === src && | |||
| sourceAnchor === srcAnchor && | |||
| target === dst && | |||
| targetAnchor === dstAnchor | |||
| ) { | |||
| edgeMap[sourceTarget].push(sedge); | |||
| tags[j] = true; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // eslint-disable-next-line | |||
| for (const key in edgeMap) { | |||
| const arcEdges = edgeMap[key]; | |||
| const { length } = arcEdges; | |||
| for (let k = 0; k < length; k++) { | |||
| const current = arcEdges[k]; | |||
| if (current.source === current.target) { | |||
| if (loopEdgeType) current.type = loopEdgeType; | |||
| // 超过8条自环边,则需要重新处理 | |||
| current.loopCfg = { | |||
| position: loopPosition[k % 8], | |||
| dist: Math.floor(k / 8) * 20 + 50, | |||
| }; | |||
| continue; | |||
| } | |||
| if ( | |||
| length === 1 && | |||
| singleEdgeType && | |||
| (current.source !== current.target || current.sourceAnchor !== current.targetAnchor) | |||
| ) { | |||
| current.type = singleEdgeType; | |||
| continue; | |||
| } | |||
| current.type = multiEdgeType; | |||
| const sign = | |||
| (k % 2 === 0 ? 1 : -1) * | |||
| (reverses[ | |||
| `${current.source}|${current.sourceAnchor}|${current.target}|${current.targetAnchor}|${k}` | |||
| ] | |||
| ? -1 | |||
| : 1); | |||
| if (length % 2 === 1) { | |||
| current.curveOffset = sign * Math.ceil(k / 2) * cod; | |||
| } else { | |||
| current.curveOffset = sign * (Math.floor(k / 2) * cod + offsetDiff); | |||
| } | |||
| } | |||
| } | |||
| return edges; | |||
| }; | |||
| // 判断两个节点之间是否有边 | |||
| const hasEdge = (source, target) => { | |||
| const neighbors = source.getNeighbors(); | |||
| for (const node of neighbors) { | |||
| // 新建边的时候,获取的 neighbors 的数据有问题,不全是 INode 类型,可能没有 getID 方法 | |||
| if (node.getID?.() === target.getID?.()) return true; | |||
| } | |||
| return false; | |||
| }; | |||
| // 复制节点 | |||
| const cloneElement = (item) => { | |||
| let data = graph.save(); | |||
| const nodeId = s8(); | |||
| data.nodes.push({ | |||
| ...item.getModel(), | |||
| label: item.getModel().label + '-copy', | |||
| x: item.getModel().x + 150, | |||
| y: item.getModel().y, | |||
| id: item.getModel().component_name + '-' + nodeId, | |||
| }); | |||
| graph.changeData(data); | |||
| }; | |||
| const addElement = () => { | |||
| let data = graph.save(); | |||
| const nodeId = s8(); | |||
| data.nodes.push({ | |||
| ...item.getModel(), | |||
| label: item.getModel().label, | |||
| x: item.getModel().x + 150, | |||
| y: item.getModel().y, | |||
| id: item.getModel().component_name + '-' + nodeId, | |||
| }); | |||
| graph.changeData(data); | |||
| }; | |||
| // 获取流水线详情 | |||
| const getFirstWorkflow = async (val) => { | |||
| getKnowledgeSubjectById(val).then((res) => { | |||
| if (res && res.data) { | |||
| setSubjectName(res.data.name); | |||
| setSubjectDesrc(res.data.description); | |||
| const { dag } = res.data; | |||
| if (dag) { | |||
| getGraphData(parseJsonText(dag)); | |||
| } | |||
| } | |||
| }); | |||
| // if (res && res.data) { | |||
| // const { global_param, dag } = res.data; | |||
| // setGlobalParam(global_param || []); | |||
| // if (dag) { | |||
| // getGraphData(parseJsonText(dag)); | |||
| // } | |||
| // } | |||
| }; | |||
| // 打开节点抽屉 | |||
| const openNodeDrawer = (node, validate = false) => { | |||
| console.log(node); | |||
| // 获取所有的上游节点 | |||
| const parentNodes = findAllParentNodes(graph, node); | |||
| // 如果没有打开过全局参数抽屉,获取不到全局参数 | |||
| const globalParams = | |||
| paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; | |||
| // 打开节点编辑抽屉 | |||
| propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate); | |||
| }; | |||
| // 打开边抽屉 | |||
| const openEdgeDrawer = (edge, validate = false) => { | |||
| // // 获取所有的上游节点 | |||
| // const parentNodes = findAllParentNodes(graph, node); | |||
| // // 如果没有打开过全局参数抽屉,获取不到全局参数 | |||
| // const globalParams = | |||
| // edgeDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; | |||
| // 打开节点编辑抽屉 | |||
| console.log(edge); | |||
| propsEdgeRef.current.showDrawer(edge.getModel(), validate); | |||
| }; | |||
| // 初始化图 | |||
| const initGraph = () => { | |||
| // const contextMenu = initMenu(); | |||
| G6.registerNode( | |||
| 'rect-node', | |||
| { | |||
| // draw anchor-point circles according to the anchorPoints in afterDraw | |||
| getAnchorPoints(cfg) { | |||
| return ( | |||
| cfg.anchorPoints || [ | |||
| // 四个,上下左右 | |||
| [0.5, 0.4], | |||
| // [0.5, 1], | |||
| // [0, 0.5], | |||
| // [1, 0.5], | |||
| ] | |||
| ); | |||
| }, | |||
| afterDraw(cfg, group) { | |||
| group.addShape('image', { | |||
| name: 'custom-image', | |||
| attrs: { | |||
| x: -20, | |||
| y: -20, | |||
| width: 40, | |||
| height: 40, | |||
| img: cfg.color ? colorList[cfg.color - 1] : colorC, | |||
| cursor: 'pointer', | |||
| stroke: '#fff', | |||
| fill: 'transparent', | |||
| }, | |||
| draggable: true, | |||
| }); | |||
| if (cfg.label) { | |||
| group.addShape('text', { | |||
| attrs: { | |||
| text: fittingString(cfg.label, 70, 10), | |||
| x: 0, | |||
| y: 30, | |||
| fontSize: 10, | |||
| textAlign: 'center', | |||
| textBaseline: 'middle', | |||
| fill: '#000', | |||
| cursor: 'pointer', | |||
| }, | |||
| name: 'text-shape', | |||
| draggable: true, | |||
| }); | |||
| } | |||
| // if (cfg.formError) { | |||
| // group.addShape('image', { | |||
| // attrs: { | |||
| // x: 43, | |||
| // y: -24, | |||
| // width: 18, | |||
| // height: 18, | |||
| // img: require('@/assets/img/pipeline-warning.png'), | |||
| // cursor: 'pointer', | |||
| // }, | |||
| // draggable: false, | |||
| // capture: false, | |||
| // }); | |||
| // } | |||
| const bbox = group.getBBox(); | |||
| // if (cfg.formError) { | |||
| // bbox.y += 6; | |||
| // bbox.width -= 6; | |||
| // bbox.height -= 6; | |||
| // } | |||
| const anchorPoints = this.getAnchorPoints(cfg); | |||
| anchorPoints.forEach((anchorPos, i) => { | |||
| group.addShape('circle', { | |||
| attrs: { | |||
| r: 3, | |||
| x: bbox.x + bbox.width * anchorPos[0], | |||
| y: bbox.y + bbox.height * anchorPos[1], | |||
| fill: '#fff', | |||
| stroke: '#a4a4a5', | |||
| cursor: 'crosshair', | |||
| lineWidth: 1, | |||
| }, | |||
| name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point') | |||
| anchorPointIdx: i, // flag the idx of the anchor-point circle | |||
| links: 0, // cache the number of edges connected to this shape | |||
| visible: false, // invisible by default, shows up when links > 1 or the node is in showAnchors state | |||
| draggable: true, | |||
| }); | |||
| }); | |||
| }, | |||
| // response the state changes and show/hide the link-point circles | |||
| setState(name, value, item) { | |||
| const group = item.getContainer(); | |||
| const shape = group.get('children')[0]; | |||
| shape.attr('stroke', 'transparent'); | |||
| const anchorPoints = group.findAll((item) => item.get('name') === 'anchor-point'); | |||
| if (name === 'hover') { | |||
| if (value) { | |||
| // shape.attr('stroke', themes['primaryColor']); | |||
| anchorPoints.forEach((point) => { | |||
| point.show(); | |||
| }); | |||
| } else { | |||
| shape.attr('stroke', 'transparent'); | |||
| anchorPoints.forEach((point) => { | |||
| point.hide(); | |||
| }); | |||
| } | |||
| } else if (name === 'drag') { | |||
| if (sourceAnchorIdx !== null && sourceAnchorIdx !== undefined) { | |||
| const anchorPoint = anchorPoints[sourceAnchorIdx]; | |||
| // anchorPoint.attr('stroke', value ? themes['primaryColor'] : '#a4a4a5'); | |||
| // anchorPoint.attr('lineWidth', value ? 2 : 1); | |||
| } | |||
| } else if (name === 'drop') { | |||
| if (dropAnchorIdx !== null && dropAnchorIdx !== undefined) { | |||
| const anchorPoint = anchorPoints[dropAnchorIdx]; | |||
| // anchorPoint.attr('stroke', value ? themes['primaryColor'] : '#a4a4a5'); | |||
| // anchorPoint.attr('lineWidth', value ? 2 : 1); | |||
| } | |||
| } | |||
| }, | |||
| }, | |||
| 'rect', | |||
| ); | |||
| graph = new G6.Graph({ | |||
| container: graphRef.current, | |||
| width: graphRef.current.clientWidth || 500, | |||
| height: graphRef.current.clientHeight || '100%', | |||
| animate: false, | |||
| groupByTypes: true, | |||
| fitView: true, | |||
| // plugins: [contextMenu], | |||
| enabledStack: false, | |||
| fitView: true, | |||
| minZoom: 0.5, | |||
| maxZoom: 5, | |||
| fitViewPadding: 200, | |||
| // behaviors: [ | |||
| // { | |||
| // type: 'create-edge', | |||
| // trigger: 'click', | |||
| // style: { | |||
| // fill: 'red', | |||
| // lineWidth: 2, | |||
| // }, | |||
| // }, | |||
| // ], | |||
| modes: { | |||
| default: [ | |||
| // config the shouldBegin for drag-node to avoid node moving while dragging on the anchor-point circles | |||
| { | |||
| type: 'drag-node', | |||
| shouldBegin: (e) => { | |||
| if (e.target.get('name') === 'anchor-point') return false; | |||
| return true; | |||
| }, | |||
| }, | |||
| // config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles | |||
| { | |||
| type: 'create-edge', | |||
| trigger: 'drag', | |||
| shouldBegin: (e) => { | |||
| // avoid beginning at other shapes on the node | |||
| if (e.target && e.target.get('name') !== 'anchor-point') return false; | |||
| sourceAnchorIdx = e.target.get('anchorPointIdx'); | |||
| e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle | |||
| dragSourceNode = e.item; | |||
| return true; | |||
| }, | |||
| shouldEnd: (e) => { | |||
| // avoid ending at other shapes on the node | |||
| // if (e.target && e.target.get('name') !== 'anchor-point') return false; | |||
| // if (!dragSourceNode || !e.item) return false; | |||
| // 不允许连接自己 | |||
| // if (dragSourceNode.getID() === e.item.getID()) return false; | |||
| // 两个节点不允许多条边 | |||
| if (hasEdge(dragSourceNode, e.item)) return false; | |||
| // if (e.target) { | |||
| // targetAnchorIdx = e.target.get('anchorPointIdx'); | |||
| // e.target.set('links', e.target.get('links') + 1); // cache the number of edge connected to this anchor-point circle | |||
| // return true; | |||
| // } | |||
| // targetAnchorIdx = undefined; | |||
| return true; | |||
| }, | |||
| }, | |||
| 'drag-canvas', | |||
| 'zoom-canvas', | |||
| ], | |||
| }, | |||
| defaultNode: { | |||
| type: 'rect-node', | |||
| size: [40, 40], | |||
| labelCfg: { | |||
| style: { | |||
| fill: 'transparent', | |||
| fontSize: 0, | |||
| }, | |||
| }, | |||
| style: { | |||
| fill: 'transparent', | |||
| lineWidth: 0, | |||
| }, | |||
| }, | |||
| defaultEdge: { | |||
| // type: 'polyline', | |||
| type: 'cubic', | |||
| style: { | |||
| endArrow: { | |||
| // 设置终点箭头 | |||
| path: G6.Arrow.triangle(3, 3, 3), // 使用内置箭头路径函数,参数为箭头的 宽度、长度、偏移量(默认为 0,与 d 对应) | |||
| d: 4.5, | |||
| fill: '#CDD0DC', | |||
| }, | |||
| cursor: 'pointer', | |||
| lineWidth: 1, | |||
| lineAppendWidth: 4, | |||
| opacity: 1, | |||
| stroke: '#CDD0DC', | |||
| radius: 1, | |||
| }, | |||
| labelCfg: { | |||
| autoRotate: true, | |||
| style: { | |||
| fontSize: 10, | |||
| fill: 'rgba(22, 100, 255, 0.5)', | |||
| }, | |||
| }, | |||
| }, | |||
| linkCenter: true, | |||
| }); | |||
| // 修改历史数据样式问题 | |||
| graph.node((node) => { | |||
| return { | |||
| style: { | |||
| stroke: 'transparent', | |||
| radius: 8, | |||
| }, | |||
| }; | |||
| }); | |||
| // 绑定事件 | |||
| bindEvents(); | |||
| }; | |||
| // 绑定事件 | |||
| const bindEvents = () => { | |||
| graph.on('node:click', (e) => { | |||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | |||
| openNodeDrawer(e.item); | |||
| } | |||
| }); | |||
| // 监听边的点击事件 | |||
| graph.on('edge:click', (e) => { | |||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | |||
| openEdgeDrawer(e.item); | |||
| } | |||
| }); | |||
| graph.on('aftercreateedge', (e) => { | |||
| // update the sourceAnchor and targetAnchor for the newly added edge | |||
| graph.updateItem(e.edge, { | |||
| sourceAnchor: sourceAnchorIdx, | |||
| targetAnchor: targetAnchorIdx, | |||
| }); | |||
| }); | |||
| // 删除边时,修改 anchor-point 的 links 值 | |||
| // graph.on('afterremoveitem', (e) => { | |||
| // if (e.item && e.item.source && e.item.target) { | |||
| // const { source, target, sourceAnchor, targetAnchor } = e.item; | |||
| // const sourceNode = graph.findById(source); | |||
| // const targetNode = graph.findById(target); | |||
| // if (sourceNode && !isNaN(sourceAnchor)) { | |||
| // const sourceAnchorShape = sourceNode | |||
| // .getContainer() | |||
| // .find( | |||
| // (ele) => | |||
| // ele.get('name') === 'anchor-point' && ele.get('anchorPointIdx') === sourceAnchor, | |||
| // ); | |||
| // sourceAnchorShape.set('links', sourceAnchorShape.get('links') - 1); | |||
| // } | |||
| // if (targetNode && !isNaN(targetAnchor)) { | |||
| // const targetAnchorShape = targetNode | |||
| // .getContainer() | |||
| // .find( | |||
| // (ele) => | |||
| // ele.get('name') === 'anchor-point' && ele.get('anchorPointIdx') === targetAnchor, | |||
| // ); | |||
| // // targetAnchorShape.set('links', targetAnchorShape.get('links') - 1); | |||
| // } | |||
| // } | |||
| // }); | |||
| // after drag on the first node, the edge is created, update the sourceAnchor | |||
| graph.on('afteradditem', (e) => { | |||
| const sourceAnchor = e.item.getModel().sourceAnchor; | |||
| if (e.item && e.item.getType() === 'edge' && !sourceAnchor) { | |||
| graph.updateItem(e.item, { | |||
| sourceAnchor: sourceAnchorIdx, | |||
| }); | |||
| } | |||
| }); | |||
| graph.on('node:mouseenter', (e) => { | |||
| graph.setItemState(e.item, 'hover', true); | |||
| }); | |||
| graph.on('node:mouseleave', (e) => { | |||
| graph.setItemState(e.item, 'hover', false); | |||
| }); | |||
| graph.on('node:dragstart', (e) => { | |||
| graph.setItemState(e.item, 'hover', true); | |||
| graph.setItemState(e.item, 'drag', true); | |||
| }); | |||
| graph.on('node:dragend', (e) => { | |||
| graph.setItemState(e.item, 'hover', false); | |||
| graph.setItemState(e.item, 'drag', false); | |||
| }); | |||
| graph.on('node:dragenter', (e) => { | |||
| if (e.item?.getID() === dragSourceNode?.getID()) return; | |||
| graph.setItemState(e.item, 'hover', true); | |||
| if (e.target.get('name') === 'anchor-point') { | |||
| dropAnchorIdx = e.target.get('anchorPointIdx'); | |||
| graph.setItemState(e.item, 'drop', true); | |||
| } else { | |||
| graph.setItemState(e.item, 'drop', false); | |||
| } | |||
| }); | |||
| graph.on('node:dragleave', (e) => { | |||
| if (e.item?.getID() === dragSourceNode?.getID()) return; | |||
| graph.setItemState(e.item, 'hover', false); | |||
| graph.setItemState(e.item, 'drop', false); | |||
| dropAnchorIdx = undefined; | |||
| }); | |||
| graph.on('node:drop', (e) => { | |||
| graph.setItemState(e.item, 'hover', false); | |||
| graph.setItemState(e.item, 'drop', false); | |||
| dropAnchorIdx = undefined; | |||
| }); | |||
| graph.on('contextmenu', (e) => { | |||
| e.preventDefault(); | |||
| console.log(e); | |||
| // 获取点击位置 | |||
| const point = graph.getPointByClient(e.clientX, e.clientY); | |||
| const contextMenu = document.getElementById('contextMenu'); | |||
| // 显示右键菜单 | |||
| contextMenu.style.display = 'block'; | |||
| contextMenu.style.left = `${e.clientX - 250}px`; | |||
| contextMenu.style.top = `${e.clientY}px`; | |||
| if (e.item) { | |||
| setContextFlag(true); | |||
| } else { | |||
| setContextFlag(false); | |||
| } | |||
| // 新增节点 | |||
| document.getElementById('addNode').onclick = () => { | |||
| const newNode = { | |||
| id: `node-${Date.now()}`, | |||
| label: '材料', | |||
| x: point.x, | |||
| y: point.y, | |||
| properties: [ | |||
| { | |||
| name: '', | |||
| isMultivalued: '', | |||
| type: '', | |||
| flag: false, | |||
| }, | |||
| ], | |||
| }; | |||
| graph.addItem('node', newNode); | |||
| graph.refresh(); | |||
| contextMenu.style.display = 'none'; | |||
| }; | |||
| document.getElementById('cloneNode').onclick = () => { | |||
| let data = graph.save(); | |||
| const nodeId = s8(); | |||
| data.nodes.push({ | |||
| ...e.item.getModel(), | |||
| label: e.item.getModel().label + '-copy', | |||
| x: e.item.getModel().x + 150, | |||
| y: e.item.getModel().y, | |||
| id: e.item.getModel().component_name + '-' + nodeId, | |||
| properties: e.item.getModel().properties || [ | |||
| { | |||
| name: '', | |||
| isMultivalued: '', | |||
| type: '', | |||
| flag: false, | |||
| }, | |||
| ], | |||
| }); | |||
| graph.changeData(data); | |||
| contextMenu.style.display = 'none'; | |||
| }; | |||
| document.getElementById('deleteNode').onclick = () => { | |||
| graph.removeItem(e.item); | |||
| contextMenu.style.display = 'none'; | |||
| }; | |||
| // 清空画布 | |||
| // document.getElementById('clearCanvas').onclick = () => { | |||
| // graph.clear(); | |||
| // contextMenu.style.display = 'none'; | |||
| // }; | |||
| // 点击其他地方隐藏菜单 | |||
| document.addEventListener('click', () => { | |||
| contextMenu.style.display = 'none'; | |||
| }); | |||
| }); | |||
| // 初始数据 | |||
| const data = { | |||
| nodes: [ | |||
| { id: 'node1', x: 100, y: 100, label: '节点1' }, | |||
| { id: 'node2', x: 300, y: 100, label: '节点2' }, | |||
| ], | |||
| edges: [{ source: 'node1', target: 'node2' }], | |||
| }; | |||
| }; | |||
| // 上下文菜单 | |||
| // const initMenu = () => { | |||
| // const contextMenu = new G6.Menu({ | |||
| // className: 'pipeline-context-menu', | |||
| // getContent(evt) { | |||
| // const type = evt.item.getType(); | |||
| // const cloneDisplay = type === 'node' ? 'flex' : 'none'; | |||
| // return ` | |||
| // <div> | |||
| // <div class="pipeline-context-menu__item" id="add"> | |||
| // <svg class="pipeline-context-menu__item__icon" id="clone-svg"> | |||
| // <use xlink:href="#icon-fuzhi1" /> | |||
| // </svg> | |||
| // 新建 | |||
| // </div> | |||
| // <div class="pipeline-context-menu__item" style="display: ${cloneDisplay}" id="clone"> | |||
| // <svg class="pipeline-context-menu__item__icon" id="clone-svg"> | |||
| // <use xlink:href="#icon-fuzhi1" /> | |||
| // </svg> | |||
| // 复制 | |||
| // </div> | |||
| // <div class="pipeline-context-menu__item" id="delete"> | |||
| // <svg class="pipeline-context-menu__item__icon" id="delete-svg"> | |||
| // <use xlink:href="#icon-shanchu1" /> | |||
| // </svg> | |||
| // 删除 | |||
| // </div> | |||
| // </div>`; | |||
| // }, | |||
| // handleMenuClick: (target, item) => { | |||
| // const id = target.id; | |||
| // if (id.startsWith('add')) { | |||
| // addElement(item); | |||
| // } | |||
| // if (id.startsWith('clone')) { | |||
| // cloneElement(item); | |||
| // } else if (id.startsWith('delete')) { | |||
| // graph.removeItem(item); | |||
| // } | |||
| // }, | |||
| // // offsetX and offsetY include the padding of the parent container | |||
| // // 需要加上父级容器的 padding-left 16 与自身偏移量 10 | |||
| // offsetX: 16 + 10, | |||
| // // 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10 | |||
| // offsetY: 0, | |||
| // // the types of items that allow the menu show up | |||
| // // 在哪些类型的元素上响应 | |||
| // itemTypes: ['node', 'edge', 'canvas'], | |||
| // }); | |||
| // return contextMenu; | |||
| // }; | |||
| return ( | |||
| <div className={styles['subjectView-container']}> | |||
| {/* <ModelMenu onComponentDragEnd={onDragEnd}></ModelMenu> */} | |||
| <div>{subjectName}</div> | |||
| <Button type="link" size="small" onClick={() => updateCodeConfig()}> | |||
| 修改主体 | |||
| </Button> | |||
| <div className={styles['subjectView-container__workflow']}> | |||
| <div className={styles['subjectView-container__workflow__top']}> | |||
| <Button type="link" size="small" onClick={() => savePipeline()}> | |||
| 保存 | |||
| </Button> | |||
| <span>图例:</span> | |||
| <div | |||
| className={styles['subjectView-container__workflow__top__circle']} | |||
| style={{ background: '#1664ff' }} | |||
| ></div> | |||
| <span style={{ color: '#1664ff', fontSize: '15px', marginRight: '20px' }}>XXXX</span> | |||
| <div | |||
| className={styles['subjectView-container__workflow__top__circle']} | |||
| style={{ background: '#63a728' }} | |||
| ></div> | |||
| <span style={{ color: '#63a728', fontSize: '15px' }}>XXXX</span> | |||
| {/* <Button | |||
| type="default" | |||
| icon={<KFIcon type="icon-quanjucanshu" />} | |||
| style={{ marginRight: '20px' }} | |||
| onClick={openParamsDrawer} | |||
| > | |||
| 全局参数 | |||
| </Button> | |||
| <Button | |||
| type="primary" | |||
| icon={<KFIcon type="icon-baocun" />} | |||
| style={{ marginRight: '20px' }} | |||
| onClick={() => { | |||
| savePipeline(false); | |||
| }} | |||
| > | |||
| 保存 | |||
| </Button> | |||
| <Button | |||
| type="primary" | |||
| style={{ | |||
| border: '1px solid', | |||
| borderColor: '#1664ff', | |||
| background: '#fff', | |||
| color: '#1664ff', | |||
| }} | |||
| icon={<KFIcon type="icon-baocunbingfanhui" />} | |||
| onClick={() => { | |||
| savePipeline(true); | |||
| }} | |||
| > | |||
| 保存并返回 | |||
| </Button> */} | |||
| </div> | |||
| <div className={styles['subjectView-container__workflow__graph']} ref={graphRef}></div> | |||
| </div> | |||
| <Props ref={propsRef} onFormChange={handleFormChange}></Props> | |||
| <PropsEdge ref={propsEdgeRef} onFormChange={handleFormEdgeChange}></PropsEdge> | |||
| <GlobalParamsDrawer | |||
| ref={paramsDrawerRef} | |||
| open={paramsDrawerOpen} | |||
| globalParam={globalParam} | |||
| onClose={closeParamsDrawer} | |||
| ></GlobalParamsDrawer> | |||
| {/* <EdgeParamsDrawer | |||
| ref={edgeDrawerRef} | |||
| open={edgeDrawerOpen} | |||
| globalParam={globalParam} | |||
| onClose={closeEdgeDrawer} | |||
| ></EdgeParamsDrawer> */} | |||
| <div className={styles['contextMenu']} id="contextMenu"> | |||
| <ul> | |||
| <li> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| disabled={contextFlag} | |||
| id="addNode" | |||
| // onClick={() => createMirror(record.id)} | |||
| > | |||
| 新增节点 | |||
| </Button> | |||
| </li> | |||
| <li id="cloneNode"> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| disabled={!contextFlag} | |||
| // onClick={() => createMirror(record.id)} | |||
| > | |||
| 复制节点 | |||
| </Button> | |||
| </li> | |||
| <li id="deleteNode"> | |||
| <Button | |||
| type="link" | |||
| disabled={!contextFlag} | |||
| size="small" | |||
| // onClick={() => createMirror(record.id)} | |||
| > | |||
| 删除节点 | |||
| </Button> | |||
| </li> | |||
| </ul> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default EditPipeline; | |||
| @@ -0,0 +1,93 @@ | |||
| .subjectView-container { | |||
| display: flex; | |||
| height: 100%; | |||
| background-color: #fff; | |||
| &__workflow { | |||
| flex: 1 1 0; | |||
| min-width: 0; | |||
| height: 100%; | |||
| &__top { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: end; | |||
| width: 100%; | |||
| height: 52px; | |||
| padding: 0 20px; | |||
| background: #ffffff; | |||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | |||
| &__circle{ | |||
| width:8px; | |||
| height:8px; | |||
| border-radius: 50%; | |||
| margin-right: 10px; | |||
| } | |||
| } | |||
| &__graph { | |||
| position: relative; | |||
| width: 100%; | |||
| height: calc(100% - 52px); | |||
| background-color: @background-color; | |||
| background-image: url(@/assets/img/pipeline-canvas-bg.png); | |||
| background-size: 100% 100%; | |||
| } | |||
| } | |||
| .contextMenu { | |||
| position: absolute; | |||
| background: #fff; | |||
| border: 1px solid #ccc; | |||
| box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); | |||
| padding: 10px 5px; | |||
| display: none; | |||
| border-radius: 10px; | |||
| } | |||
| .contextMenu ul { | |||
| list-style: none; | |||
| margin: 0; | |||
| padding: 0; | |||
| } | |||
| .contextMenu ul li { | |||
| padding: 5px 5px; | |||
| cursor: pointer; | |||
| } | |||
| .contextMenu ul li:hover { | |||
| background: #f0f0f0; | |||
| } | |||
| } | |||
| :global { | |||
| .pipeline-context-menu { | |||
| width: 78px; | |||
| padding: 10px 0; | |||
| background: #ffffff; | |||
| border-radius: 6px; | |||
| box-shadow: 0px 0px 6px rgba(40, 84, 168, 0.21); | |||
| &__item { | |||
| display: flex; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 34px; | |||
| padding-left: 12px; | |||
| color: @text-color-secondary; | |||
| font-size: 15px; | |||
| cursor: pointer; | |||
| &:hover { | |||
| color: #0d5ef8; | |||
| font-weight: 500; | |||
| background-color: .addAlpha(#8895a8, 0.11) []; | |||
| } | |||
| &__icon { | |||
| width: 1em; | |||
| height: 1em; | |||
| margin-right: 9px; | |||
| fill: currentColor; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,94 @@ | |||
| import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { Graph, INode } from '@antv/g6'; | |||
| import { type MenuProps } from 'antd'; | |||
| // 找到节点所以的上游节点 | |||
| export const findAllParentNodes = (graph: Graph, node: INode) => { | |||
| const parentNodes: INode[] = []; | |||
| let index = -1; | |||
| let targetNode = node; | |||
| while (targetNode) { | |||
| const neighbors: INode[] = graph.getNeighbors(targetNode, 'source'); | |||
| for (const sourceNode of neighbors) { | |||
| // 避免重复,也避免循环 | |||
| const idx = parentNodes.findIndex((item) => sourceNode.getID() === item.getID()); | |||
| if (idx === -1 && sourceNode.getID() !== node.getID()) { | |||
| parentNodes.push(sourceNode); | |||
| } | |||
| } | |||
| targetNode = parentNodes[++index]; | |||
| } | |||
| return parentNodes; | |||
| }; | |||
| // 判断并找到全局参数第一个重复项,有重复项时,全局参数不允许保存 | |||
| export function findFirstDuplicate(params: PipelineGlobalParam[]): string | null { | |||
| const seen = new Set(); | |||
| for (const item of params) { | |||
| if (seen.has(item.param_name)) { | |||
| return item.param_name; | |||
| } | |||
| seen.add(item.param_name); | |||
| } | |||
| return null; | |||
| } | |||
| // 创建参数下拉菜单 | |||
| export function createMenuItems( | |||
| params: PipelineGlobalParam[], | |||
| parentNodes: INode[], | |||
| ): MenuProps['items'] { | |||
| const nodes: MenuProps['items'] = parentNodes.map((item) => { | |||
| const model = item.getModel(); | |||
| const out_parameters = model.out_parameters as string | undefined | null; | |||
| const out_parametersObj = parseJsonText(out_parameters); | |||
| const outParametersList = Object.keys(out_parametersObj ?? {}); | |||
| return { | |||
| key: model.id as string, | |||
| label: model.label as string, | |||
| children: outParametersList.map((key: string) => ({ | |||
| key: key as string, | |||
| label: out_parametersObj[key].label, | |||
| })), | |||
| }; | |||
| }); | |||
| if (params.length > 0) { | |||
| return [ | |||
| { | |||
| key: 'global', | |||
| label: '全局参数', | |||
| children: params.map((item) => ({ | |||
| key: item.param_name, | |||
| label: item.param_name, | |||
| })), | |||
| }, | |||
| ...nodes, | |||
| ]; | |||
| } else { | |||
| return [...nodes]; | |||
| } | |||
| } | |||
| export function getInParameterComponent( | |||
| parameter: PipelineNodeModelParameter, | |||
| ): React.ReactNode | null { | |||
| if (parameter.value) { | |||
| } | |||
| return null; | |||
| } | |||
| // 判断是否允许输入 | |||
| export function canInput(parameter: PipelineNodeModelParameter) { | |||
| const { type, item_type } = parameter; | |||
| return !( | |||
| type === 'ref' && | |||
| (item_type === 'dataset' || | |||
| item_type === 'model' || | |||
| item_type === 'image' || | |||
| item_type === 'code') | |||
| ); | |||
| } | |||
| @@ -0,0 +1,228 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { AvailableRange } from '@/enums'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { addKnowledgeGraph, editKnowledgeGraph, getEntityGraph } from '@/services/graph'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Form, Input, Select, message, type ModalProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| export enum VerifyMode { | |||
| Password = 0, // 用户名密码 | |||
| SSH = 1, // SSH Key | |||
| } | |||
| export enum OperationType { | |||
| Create = 0, // 新建 | |||
| Update = 1, // 更新 | |||
| } | |||
| type FormData = Partial<CodeConfigData>; | |||
| interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> { | |||
| opType: OperationType; | |||
| codeConfigData?: CodeConfigData; | |||
| onOk: () => void; | |||
| } | |||
| function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) { | |||
| const [form] = Form.useForm(); | |||
| const [ontologyList, setOntologyList] = useState([]); | |||
| const isPublic = Form.useWatch('code_repo_vis', form) === AvailableRange.Public; | |||
| useEffect(() => { | |||
| getEntityGraph().then((ret) => { | |||
| setOntologyList(ret.data); | |||
| }); | |||
| }, []); | |||
| // 创建 | |||
| const createCodeConfig = async (formData: FormData) => { | |||
| console.log(OperationType); | |||
| const params: FormData & { id?: number } = { | |||
| ...formData, | |||
| }; | |||
| // 清除多余的信息 | |||
| // if (formData.code_repo_vis === AvailableRange.Public) { | |||
| // omit(params, ['verify_mode', 'git_user_name', 'git_password', 'ssh_key']); | |||
| // } | |||
| // if (formData.verify_mode === VerifyMode.Password) { | |||
| // omit(params, ['ssh_key']); | |||
| // } else if (formData.verify_mode === VerifyMode.SSH) { | |||
| // omit(params, ['git_user_name', 'git_password']); | |||
| // } | |||
| if (opType === OperationType.Update) { | |||
| params.id = codeConfigData?.id; | |||
| } | |||
| const request = opType === OperationType.Create ? addKnowledgeGraph : editKnowledgeGraph; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| message.success(opType === OperationType.Create ? '创建成功' : '修改成功'); | |||
| onOk?.(); | |||
| } | |||
| }; | |||
| // 提交 | |||
| const onFinish = (formData: FormData) => { | |||
| createCodeConfig(formData); | |||
| }; | |||
| // 设置初始值 | |||
| const initialValues: FormData = codeConfigData ?? { | |||
| code_repo_vis: AvailableRange.Public, | |||
| verify_mode: VerifyMode.Password, | |||
| }; | |||
| if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) { | |||
| initialValues.verify_mode = VerifyMode.Password; | |||
| } | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title={opType === OperationType.Create ? '创建图谱' : '修改图谱'} | |||
| image={require('@/assets/img/create-experiment.png')} | |||
| width={825} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'form', | |||
| }} | |||
| destroyOnClose | |||
| > | |||
| <Form | |||
| name="form" | |||
| form={form} | |||
| layout="vertical" | |||
| onFinish={onFinish} | |||
| initialValues={initialValues} | |||
| autoComplete="off" | |||
| > | |||
| <Form.Item | |||
| label="图谱名称" | |||
| name="name" | |||
| required | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入图谱名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入图谱名称" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="关联实体" | |||
| name="ontologyId" | |||
| rules={[ | |||
| { | |||
| required: false, | |||
| message: '请选择关联实体', | |||
| }, | |||
| ]} | |||
| > | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择关联实体" | |||
| options={ontologyList} | |||
| fieldNames={{ label: 'name', value: 'id' }} | |||
| optionFilterProp="name" | |||
| showSearch | |||
| /> | |||
| </Form.Item> | |||
| {/* <Form.Item | |||
| noStyle | |||
| shouldUpdate={(prevValues, currentValues) => | |||
| prevValues?.code_repo_vis !== currentValues?.code_repo_vis | |||
| } | |||
| > | |||
| {({ getFieldValue }) => { | |||
| return getFieldValue('code_repo_vis') === AvailableRange.Private ? ( | |||
| <> | |||
| <Form.Item | |||
| label="验证方式" | |||
| name="verify_mode" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请选择验证方式', | |||
| }, | |||
| ]} | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={VerifyMode.Password}>用户名/密码</Radio> | |||
| <Radio value={VerifyMode.SSH}>SSH Key</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| noStyle | |||
| shouldUpdate={(prevValues, currentValues) => | |||
| prevValues?.verify_mode !== currentValues?.verify_mode | |||
| } | |||
| > | |||
| {({ getFieldValue }) => { | |||
| return getFieldValue('verify_mode') === VerifyMode.Password ? ( | |||
| <> | |||
| <Form.Item | |||
| label="Git 用户名" | |||
| name="git_user_name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 Git 用户名', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input | |||
| placeholder="请输入 Git 用户名" | |||
| autoComplete="off" | |||
| showCount | |||
| allowClear | |||
| maxLength={64} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="Git 密码" | |||
| name="git_password" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 Git 密码', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.Password | |||
| autoComplete="new-password" | |||
| placeholder="请输入 Git 密码" | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </> | |||
| ) : ( | |||
| <Form.Item | |||
| label="SSH Key" | |||
| name="ssh_key" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入 SSH Key', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入 SSH Key" | |||
| showCount | |||
| maxLength={4096} | |||
| autoSize={{ minRows: 4, maxRows: 8 }} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| ); | |||
| }} | |||
| </Form.Item> | |||
| </> | |||
| ) : null; | |||
| }} | |||
| </Form.Item> */} | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default AddCodeConfigModal; | |||
| @@ -0,0 +1,136 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { AvailableRange } from '@/enums'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { addKnowledgeSubject } from '@/services/subject'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Form, Input, message, type FormRule, type ModalProps } from 'antd'; | |||
| import { useMemo, useState } from 'react'; | |||
| export enum VerifyMode { | |||
| Password = 0, // 用户名密码 | |||
| SSH = 1, // SSH Key | |||
| } | |||
| export enum OperationType { | |||
| Create = 0, // 新建 | |||
| Update = 1, // 更新 | |||
| } | |||
| type FormData = Partial<CodeConfigData>; | |||
| interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> { | |||
| opType: OperationType; | |||
| codeConfigData?: CodeConfigData; | |||
| onOk: () => void; | |||
| } | |||
| function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) { | |||
| const [form] = Form.useForm(); | |||
| const [typeList, setTypeList] = useState([]); | |||
| const isPublic = Form.useWatch('code_repo_vis', form) === AvailableRange.Public; | |||
| const urlExample = useMemo( | |||
| () => | |||
| isPublic | |||
| ? 'https://gitlink.org.cn/ci4s/ci4sManagement-cloud.git' | |||
| : 'git@code.gitlink.org.cn:ci4s/ci4sManagement-cloud.git', | |||
| [isPublic], | |||
| ); | |||
| // /^(git@[\w.-]+:[\w./-]+\.git)$/ | |||
| const urlRules: FormRule[] = useMemo( | |||
| () => | |||
| isPublic | |||
| ? [ | |||
| { | |||
| type: 'url', | |||
| message: '请输入正确的 Git 地址', | |||
| }, | |||
| ] | |||
| : ([] as FormRule[]), | |||
| [isPublic], | |||
| ); | |||
| // 创建 | |||
| const createCodeConfig = async (formData: FormData) => { | |||
| const params: FormData & { id?: number } = { | |||
| ...formData, | |||
| }; | |||
| if (opType === OperationType.Update) { | |||
| params.id = codeConfigData?.id; | |||
| } | |||
| const request = opType === OperationType.Create ? addKnowledgeSubject : ''; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| message.success(opType === OperationType.Create ? '创建成功' : '修改成功'); | |||
| onOk?.(); | |||
| } | |||
| }; | |||
| // 提交 | |||
| const onFinish = (formData: FormData) => { | |||
| createCodeConfig(formData); | |||
| }; | |||
| // 设置初始值 | |||
| const initialValues: FormData = codeConfigData ?? { | |||
| code_repo_vis: AvailableRange.Public, | |||
| verify_mode: VerifyMode.Password, | |||
| }; | |||
| if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) { | |||
| initialValues.verify_mode = VerifyMode.Password; | |||
| } | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title="创建主体" | |||
| image={require('@/assets/img/create-experiment.png')} | |||
| width={825} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'form', | |||
| }} | |||
| destroyOnClose | |||
| > | |||
| <Form | |||
| name="form" | |||
| form={form} | |||
| layout="vertical" | |||
| onFinish={onFinish} | |||
| initialValues={initialValues} | |||
| autoComplete="off" | |||
| > | |||
| <Form.Item | |||
| label="主体名称" | |||
| name="name" | |||
| required | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入主体名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入主体名称" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="主体描述" | |||
| name="description" | |||
| required | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入主体描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入主体描述" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default AddCodeConfigModal; | |||
| @@ -0,0 +1,161 @@ | |||
| import { getAccessToken } from '@/access'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { AvailableRange } from '@/enums'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { getEntityGraph, postVersion } from '@/services/graph'; | |||
| import { getFileListFromEvent } from '@/utils/ui'; | |||
| import { Button, Form, message, Tabs, Upload, type ModalProps, type UploadProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import './index.less'; | |||
| export enum VerifyMode { | |||
| Password = 0, // 用户名密码 | |||
| SSH = 1, // SSH Key | |||
| } | |||
| export enum OperationType { | |||
| Create = 0, // 新建 | |||
| Update = 1, // 更新 | |||
| } | |||
| type FormData = Partial<CodeConfigData>; | |||
| interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> { | |||
| opType: OperationType; | |||
| codeConfigData?: CodeConfigData; | |||
| onOk: () => void; | |||
| } | |||
| function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) { | |||
| const items = [ | |||
| { | |||
| key: '1', | |||
| label: `CSV`, | |||
| children: null, | |||
| }, | |||
| ]; | |||
| const [form] = Form.useForm(); | |||
| const [activeTab, setActiveTab] = useState<string>('1'); | |||
| const [ontologyList, setOntologyList] = useState([]); | |||
| const isPublic = Form.useWatch('code_repo_vis', form) === AvailableRange.Public; | |||
| useEffect(() => { | |||
| getEntityGraph().then((ret) => { | |||
| setOntologyList(ret.data); | |||
| }); | |||
| }, []); | |||
| const [fileList, setFileList] = useState([]); | |||
| const handleChange: UploadProps['onChange'] = (info) => { | |||
| const { status } = info; | |||
| if (status == 'done') { | |||
| console.log(info); | |||
| } | |||
| }; | |||
| // 上传前认证 | |||
| const beforeUpload: UploadProps['beforeUpload'] = () => { | |||
| const fileList = form.getFieldValue('fileList'); | |||
| if (Array.isArray(fileList) && fileList.length >= 1) { | |||
| message.error('只允许上传一个文件'); | |||
| return Upload.LIST_IGNORE; | |||
| } | |||
| return true; | |||
| }; | |||
| const uploadProps: UploadProps = { | |||
| action: '/api//kg/version/upload', | |||
| headers: { | |||
| Authorization: getAccessToken() || '', | |||
| }, | |||
| maxCount: 1, | |||
| defaultFileList: [], | |||
| }; | |||
| // 创建 | |||
| // const createCodeConfig = async (formData: FormData) => { | |||
| // console.log(OperationType); | |||
| // const params: FormData & { id?: number } = { | |||
| // ...formData, | |||
| // }; | |||
| // // 清除多余的信息 | |||
| // // if (formData.code_repo_vis === AvailableRange.Public) { | |||
| // // omit(params, ['verify_mode', 'git_user_name', 'git_password', 'ssh_key']); | |||
| // // } | |||
| // // if (formData.verify_mode === VerifyMode.Password) { | |||
| // // omit(params, ['ssh_key']); | |||
| // // } else if (formData.verify_mode === VerifyMode.SSH) { | |||
| // // omit(params, ['git_user_name', 'git_password']); | |||
| // // } | |||
| // if (opType === OperationType.Update) { | |||
| // params.id = codeConfigData?.id; | |||
| // } | |||
| // const request = opType === OperationType.Create ? addKnowledgeGraph : editKnowledgeGraph; | |||
| // const [res] = await to(request(params)); | |||
| // if (res) { | |||
| // message.success(opType === OperationType.Create ? '增量更新成功' : '全量更新成功'); | |||
| // onOk?.(); | |||
| // } | |||
| // }; | |||
| // 提交 | |||
| const onFinish = (formData: FormData) => { | |||
| console.log(formData); | |||
| postVersion({ | |||
| path: formData.fileList[0].response.data, | |||
| kgId: codeConfigData.id, | |||
| }).then((ret) => { | |||
| if (ret.code == 200) { | |||
| message.success('导入成功'); | |||
| onOk?.(); | |||
| } | |||
| }); | |||
| }; | |||
| // 设置初始值 | |||
| const initialValues: FormData = codeConfigData ?? { | |||
| code_repo_vis: AvailableRange.Public, | |||
| verify_mode: VerifyMode.Password, | |||
| }; | |||
| if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) { | |||
| initialValues.verify_mode = VerifyMode.Password; | |||
| } | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title={'导入'} | |||
| image={require('@/assets/img/create-experiment.png')} | |||
| width={825} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'form', | |||
| }} | |||
| destroyOnClose | |||
| > | |||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||
| <Form | |||
| name="form" | |||
| form={form} | |||
| layout="vertical" | |||
| onFinish={onFinish} | |||
| initialValues={initialValues} | |||
| autoComplete="off" | |||
| > | |||
| <Form.Item | |||
| name="fileList" | |||
| valuePropName="fileList" | |||
| getValueFromEvent={getFileListFromEvent} | |||
| required | |||
| > | |||
| <Upload {...uploadProps} beforeUpload={beforeUpload} fileList={fileList}> | |||
| <Button type="link" style={{ paddingLeft: 0, paddingRight: 0 }}> | |||
| 选择CSV文件 | |||
| </Button> | |||
| </Upload> | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default AddCodeConfigModal; | |||
| @@ -0,0 +1,108 @@ | |||
| .code-config-item { | |||
| position: relative; | |||
| width: calc(25% - 15px); | |||
| padding: 20px; | |||
| background: linear-gradient(180deg, #f7faff 0%, #ffffff 100%); | |||
| border: 2px solid white; | |||
| border-radius: 4px; | |||
| box-shadow: 0px 3px 10px rgba(164, 169, 181, 0.13); | |||
| cursor: pointer; | |||
| &:hover { | |||
| border-color: @primary-color; | |||
| } | |||
| @media screen and (max-width: 1860px) { | |||
| & { | |||
| width: calc(33.33% - 13.33px); | |||
| } | |||
| } | |||
| &__icon { | |||
| flex: none; | |||
| width: 16px; | |||
| height: 16px; | |||
| margin-right: 10px; | |||
| } | |||
| &__name { | |||
| position: relative; | |||
| margin-right: 20px; | |||
| margin-bottom: 0 !important; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 16px; | |||
| &::after { | |||
| position: absolute; | |||
| top: 14px; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 6px; | |||
| background: linear-gradient( | |||
| to right, | |||
| .addAlpha(@primary-color, 0.4) [] 0, | |||
| .addAlpha(@primary-color, 0) [] 100% | |||
| ); | |||
| content: ''; | |||
| } | |||
| } | |||
| &:hover &__name { | |||
| color: @primary-color; | |||
| } | |||
| &__tag { | |||
| flex: none; | |||
| padding: 1px 10px; | |||
| font-size: 13px; | |||
| border-radius: 2px; | |||
| &--public { | |||
| color: @primary-color; | |||
| background-color: .addAlpha(@primary-color, 0.1) []; | |||
| border: 1px solid .addAlpha(@primary-color, 0.5) []; | |||
| } | |||
| &--private { | |||
| color: @warning-color; | |||
| background-color: .addAlpha(@warning-color, 0.1) []; | |||
| border: 1px solid .addAlpha(@warning-color, 0.5) []; | |||
| } | |||
| } | |||
| :global { | |||
| .ant-btn { | |||
| flex: none; | |||
| color: #808080; | |||
| } | |||
| } | |||
| &__url-box { | |||
| margin-bottom: 15px; | |||
| padding: 14px; | |||
| background-color: .addAlpha(@primary-color, 0.04) []; | |||
| border-radius: 4px; | |||
| } | |||
| &__url { | |||
| margin-bottom: 15px !important; | |||
| color: @text-color; | |||
| font-size: 14px; | |||
| } | |||
| &__branch { | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| } | |||
| &__user, | |||
| &__time { | |||
| display: flex; | |||
| flex: 0 1 content; | |||
| align-items: center; | |||
| width: 100%; | |||
| color: #808080; | |||
| font-size: 13px; | |||
| } | |||
| } | |||
| @@ -0,0 +1,94 @@ | |||
| import clock from '@/assets/img/clock.png'; | |||
| import creatByImg from '@/assets/img/creatBy.png'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { AvailableRange } from '@/enums'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Button, Flex, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import styles from './index.less'; | |||
| type CodeConfigItemProps = { | |||
| item: CodeConfigData; | |||
| onClick?: (item: CodeConfigData) => void; | |||
| onEdit?: (item: CodeConfigData) => void; | |||
| onRemove?: (item: CodeConfigData) => void; | |||
| }; | |||
| function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps) { | |||
| return ( | |||
| <div className={styles['code-config-item']} onClick={() => onClick?.(item)}> | |||
| <Flex justify="space-between" align="center" style={{ marginBottom: '20px', height: '32px' }}> | |||
| <img | |||
| className={styles['code-config-item__icon']} | |||
| src={require('@/assets/img/code-name-icon.png')} | |||
| alt="" | |||
| /> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__name']} | |||
| ellipsis={{ tooltip: item.code_repo_name }} | |||
| > | |||
| {item.code_repo_name} | |||
| </Typography.Paragraph> | |||
| <div | |||
| className={classNames( | |||
| styles['code-config-item__tag'], | |||
| item.code_repo_vis === AvailableRange.Public | |||
| ? styles['code-config-item__tag--public'] | |||
| : styles['code-config-item__tag--private'], | |||
| )} | |||
| > | |||
| {item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'} | |||
| </div> | |||
| <Button | |||
| type="text" | |||
| shape="circle" | |||
| style={{ marginLeft: 'auto', marginRight: '4px' }} | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onEdit?.(item); | |||
| }} | |||
| > | |||
| <KFIcon type="icon-bianji" font={17} /> | |||
| </Button> | |||
| <Button | |||
| type="text" | |||
| shape="circle" | |||
| style={{ marginRight: '-4px' }} | |||
| onClick={(e) => { | |||
| e.stopPropagation(); | |||
| onRemove?.(item); | |||
| }} | |||
| > | |||
| <KFIcon type="icon-shanchu" font={17} /> | |||
| </Button> | |||
| </Flex> | |||
| <div className={styles['code-config-item__url-box']}> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__url']} | |||
| ellipsis={{ tooltip: item.git_url }} | |||
| > | |||
| {item.git_url} | |||
| </Typography.Paragraph> | |||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||
| </div> | |||
| <Flex justify="space-between"> | |||
| <div className={styles['code-config-item__user']}> | |||
| <img | |||
| style={{ width: '16px', marginRight: '6px' }} | |||
| src={creatByImg} | |||
| alt="" | |||
| draggable={false} | |||
| /> | |||
| <span>{item.create_by}</span> | |||
| </div> | |||
| <div className={styles['code-config-item__time']}> | |||
| <img style={{ width: '12px', marginRight: '5px' }} src={clock} alt="" draggable={false} /> | |||
| <span>最近更新: {formatDate(item.update_time, 'YYYY-MM-DD')}</span> | |||
| </div> | |||
| </Flex> | |||
| </div> | |||
| ); | |||
| } | |||
| export default CodeConfigItem; | |||
| @@ -0,0 +1,50 @@ | |||
| .code-selector { | |||
| width: 100%; | |||
| height: 100%; | |||
| &__search { | |||
| width: 100%; | |||
| } | |||
| :global { | |||
| .ant-input-affix-wrapper { | |||
| border-radius: 23px !important; | |||
| .ant-input-prefix { | |||
| margin-inline-end: 12px; | |||
| } | |||
| .ant-input-suffix { | |||
| margin-inline-end: 12px; | |||
| } | |||
| .ant-input-clear-icon { | |||
| font-size: 16px; | |||
| } | |||
| } | |||
| .ant-input-group-addon { | |||
| display: none; | |||
| } | |||
| .ant-pagination { | |||
| .ant-select-single { | |||
| height: 32px !important; | |||
| } | |||
| } | |||
| } | |||
| &__content { | |||
| display: flex; | |||
| flex-direction: row; | |||
| flex-wrap: wrap; | |||
| gap: 10px; | |||
| width: 100%; | |||
| max-height: 50vh; | |||
| margin-top: 24px; | |||
| margin-bottom: 30px; | |||
| overflow-x: hidden; | |||
| overflow-y: auto; | |||
| } | |||
| &__empty { | |||
| padding-top: 40px; | |||
| } | |||
| } | |||
| @@ -0,0 +1,120 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-11 16:31:18 | |||
| * @Description: 选择代码 | |||
| */ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { getCodeConfigListReq } from '@/services/codeConfig'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Icon } from '@umijs/max'; | |||
| import type { ModalProps, PaginationProps } from 'antd'; | |||
| import { Empty, Input, Pagination } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import CodeConfigItem from '../CodeConfigItem'; | |||
| import styles from './index.less'; | |||
| export { type CodeConfigData }; | |||
| export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| onOk?: (params: CodeConfigData | undefined) => void; | |||
| } | |||
| function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||
| current: 1, | |||
| pageSize: 20, | |||
| }); | |||
| const [searchText, setSearchText] = useState<string | undefined>(undefined); | |||
| const [inputText, setInputText] = useState<string | undefined>(undefined); | |||
| useEffect(() => { | |||
| getDataList(); | |||
| }, [pagination, searchText]); | |||
| // 获取数据请求 | |||
| const getDataList = async () => { | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| code_repo_name: searchText !== '' ? searchText : undefined, | |||
| }; | |||
| const [res] = await to(getCodeConfigListReq(params)); | |||
| if (res && res.data && res.data.content) { | |||
| setDataList(res.data.content); | |||
| setTotal(res.data.totalElements); | |||
| } | |||
| }; | |||
| // 搜索 | |||
| const handleSearch = (value: string) => { | |||
| setSearchText(value); | |||
| }; | |||
| const handleClick = (item: CodeConfigData) => { | |||
| onOk?.(item); | |||
| }; | |||
| // 分页切换 | |||
| const handlePageChange: PaginationProps['onChange'] = (page, pageSize) => { | |||
| setPagination({ | |||
| current: page, | |||
| pageSize: pageSize, | |||
| }); | |||
| }; | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title="选择代码配置" | |||
| image={require('@/assets/img/modal-code-config.png')} | |||
| width={920} | |||
| footer={null} | |||
| destroyOnClose | |||
| > | |||
| <div className={styles['code-selector']}> | |||
| <Input.Search | |||
| className={styles['code-selector__search']} | |||
| placeholder="按代码仓库名称筛选" | |||
| allowClear | |||
| onSearch={handleSearch} | |||
| size="large" | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| suffix={null} | |||
| value={inputText} | |||
| prefix={ | |||
| <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||
| } | |||
| /> | |||
| {dataList?.length !== 0 ? ( | |||
| <> | |||
| <div className={styles['code-selector__content']}> | |||
| {dataList?.map((item) => ( | |||
| <CodeConfigItem item={item} key={item.id} onClick={handleClick} /> | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| align="center" | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||
| showQuickJumper | |||
| onChange={handlePageChange} | |||
| {...pagination} | |||
| /> | |||
| </> | |||
| ) : ( | |||
| <div className={styles['code-selector__empty']}> | |||
| <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default CodeSelectorModal; | |||
| @@ -0,0 +1,29 @@ | |||
| .form-item { | |||
| position: relative; | |||
| padding-top: 40px; | |||
| border-bottom: 1px dashed rgba(20, 49, 179, 0.12); | |||
| &__delete-button { | |||
| position: absolute; | |||
| top: 5px; | |||
| right: 24px; | |||
| } | |||
| :global { | |||
| .anticon.anticon-question-circle { | |||
| margin-top: -12px; | |||
| } | |||
| } | |||
| } | |||
| .form-item-add { | |||
| margin-top: 15px; | |||
| &:first-child { | |||
| margin-top: 0; | |||
| } | |||
| &__add-button { | |||
| padding: 0; | |||
| } | |||
| } | |||
| @@ -0,0 +1,189 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { getParamComponent, getParamRules } from '@/pages/Experiment/components/AddExperimentModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { PlusOutlined } from '@ant-design/icons'; | |||
| import { Button, Drawer, Form, Input, Radio, Tooltip } from 'antd'; | |||
| import { NamePath } from 'antd/es/form/interface'; | |||
| import { forwardRef, useImperativeHandle } from 'react'; | |||
| import styles from './index.less'; | |||
| type GlobalParamsDrawerProps = { | |||
| open: boolean; | |||
| onClose: () => void; | |||
| globalParam: PipelineGlobalParam[] | null; | |||
| }; | |||
| const GlobalParamsDrawer = forwardRef( | |||
| ({ open = false, onClose, globalParam = [] }: GlobalParamsDrawerProps, ref) => { | |||
| const [form] = Form.useForm(); | |||
| useImperativeHandle(ref, () => ({ | |||
| validateFields: async () => { | |||
| const [values, error] = await to(form.validateFields()); | |||
| if (!error && values) { | |||
| return values; | |||
| } else { | |||
| return Promise.reject(error); | |||
| } | |||
| }, | |||
| getFieldsValue: () => { | |||
| return form.getFieldsValue(); | |||
| }, | |||
| })); | |||
| // 处理参数类型变化 | |||
| const handleTypeChange = (name: NamePath) => { | |||
| form.setFieldValue(name, null); | |||
| }; | |||
| const removeParameter = (name: number, remove: (param: number) => void) => { | |||
| modalConfirm({ | |||
| title: '确认删除该参数吗?', | |||
| onOk: () => { | |||
| remove(name); | |||
| }, | |||
| }); | |||
| }; | |||
| return ( | |||
| <Drawer | |||
| rootStyle={{ marginTop: '45px' }} | |||
| title="全局参数" | |||
| placement="right" | |||
| closeIcon={false} | |||
| getContainer={false} | |||
| onClose={onClose} | |||
| open={open} | |||
| width={520} | |||
| > | |||
| <Form | |||
| name="global_params_form" | |||
| autoComplete="off" | |||
| form={form} | |||
| labelCol={{ span: 5 }} | |||
| wrapperCol={{ span: 19 }} | |||
| initialValues={{ global_param: globalParam }} | |||
| labelAlign="left" | |||
| > | |||
| <Form.List name="global_param"> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }) => ( | |||
| <div key={key} className={styles['form-item']}> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'param_name']} | |||
| label="参数名称" | |||
| validateTrigger={[]} | |||
| rules={[ | |||
| { required: true, message: '请输入参数名称' }, | |||
| { | |||
| validator: (_, value) => { | |||
| const list = form.getFieldValue('global_param') || []; | |||
| const names = list.filter((item: any) => item?.param_name === value); | |||
| if (value && names.length > 1) { | |||
| return Promise.reject('参数名称不能重复'); | |||
| } else { | |||
| return Promise.resolve(); | |||
| } | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input | |||
| placeholder="请输入参数名称" | |||
| allowClear | |||
| onBlur={() => form.validateFields()} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'description']} | |||
| label="参数描述" | |||
| rules={[{ required: true, message: '请输入参数描述' }]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入参数描述" | |||
| autoSize={{ minRows: 2, maxRows: 4 }} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'param_type']} | |||
| label="类 型" | |||
| rules={[{ required: true, message: '请选择类型' }]} | |||
| > | |||
| <Radio.Group | |||
| onChange={() => handleTypeChange(['global_param', name, 'param_value'])} | |||
| > | |||
| <Radio value={1}>字符串</Radio> | |||
| <Radio value={2}>整型</Radio> | |||
| <Radio value={3}>布尔类型</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| noStyle | |||
| shouldUpdate={(prev, cur) => | |||
| prev.global_param?.[name]?.param_type !== | |||
| cur.global_param?.[name]?.param_type | |||
| } | |||
| > | |||
| {({ getFieldValue }) => ( | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'param_value']} | |||
| label="值" | |||
| rules={getParamRules( | |||
| getFieldValue(['global_param', name, 'param_type']), | |||
| true, | |||
| )} | |||
| > | |||
| {getParamComponent(getFieldValue(['global_param', name, 'param_type']))} | |||
| </Form.Item> | |||
| )} | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'is_sensitive']} | |||
| label="脱敏显示" | |||
| rules={[{ required: true, message: '请选择' }]} | |||
| tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示" | |||
| > | |||
| <Radio.Group> | |||
| <Radio value={1}>是</Radio> | |||
| <Radio value={0}>否</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Tooltip title="删除参数"> | |||
| <Button | |||
| className={styles['form-item__delete-button']} | |||
| type="link" | |||
| onClick={() => removeParameter(name, remove)} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| ></Button> | |||
| </Tooltip> | |||
| </div> | |||
| ))} | |||
| <Form.Item className={styles['form-item-add']}> | |||
| <Button | |||
| className={styles['form-item-add__add-button']} | |||
| type="link" | |||
| onClick={() => add()} | |||
| icon={<PlusOutlined />} | |||
| > | |||
| 流水线参数 | |||
| </Button> | |||
| </Form.Item> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| </Form> | |||
| </Drawer> | |||
| ); | |||
| }, | |||
| ); | |||
| export default GlobalParamsDrawer; | |||
| @@ -0,0 +1,61 @@ | |||
| .pipeline-drawer { | |||
| :global { | |||
| label { | |||
| width: 100%; | |||
| &::after { | |||
| display: none; | |||
| } | |||
| } | |||
| .ant-tabs-nav .ant-tabs-nav-list{ | |||
| width: 100%; | |||
| justify-content: space-between; | |||
| } | |||
| .ant-tabs .ant-tabs-tab{ | |||
| padding: 12px 20px 12px 24px; | |||
| margin: 0; | |||
| } | |||
| .ant-tabs .ant-tabs-ink-bar{ | |||
| background-color: #1664ff; | |||
| } | |||
| } | |||
| &__title { | |||
| display: flex; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 43px; | |||
| margin-bottom: 20px; | |||
| padding: 0 24px; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| background: #f8fbff; | |||
| } | |||
| &__ref-row { | |||
| display: flex; | |||
| align-items: center; | |||
| &__select-button { | |||
| display: flex; | |||
| flex: none; | |||
| align-items: center; | |||
| justify-content: flex-start; | |||
| margin-left: 10px; | |||
| padding-right: 0; | |||
| padding-left: 0; | |||
| } | |||
| } | |||
| .graph-drawer-box{ | |||
| padding: 12px 20px; | |||
| color:#1d1d20; | |||
| font-size:15px; | |||
| &__slice{ | |||
| width: 100%; | |||
| height:0px; | |||
| border:1px solid; | |||
| border-color:rgba(20, 49, 179, 0.12); | |||
| margin: 5px 0 20px 0; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,382 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { requiredValidator } from '@/components/ParameterInput'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { createMenuItems } from '@/pages/Pipeline/Info/utils'; | |||
| import { | |||
| PipelineGlobalParam, | |||
| PipelineNodeModel, | |||
| PipelineNodeModelParameter, | |||
| PipelineNodeModelSerialize, | |||
| } from '@/types'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { INode } from '@antv/g6'; | |||
| import { Drawer, Form, Input, MenuProps, Tabs } from 'antd'; | |||
| import { NamePath } from 'antd/es/form/interface'; | |||
| import { forwardRef, useImperativeHandle, useState } from 'react'; | |||
| import CodeSelectorModal from '../CodeSelectorModal'; | |||
| import PropsLabel from '../PropsLabel'; | |||
| import ResourceSelectorModal, { | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '../ResourceSelectorModal'; | |||
| import styles from './index.less'; | |||
| const { TextArea } = Input; | |||
| type PipelineNodeParameterProps = { | |||
| onFormChange: (data: PipelineNodeModelSerialize) => void; | |||
| }; | |||
| const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParameterProps, ref) => { | |||
| const [form] = Form.useForm(); | |||
| const [activeTab, setActiveTab] = useState<string>('1'); | |||
| const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( | |||
| {} as PipelineNodeModelSerialize, | |||
| ); | |||
| const [open, setOpen] = useState(false); | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 | |||
| const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | |||
| const items = [ | |||
| { | |||
| key: '1', | |||
| label: `属性`, | |||
| children: null, | |||
| }, | |||
| { | |||
| key: '2', | |||
| label: `条件过滤`, | |||
| children: null, | |||
| }, | |||
| { | |||
| key: '3', | |||
| label: `多条过滤`, | |||
| children: null, | |||
| }, | |||
| ]; | |||
| const afterOpenChange = async () => { | |||
| if (!open) { | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_values, error] = await to(form.validateFields()); | |||
| const fields = form.getFieldsValue(); | |||
| const control_strategy = JSON.stringify(fields.control_strategy); | |||
| const in_parameters = JSON.stringify(fields.in_parameters); | |||
| const out_parameters = JSON.stringify(fields.out_parameters); | |||
| // console.log('getFieldsValue', fields); | |||
| const res = { | |||
| ...stagingItem, | |||
| ...fields, | |||
| control_strategy: control_strategy, | |||
| in_parameters: in_parameters, | |||
| out_parameters: out_parameters, | |||
| formError: !!error, | |||
| }; | |||
| console.log('res', res); | |||
| onFormChange(res); | |||
| } | |||
| }; | |||
| const onClose = () => { | |||
| setOpen(false); | |||
| }; | |||
| useImperativeHandle(ref, () => ({ | |||
| showDrawer( | |||
| model: PipelineNodeModel, | |||
| params: PipelineGlobalParam[], | |||
| parentNodes: INode[], | |||
| validate: boolean = false, | |||
| ) { | |||
| try { | |||
| const nodeData: PipelineNodeModelSerialize = { | |||
| ...model, | |||
| in_parameters: JSON.parse(model.in_parameters), | |||
| out_parameters: JSON.parse(model.out_parameters), | |||
| control_strategy: JSON.parse(model.control_strategy), | |||
| }; | |||
| // console.log('model', nodeData); | |||
| setStagingItem({ | |||
| ...nodeData, | |||
| }); | |||
| form.resetFields(); | |||
| form.setFieldsValue({ | |||
| ...nodeData, | |||
| }); | |||
| if (validate) { | |||
| form.validateFields(); | |||
| } | |||
| } catch (error) { | |||
| console.error('JSON.parse error: ', error); | |||
| } | |||
| setOpen(true); | |||
| // 参数下拉菜单 | |||
| setMenuItems(createMenuItems(params, parentNodes)); | |||
| }, | |||
| close: () => { | |||
| onClose(); | |||
| }, | |||
| validateFields: async () => { | |||
| if (!open) { | |||
| return; | |||
| } | |||
| const [values, error] = await to(form.validateFields()); | |||
| if (!error && values) { | |||
| return values; | |||
| } else { | |||
| form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' }); | |||
| return Promise.reject(error); | |||
| } | |||
| }, | |||
| })); | |||
| // ref 类型选择 | |||
| const selectRefData = ( | |||
| formItemName: NamePath, | |||
| item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, | |||
| ) => { | |||
| if (item.item_type === 'code') { | |||
| selectCodeConfig(formItemName, item); | |||
| } else { | |||
| selectResource(formItemName, item); | |||
| } | |||
| }; | |||
| // 选择代码配置 | |||
| const selectCodeConfig = ( | |||
| formItemName: NamePath, | |||
| item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, | |||
| ) => { | |||
| const { close } = openAntdModal(CodeSelectorModal, { | |||
| onOk: (res) => { | |||
| if (res) { | |||
| const value = JSON.stringify({ | |||
| id: res.id, | |||
| name: res.code_repo_name, | |||
| code_path: res.git_url, | |||
| branch: res.git_branch, | |||
| username: res.git_user_name, | |||
| password: res.git_password, | |||
| ssh_private_key: res.ssh_key, | |||
| }); | |||
| form.setFieldValue(formItemName, { | |||
| ...item, | |||
| value, | |||
| showValue: res.code_repo_name, | |||
| fromSelect: true, | |||
| }); | |||
| } | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 选择数据集、模型、镜像 | |||
| const selectResource = ( | |||
| formItemName: NamePath, | |||
| item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, | |||
| ) => { | |||
| let type: ResourceSelectorType; | |||
| switch (item.item_type) { | |||
| case 'dataset': | |||
| type = ResourceSelectorType.Dataset; | |||
| break; | |||
| case 'model': | |||
| type = ResourceSelectorType.Model; | |||
| break; | |||
| default: | |||
| type = ResourceSelectorType.Mirror; | |||
| break; | |||
| } | |||
| const fieldValue = form.getFieldValue(formItemName); | |||
| const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined; | |||
| const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : []; | |||
| const checkedKeys = Array.isArray(fieldValue?.checkedKeys) ? fieldValue?.checkedKeys : []; | |||
| const { close } = openAntdModal(ResourceSelectorModal, { | |||
| type, | |||
| defaultExpandedKeys: expandedKeys, | |||
| defaultCheckedKeys: checkedKeys, | |||
| defaultActiveTab: activeTab, | |||
| onOk: (res) => { | |||
| if (res) { | |||
| if (type === ResourceSelectorType.Mirror) { | |||
| const { activeTab, id, version, path } = res; | |||
| if (formItemName === 'image') { | |||
| // 单独的选择镜像 | |||
| form.setFieldValue(formItemName, path); | |||
| } else { | |||
| // 输入参数选择镜像 | |||
| form.setFieldValue(formItemName, { | |||
| ...item, | |||
| value: path, | |||
| showValue: path, | |||
| fromSelect: true, | |||
| activeTab, | |||
| expandedKeys: [id], | |||
| checkedKeys: [`${id}-${version}`], | |||
| }); | |||
| } | |||
| } else { | |||
| const { activeTab, id, name, version, path, identifier, owner } = res; | |||
| const value = JSON.stringify({ | |||
| id, | |||
| name, | |||
| version, | |||
| path, | |||
| identifier, | |||
| owner, | |||
| }); | |||
| const showValue = `${name}:${version}`; | |||
| form.setFieldValue(formItemName, { | |||
| ...item, | |||
| value, | |||
| showValue, | |||
| fromSelect: true, | |||
| activeTab, | |||
| expandedKeys: [id], | |||
| checkedKeys: [`${id}-${version}`], | |||
| }); | |||
| } | |||
| } else { | |||
| if (type === ResourceSelectorType.Mirror && formItemName === 'image') { | |||
| form.setFieldValue(formItemName, undefined); | |||
| } else { | |||
| form.setFieldValue(formItemName, { | |||
| ...item, | |||
| value: undefined, | |||
| showValue: undefined, | |||
| fromSelect: false, | |||
| activeTab: undefined, | |||
| expandedKeys: [], | |||
| checkedKeys: [], | |||
| }); | |||
| } | |||
| } | |||
| form.validateFields([formItemName]); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 获取选择数据集、模型后面按钮 icon | |||
| const getSelectBtnIcon = (item: { item_type: string }) => { | |||
| const type = item.item_type; | |||
| if (type === 'code') { | |||
| return <KFIcon type="icon-xuanzedaimapeizhi" />; | |||
| } | |||
| let selectorType: ResourceSelectorType; | |||
| if (type === 'dataset') { | |||
| selectorType = ResourceSelectorType.Dataset; | |||
| } else if (type === 'model') { | |||
| selectorType = ResourceSelectorType.Model; | |||
| } else { | |||
| selectorType = ResourceSelectorType.Mirror; | |||
| } | |||
| return <KFIcon type={selectorTypeConfig[selectorType].buttonIcon} />; | |||
| }; | |||
| // 参数回填 | |||
| const handleParameterClick = (name: NamePath, value: any) => { | |||
| form.setFieldValue(name, value); | |||
| }; | |||
| // form item label | |||
| const getLabel = ( | |||
| item: { key: string; value: PipelineNodeModelParameter }, | |||
| namePrefix: string, | |||
| ) => { | |||
| return item.value.type === 'select' ? ( | |||
| item.value.label + '(' + item.key + ')' | |||
| ) : ( | |||
| <PropsLabel | |||
| menuItems={menuItems} | |||
| title={item.value.label + '(' + item.key + ')'} | |||
| onClick={(value) => { | |||
| handleParameterClick([namePrefix, item.key], { | |||
| ...item.value, | |||
| value, | |||
| fromSelect: true, | |||
| showValue: value, | |||
| }); | |||
| }} | |||
| /> | |||
| ); | |||
| }; | |||
| // 必填项校验规则 | |||
| const getFormRules = (item: { key: string; value: PipelineNodeModelParameter }) => { | |||
| return item.value.require | |||
| ? [ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '必填项', | |||
| }, | |||
| ] | |||
| : []; | |||
| }; | |||
| // 控制策略 | |||
| const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map( | |||
| ([key, value]) => ({ key, value }), | |||
| ); | |||
| // 输入参数 | |||
| const inParametersList = Object.entries(stagingItem.in_parameters ?? {}).map(([key, value]) => ({ | |||
| key, | |||
| value, | |||
| })); | |||
| // 输出参数 | |||
| const outParametersList = Object.entries(stagingItem.out_parameters ?? {}).map( | |||
| ([key, value]) => ({ key, value }), | |||
| ); | |||
| return ( | |||
| <Drawer | |||
| placement="right" | |||
| rootStyle={{ marginTop: '52px' }} | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| onClose={onClose} | |||
| afterOpenChange={afterOpenChange} | |||
| open={open} | |||
| width={358} | |||
| className={styles['pipeline-drawer']} | |||
| > | |||
| <Tabs | |||
| style={{ width: '100%' }} | |||
| activeKey={activeTab} | |||
| items={items} | |||
| onChange={(key) => setActiveTab(key)} | |||
| ></Tabs> | |||
| <div className={styles['graph-drawer-box']}> | |||
| <div style={{ marginBottom: '15px' }}> | |||
| <span>ID:</span> | |||
| <span style={{ marginLeft: '10px' }}>9855312121212121212</span> | |||
| </div> | |||
| <div style={{ marginBottom: '15px' }}> | |||
| <span>Label:</span> | |||
| <span style={{ marginLeft: '10px' }}>9855312121212121212</span> | |||
| </div> | |||
| <div className={styles['graph-drawer-box__slice']}></div> | |||
| <div style={{ marginBottom: '15px' }}> | |||
| <span>Name:</span> | |||
| <span style={{ marginLeft: '10px' }}>小时一西</span> | |||
| </div> | |||
| <div style={{ marginBottom: '15px' }}> | |||
| <span>XXX时间:</span> | |||
| <span style={{ marginLeft: '10px' }}>15: 10: 205分26秒</span> | |||
| </div> | |||
| <div style={{ marginBottom: '15px' }}> | |||
| <span>XXX:</span> | |||
| <span style={{ marginLeft: '10px' }}>15: 10: 205分26秒</span> | |||
| </div> | |||
| </div> | |||
| </Drawer> | |||
| ); | |||
| }); | |||
| export default PipelineNodeParameter; | |||
| @@ -0,0 +1,54 @@ | |||
| .collapse { | |||
| flex: none; | |||
| // width: 250px; | |||
| height: 100%; | |||
| padding: 20px 0 0 15px; | |||
| :global { | |||
| .ant-collapse { | |||
| height: calc(100% - 60px); | |||
| overflow-y: auto; | |||
| background-color: #fff; | |||
| border-color: transparent !important; | |||
| } | |||
| .ant-collapse > .ant-collapse-item > .ant-collapse-header { | |||
| margin-bottom: 5px; | |||
| padding: 20px 16px 15px 16px; | |||
| background-color: #fff; | |||
| border-color: transparent; | |||
| } | |||
| .ant-collapse > .ant-collapse-item { | |||
| margin: 0 10px; | |||
| border-bottom: 0.5px dashed rgba(20, 49, 179, 0.12); | |||
| border-radius: 0px; | |||
| } | |||
| .ant-collapse .ant-collapse-content { | |||
| padding-bottom: 15px; | |||
| border-top: 1px solid transparent; | |||
| } | |||
| .ant-collapse .ant-collapse-content > .ant-collapse-content-box { | |||
| padding: 0; | |||
| } | |||
| } | |||
| } | |||
| .collapseItem { | |||
| display: flex; | |||
| align-items: center; | |||
| height: 40px; | |||
| padding: 0 16px; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| &:hover { | |||
| color: @primary-color; | |||
| background: rgba(22, 100, 255, 0.08); | |||
| } | |||
| } | |||
| .modelMenusTitle { | |||
| margin-bottom: 10px; | |||
| padding: 12px 25px; | |||
| color: #111111; | |||
| font-size: 16px; | |||
| } | |||
| @@ -0,0 +1,108 @@ | |||
| import { getComponentAll } from '@/services/pipeline/index.js'; | |||
| import { PipelineNodeModel } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Button, Collapse, type CollapseProps, Input } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import Styles from './index.less'; | |||
| type ModelMenuData = { | |||
| key: string; | |||
| name: string; | |||
| value: PipelineNodeModel[]; | |||
| }; | |||
| type ModelMenuProps = { | |||
| onComponentDragEnd: ( | |||
| data: PipelineNodeModel & { x: number; y: number; label: string; img: string }, | |||
| ) => void; | |||
| }; | |||
| const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { | |||
| const [modelMenusList, setModelMenusList] = useState<ModelMenuData[]>([]); | |||
| const [collapseItems, setCollapseItems] = useState<CollapseProps['items']>([]); | |||
| useEffect(() => { | |||
| getAllComponents(); | |||
| }, []); | |||
| // 获取所有组件 | |||
| const getAllComponents = async () => { | |||
| const [res] = await to(getComponentAll()); | |||
| if (res && res.data) { | |||
| const menus = res.data as ModelMenuData[]; | |||
| setModelMenusList(menus); | |||
| const items = menus.map((item) => { | |||
| return { | |||
| key: item.key, | |||
| label: item.name, | |||
| children: item.value.map((ele) => { | |||
| return ( | |||
| <div | |||
| key={ele.id} | |||
| draggable="true" | |||
| onDragEnd={(e) => { | |||
| dragEnd(e, ele); | |||
| }} | |||
| className={Styles.collapseItem} | |||
| > | |||
| {ele.icon_path && ( | |||
| <img | |||
| style={{ height: '16px', marginRight: '15px' }} | |||
| src={`/assets/images/${ele.icon_path}.png`} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| )} | |||
| {ele.component_label} | |||
| </div> | |||
| ); | |||
| }), | |||
| }; | |||
| }); | |||
| setCollapseItems(items); | |||
| } | |||
| }; | |||
| const dragEnd = (e: React.DragEvent<HTMLDivElement>, data: PipelineNodeModel) => { | |||
| onComponentDragEnd({ | |||
| ...data, | |||
| x: e.clientX, | |||
| y: e.clientY, | |||
| label: data.component_label, | |||
| img: `/assets/images/${data.icon_path}.png`, | |||
| }); | |||
| }; | |||
| const defaultActiveKey = modelMenusList.map((item) => item.key + ''); | |||
| return ( | |||
| <div className={Styles.collapse}> | |||
| <Input.Search | |||
| placeholder="按XXXX筛选" | |||
| allowClear | |||
| // onSearch={onSearch} | |||
| // onChange={(e) => setInputText(e.target.value)} | |||
| style={{ width: 300 }} | |||
| // value={inputText} | |||
| /> | |||
| <Button | |||
| key="info" | |||
| type="primary" | |||
| style={{ marginLeft: '20px' }} | |||
| // onClick={() => toDetail(record)} | |||
| > | |||
| 查询 | |||
| </Button> | |||
| {/* <div className={Styles.modelMenusTitle}>组件库</div> */} | |||
| {/* 这样 defaultActiveKey 才能生效 */} | |||
| {modelMenusList.length > 0 ? ( | |||
| <Collapse | |||
| collapsible="header" | |||
| expandIconPosition="end" | |||
| defaultActiveKey={defaultActiveKey} | |||
| items={collapseItems} | |||
| ></Collapse> | |||
| ) : null} | |||
| </div> | |||
| ); | |||
| }; | |||
| export default ModelMenu; | |||
| @@ -0,0 +1,6 @@ | |||
| .props-label { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| width: 100%; | |||
| } | |||
| @@ -0,0 +1,44 @@ | |||
| import { Dropdown, type MenuProps } from 'antd'; | |||
| import { useEffect } from 'react'; | |||
| import styles from './index.less'; | |||
| type PropsLabelProps = { | |||
| title: string; | |||
| menuItems: MenuProps['items']; | |||
| onClick?: (key: string) => void; | |||
| }; | |||
| function PropsLabel({ title, menuItems, onClick }: PropsLabelProps) { | |||
| useEffect(() => {}, []); | |||
| const handleItemClick: MenuProps['onClick'] = (e) => { | |||
| const keyPath = e.keyPath.reverse(); | |||
| if (keyPath[0] === 'global') { | |||
| onClick?.(`\${${e.key}}`); | |||
| } else { | |||
| onClick?.(`{{${keyPath.join('.')}}}`); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles['props-label']}> | |||
| <div>{title}</div> | |||
| {menuItems && menuItems.length > 0 && ( | |||
| <Dropdown | |||
| menu={{ | |||
| items: menuItems, | |||
| onClick: handleItemClick, | |||
| triggerSubMenuAction: 'hover', | |||
| }} | |||
| trigger={['click']} | |||
| placement="topRight" | |||
| arrow | |||
| > | |||
| <a onClick={(e) => e.preventDefault()}>参数</a> | |||
| </Dropdown> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| export default PropsLabel; | |||
| @@ -0,0 +1,251 @@ | |||
| import datasetImg from '@/assets/img/modal-select-dataset.png'; | |||
| import mirrorImg from '@/assets/img/modal-select-mirror.png'; | |||
| import modelImg from '@/assets/img/modal-select-model.png'; | |||
| import { AvailableRange, CommonTabKeys } from '@/enums'; | |||
| import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config'; | |||
| import { MirrorVersionData } from '@/pages/Mirror/Info'; | |||
| import { MirrorData } from '@/pages/Mirror/List'; | |||
| import { | |||
| getDatasetInfo, | |||
| getDatasetList, | |||
| getDatasetVersionList, | |||
| getModelInfo, | |||
| getModelList, | |||
| getModelVersionList, | |||
| } from '@/services/dataset/index.js'; | |||
| import { getMirrorListReq, getMirrorVersionListReq } from '@/services/mirror'; | |||
| import type { TabsProps, TreeDataNode } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| export enum ResourceSelectorType { | |||
| Model = 'Model', // 模型 | |||
| Dataset = 'Dataset', // 数据集 | |||
| Mirror = 'Mirror', //镜像 | |||
| } | |||
| // 数据集、模型列表转为树形结构 | |||
| const convertDatasetToTreeData = (list: ResourceData[]): TreeDataNode[] => { | |||
| return list.map((v) => ({ | |||
| ...v, | |||
| key: `${v.id}`, | |||
| title: v.name, | |||
| isLeaf: false, | |||
| checkable: false, | |||
| })); | |||
| }; | |||
| // 镜像列表转为树形结构 | |||
| const convertMirrorToTreeData = (list: MirrorData[]): TreeDataNode[] => { | |||
| return list.map((v) => ({ | |||
| key: `${v.id}`, | |||
| title: v.name, | |||
| isLeaf: false, | |||
| checkable: false, | |||
| })); | |||
| }; | |||
| // 数据集版本列表转为树形结构 | |||
| const convertDatasetVersionToTreeData = ( | |||
| parentId: string, | |||
| info: ResourceData, | |||
| list: ResourceVersionData[], | |||
| ): TreeDataNode[] => { | |||
| return list.map((item: ResourceVersionData) => ({ | |||
| ...pick(info, ['id', 'name', 'owner', 'identifier']), | |||
| version: item.name, | |||
| title: item.name, | |||
| key: `${parentId}-${item.name}`, | |||
| isLeaf: true, | |||
| checkable: true, | |||
| })); | |||
| }; | |||
| // 镜像版本列表转为树形结构 | |||
| const convertMirrorVersionToTreeData = ( | |||
| parentId: string, | |||
| list: MirrorVersionData[], | |||
| ): TreeDataNode[] => { | |||
| return list.map((item: MirrorVersionData) => ({ | |||
| url: item.url, | |||
| title: item.tag_name, | |||
| key: `${parentId}-${item.id}`, | |||
| isLeaf: true, | |||
| checkable: true, | |||
| })); | |||
| }; | |||
| interface SelectorTypeInfo { | |||
| getList: (isPublic: boolean) => Promise<any>; // 获取资源列表 | |||
| getVersions: (parentKey: string, parentNode: any) => Promise<any>; // 获取资源版本列表 | |||
| getFiles: (parentKey: string, parentNode: any) => Promise<any>; // 获取资源文件列表 | |||
| readonly modalIcon: string; // modal icon | |||
| readonly buttonIcon: string; // button icon | |||
| readonly name: string; // 名称 | |||
| readonly tabItems: TabsProps['items']; // tab 列表 | |||
| readonly buttontTitle: string; // 按钮 title | |||
| } | |||
| export class DatasetSelector implements SelectorTypeInfo { | |||
| readonly name = '数据集'; | |||
| readonly modalIcon = datasetImg; | |||
| readonly buttonIcon = 'icon-xuanzeshujuji'; | |||
| readonly tabItems = [ | |||
| { | |||
| key: CommonTabKeys.Private, | |||
| label: '我的数据集', | |||
| }, | |||
| { | |||
| key: CommonTabKeys.Public, | |||
| label: '公开数据集', | |||
| }, | |||
| ]; | |||
| readonly buttontTitle = '选择数据集'; | |||
| async getList(isPublic: boolean) { | |||
| const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 }); | |||
| if (res && res.data) { | |||
| const list = res.data.content || []; | |||
| return convertDatasetToTreeData(list); | |||
| } else { | |||
| return Promise.reject('获取数据集列表失败'); | |||
| } | |||
| } | |||
| async getVersions(parentKey: string, parentNode: ResourceData) { | |||
| const res = await getDatasetVersionList(pick(parentNode, ['owner', 'identifier'])); | |||
| if (res && res.data) { | |||
| const list = res.data; | |||
| return convertDatasetVersionToTreeData(parentKey, parentNode, list); | |||
| } else { | |||
| return Promise.reject('获取数据集版本列表失败'); | |||
| } | |||
| } | |||
| async getFiles(_parentKey: string, parentNode: ResourceData & ResourceVersionData) { | |||
| const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version']); | |||
| const res = await getDatasetInfo(params); | |||
| if (res && res.data) { | |||
| const path = res.data.relative_paths || ''; | |||
| const list = res.data.dataset_version_vos || []; | |||
| return { | |||
| path, | |||
| content: list, | |||
| }; | |||
| } else { | |||
| return Promise.reject('获取数据集文件列表失败'); | |||
| } | |||
| } | |||
| } | |||
| export class ModelSelector implements SelectorTypeInfo { | |||
| readonly name = '模型'; | |||
| readonly modalIcon = modelImg; | |||
| readonly buttonIcon = 'icon-xuanzemoxing'; | |||
| readonly tabItems = [ | |||
| { | |||
| key: CommonTabKeys.Private, | |||
| label: '我的模型', | |||
| }, | |||
| { | |||
| key: CommonTabKeys.Public, | |||
| label: '公开模型', | |||
| }, | |||
| ]; | |||
| readonly buttontTitle = '选择模型'; | |||
| async getList(isPublic: boolean) { | |||
| const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 }); | |||
| if (res && res.data) { | |||
| const list = res.data.content || []; | |||
| return convertDatasetToTreeData(list); | |||
| } else { | |||
| return Promise.reject('获取模型列表失败'); | |||
| } | |||
| } | |||
| async getVersions(key: string, parentNode: ResourceData) { | |||
| const res = await getModelVersionList(pick(parentNode, ['owner', 'identifier'])); | |||
| if (res && res.data) { | |||
| const list = res.data; | |||
| return convertDatasetVersionToTreeData(key, parentNode, list); | |||
| } else { | |||
| return Promise.reject('获取模型版本列表失败'); | |||
| } | |||
| } | |||
| async getFiles(_parentKey: string, parentNode: ResourceData & ResourceVersionData) { | |||
| const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version']); | |||
| const res = await getModelInfo(params); | |||
| if (res && res.data) { | |||
| const path = res.data.relative_paths || ''; | |||
| const list = res.data.model_version_vos || []; | |||
| return { | |||
| path, | |||
| content: list, | |||
| }; | |||
| } else { | |||
| return Promise.reject('获取模型文件列表失败'); | |||
| } | |||
| } | |||
| } | |||
| export class MirrorSelector implements SelectorTypeInfo { | |||
| readonly name = '镜像'; | |||
| readonly modalIcon = mirrorImg; | |||
| readonly buttonIcon = 'icon-xuanzejingxiang'; | |||
| readonly tabItems = [ | |||
| { | |||
| key: CommonTabKeys.Private, | |||
| label: '我的镜像', | |||
| }, | |||
| { | |||
| key: CommonTabKeys.Public, | |||
| label: '公开镜像', | |||
| }, | |||
| ]; | |||
| readonly buttontTitle = '选择镜像'; | |||
| async getList(isPublic: boolean) { | |||
| const res = await getMirrorListReq({ | |||
| image_type: isPublic ? AvailableRange.Public : AvailableRange.Private, | |||
| page: 0, | |||
| size: 2000, | |||
| }); | |||
| if (res && res.data) { | |||
| const list = res.data.content || []; | |||
| return convertMirrorToTreeData(list); | |||
| } else { | |||
| return Promise.reject('获取镜像列表失败'); | |||
| } | |||
| } | |||
| async getVersions(parentKey: string) { | |||
| const res = await getMirrorVersionListReq({ | |||
| image_id: parentKey, | |||
| page: 0, | |||
| size: 2000, | |||
| }); | |||
| if (res && res.data) { | |||
| const list = res.data.content || []; | |||
| return convertMirrorVersionToTreeData(parentKey, list); | |||
| } else { | |||
| return Promise.reject('获取镜像版本列表失败'); | |||
| } | |||
| } | |||
| async getFiles(_parentKey: string, parentNode: MirrorVersionData) { | |||
| const { url } = parentNode; | |||
| return { | |||
| path: url, | |||
| content: [ | |||
| { | |||
| url: url, | |||
| file_name: `${url}`, | |||
| }, | |||
| ], | |||
| }; | |||
| } | |||
| } | |||
| export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> = { | |||
| [ResourceSelectorType.Model]: new ModelSelector(), | |||
| [ResourceSelectorType.Dataset]: new DatasetSelector(), | |||
| [ResourceSelectorType.Mirror]: new MirrorSelector(), | |||
| }; | |||
| @@ -0,0 +1,76 @@ | |||
| .model-tabs { | |||
| margin-left: 8px; | |||
| :global { | |||
| .ant-tabs-tab { | |||
| padding-top: 0; | |||
| } | |||
| } | |||
| } | |||
| .model-selector { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| :global { | |||
| .ant-input-affix-wrapper .ant-input-prefix { | |||
| margin-inline-end: 12px; | |||
| } | |||
| } | |||
| &__left { | |||
| width: 488px; | |||
| height: 398px; | |||
| margin-right: 15px; | |||
| padding: 15px; | |||
| background-color: @background-color-primary; | |||
| border: 1px solid @border-color; | |||
| border-radius: 8px; | |||
| &__search { | |||
| margin-bottom: 14px; | |||
| padding-left: 0; | |||
| background-color: transparent; | |||
| border-width: 0; | |||
| border-bottom: 1px solid @border-color-secondary; | |||
| border-radius: 0; | |||
| } | |||
| &__tree-title { | |||
| display: inline-block; | |||
| .singleLine(); | |||
| } | |||
| } | |||
| &__right { | |||
| width: calc(100% - 488px - 15px); | |||
| height: 398px; | |||
| padding: 15px; | |||
| background-color: @background-color-primary; | |||
| border: 1px solid @border-color; | |||
| border-radius: 8px; | |||
| &__title { | |||
| height: 46px; | |||
| margin-bottom: 15px; | |||
| padding: 3px 0 6px; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| line-height: 46px; | |||
| border-bottom: 1px solid @border-color-secondary; | |||
| } | |||
| &__files { | |||
| height: calc(100% - 75px); | |||
| overflow-y: auto; | |||
| &__file { | |||
| margin-bottom: 10px; | |||
| padding: 3px 10px; | |||
| color: @text-color-secondary; | |||
| font-size: 13px; | |||
| word-break: break-all; | |||
| background: @background-color-gray; | |||
| border-radius: 4px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,314 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-11 16:31:18 | |||
| * @Description: 选择数据集、模型、镜像 | |||
| */ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { ResourceFileData } from '@/pages/Dataset/config'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Icon } from '@umijs/max'; | |||
| import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; | |||
| import { Input, Tabs, Tree } from 'antd'; | |||
| import React, { useEffect, useMemo, useRef, useState } from 'react'; | |||
| import { ResourceSelectorType, selectorTypeConfig } from './config'; | |||
| import styles from './index.less'; | |||
| export { ResourceSelectorType, selectorTypeConfig }; | |||
| // 选择数据集\模型\镜像的返回类型 | |||
| export type ResourceSelectorResponse = { | |||
| id: string; // 数据集\模型\镜像 id | |||
| name: string; // 数据集\模型\镜像 name | |||
| version: string; // 数据集\模型\镜像版本 | |||
| path: string; // 数据集\模型\镜像版本路径 | |||
| identifier: string; // 数据集\模型 identifier | |||
| owner: string; // 数据集\模型 owner | |||
| activeTab: CommonTabKeys; // 是我的还是公开的 | |||
| }; | |||
| export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| type: ResourceSelectorType; // 数据集\模型\镜像 | |||
| defaultExpandedKeys?: React.Key[]; | |||
| defaultCheckedKeys?: React.Key[]; | |||
| defaultActiveTab?: CommonTabKeys; | |||
| onOk?: (params: ResourceSelectorResponse | undefined) => void; | |||
| } | |||
| type TreeRef = GetRef<typeof Tree<TreeDataNode>>; | |||
| // 更新树形结构的 children | |||
| const updateChildren = (parentId: string, children: TreeDataNode[]) => { | |||
| return (node: TreeDataNode) => { | |||
| if (node.key === parentId) { | |||
| return { | |||
| ...node, | |||
| children, | |||
| }; | |||
| } | |||
| return node; | |||
| }; | |||
| }; | |||
| // 得到数据集\模型\镜像 id 和下属版本号 | |||
| const getIdAndVersion = (versionKey: string) => { | |||
| const index = versionKey.indexOf('-'); | |||
| const id = versionKey.slice(0, index); | |||
| const version = versionKey.slice(index + 1); | |||
| return { | |||
| id, | |||
| version, | |||
| }; | |||
| }; | |||
| function ResourceSelectorModal({ | |||
| type, | |||
| defaultExpandedKeys = [], | |||
| defaultCheckedKeys = [], | |||
| defaultActiveTab = CommonTabKeys.Private, | |||
| onOk, | |||
| ...rest | |||
| }: ResourceSelectorModalProps) { | |||
| const [activeTab, setActiveTab] = useState<string>(defaultActiveTab); | |||
| const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]); | |||
| const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]); | |||
| const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); | |||
| const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]); | |||
| const [files, setFiles] = useState<ResourceFileData[]>([]); | |||
| const [versionPath, setVersionPath] = useState(''); | |||
| const [searchText, setSearchText] = useState(''); | |||
| const [firstLoadList, setFirstLoadList] = useState(false); | |||
| const [firstLoadVersions, setFirstLoadVersions] = useState(false); | |||
| const treeRef = useRef<TreeRef>(null); | |||
| const config = selectorTypeConfig[type]; | |||
| useEffect(() => { | |||
| setExpandedKeys([]); | |||
| setCheckedKeys([]); | |||
| setLoadedKeys([]); | |||
| setFiles([]); | |||
| setVersionPath(''); | |||
| setSearchText(''); | |||
| getTreeData(); | |||
| }, [activeTab, type]); | |||
| const treeData = useMemo( | |||
| () => | |||
| originTreeData.filter((v) => | |||
| (v.title as string).toLowerCase()?.includes(searchText.toLowerCase()), | |||
| ), | |||
| [originTreeData, searchText], | |||
| ); | |||
| // 获取数据集\模型\镜像列表 | |||
| const getTreeData = async () => { | |||
| const isPublic = activeTab === CommonTabKeys.Private ? false : true; | |||
| const [res] = await to(config.getList(isPublic)); | |||
| if (res) { | |||
| setOriginTreeData(res); | |||
| // 恢复上一次的 Expand 操作 | |||
| restoreLastExpand(); | |||
| } else { | |||
| setOriginTreeData([]); | |||
| } | |||
| }; | |||
| // 获取数据集\模型\镜像版本列表 | |||
| const getVersions = async (parentId: string, parentNode: any) => { | |||
| const [res, error] = await to(config.getVersions(parentId, parentNode)); | |||
| if (res) { | |||
| // 更新 treeData children | |||
| setOriginTreeData((prev) => prev.map(updateChildren(parentId, res))); | |||
| // 缓存 loadedKeys | |||
| const index = loadedKeys.find((v) => v === parentId); | |||
| if (!index) { | |||
| setLoadedKeys((prev) => prev.concat(parentId)); | |||
| } | |||
| // 恢复上一次的 Check 操作 | |||
| setTimeout(() => { | |||
| restoreLastCheck(parentId, res); | |||
| }, 300); | |||
| } else { | |||
| setExpandedKeys([]); | |||
| return Promise.reject(error); | |||
| } | |||
| }; | |||
| // 获取版本下的文件 | |||
| const getFiles = async (parentId: string, parentNode: any) => { | |||
| const [res] = await to(config.getFiles(parentId, parentNode)); | |||
| if (res) { | |||
| setVersionPath(res.path); | |||
| setFiles(res.content); | |||
| } else { | |||
| setVersionPath(''); | |||
| setFiles([]); | |||
| } | |||
| }; | |||
| // 动态加载 tree children | |||
| const onLoadData = ({ key, children, ...rest }: TreeDataNode) => { | |||
| if (children) { | |||
| return Promise.resolve(); | |||
| } else { | |||
| return getVersions(key as string, rest); | |||
| } | |||
| }; | |||
| // 扩展 | |||
| const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => { | |||
| const lastKeys = expandedKeysValue.slice(-1); | |||
| setExpandedKeys(lastKeys); | |||
| }; | |||
| // 选中 | |||
| const onCheck: TreeProps['onCheck'] = (checkedKeysValue, { checkedNodes }) => { | |||
| const lastKeys = (checkedKeysValue as React.Key[]).slice(-1); | |||
| setCheckedKeys(lastKeys); | |||
| if (lastKeys.length && checkedNodes.length) { | |||
| const last = lastKeys[0] as string; | |||
| const lastNode = checkedNodes[checkedNodes.length - 1]; | |||
| getFiles(last, lastNode); | |||
| } else { | |||
| setVersionPath(''); | |||
| setFiles([]); | |||
| } | |||
| }; | |||
| // 恢复上一次的 Expand 操作 | |||
| // 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys | |||
| // fisrtLoadList 标志位 | |||
| const restoreLastExpand = () => { | |||
| if (!firstLoadList && defaultExpandedKeys.length > 0) { | |||
| setTimeout(() => { | |||
| setExpandedKeys(defaultExpandedKeys); | |||
| setFirstLoadList(true); | |||
| setTimeout(() => { | |||
| treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' }); | |||
| }, 100); | |||
| }, 0); | |||
| } | |||
| }; | |||
| // 恢复上一次的 Check 操作 | |||
| // 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口 | |||
| // fisrtLoadVersions 标志位 | |||
| const restoreLastCheck = (parentId: string, versions: TreeDataNode[]) => { | |||
| if (!firstLoadVersions && defaultCheckedKeys.length > 0) { | |||
| const last = defaultCheckedKeys[0] as string; | |||
| const { id } = getIdAndVersion(last); | |||
| // 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致 | |||
| if (id === parentId) { | |||
| setTimeout(() => { | |||
| setCheckedKeys(defaultCheckedKeys); | |||
| const parentNode = versions.find((v) => v.key === last); | |||
| getFiles(last, parentNode); | |||
| setFirstLoadVersions(true); | |||
| setTimeout(() => { | |||
| treeRef?.current?.scrollTo({ | |||
| key: defaultCheckedKeys[0], | |||
| align: 'bottom', | |||
| }); | |||
| }, 100); | |||
| }, 0); | |||
| } | |||
| } | |||
| }; | |||
| // 提交 | |||
| const handleOk = () => { | |||
| if (checkedKeys.length > 0) { | |||
| const last = checkedKeys[0] as string; | |||
| const { id, version } = getIdAndVersion(last); | |||
| const treeNode = treeData.find((v) => v.key === id) as any; | |||
| const name = (treeNode?.title ?? '') as string; | |||
| const identifier = (treeNode?.identifier ?? '') as string; | |||
| const owner = (treeNode?.owner ?? '') as string; | |||
| const res = { | |||
| id, | |||
| name, | |||
| path: versionPath, | |||
| version, | |||
| identifier, | |||
| owner, | |||
| activeTab: activeTab as CommonTabKeys, | |||
| }; | |||
| onOk?.(res); | |||
| } else { | |||
| onOk?.(undefined); | |||
| } | |||
| }; | |||
| const title = `选择${config.name}`; | |||
| const palceholder = `请输入${config.name}名称`; | |||
| const fileTitle = | |||
| type === ResourceSelectorType.Mirror ? '已选镜像' : `已选${config.name}文件(${files.length})`; | |||
| const tabItems = config.tabItems; | |||
| const titleImg = config.modalIcon; | |||
| return ( | |||
| <KFModal {...rest} title={title} image={titleImg} onOk={handleOk} width={920} destroyOnClose> | |||
| <div> | |||
| <Tabs | |||
| activeKey={activeTab} | |||
| items={tabItems} | |||
| onChange={setActiveTab} | |||
| className={styles['model-tabs']} | |||
| /> | |||
| <div className={styles['model-selector']}> | |||
| <div className={styles['model-selector__left']}> | |||
| <Input | |||
| className={styles['model-selector__left__search']} | |||
| placeholder={palceholder} | |||
| allowClear | |||
| variant="borderless" | |||
| value={searchText} | |||
| onChange={(e) => setSearchText(e.target.value)} | |||
| prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />} | |||
| /> | |||
| <Tree | |||
| ref={treeRef} | |||
| rootStyle={{ backgroundColor: 'transparent' }} | |||
| loadData={onLoadData} | |||
| treeData={treeData} | |||
| onCheck={onCheck} | |||
| checkedKeys={checkedKeys} | |||
| multiple={false} | |||
| selectable={false} | |||
| height={324} | |||
| loadedKeys={loadedKeys} | |||
| expandedKeys={expandedKeys} | |||
| onExpand={onExpand} | |||
| checkable | |||
| titleRender={(nodeData) => { | |||
| return ( | |||
| <span | |||
| className={styles['model-selector__left__tree-title']} | |||
| style={{ width: nodeData.isLeaf ? '370px' : '420px' }} | |||
| > | |||
| {nodeData.title as string} | |||
| </span> | |||
| ); | |||
| }} | |||
| /> | |||
| </div> | |||
| <div className={styles['model-selector__right']}> | |||
| <div className={styles['model-selector__right__title']}>{fileTitle}</div> | |||
| <div className={styles['model-selector__right__files']}> | |||
| {files.map((v) => ( | |||
| <div key={v.url} className={styles['model-selector__right__files__file']}> | |||
| {v.file_name} | |||
| </div> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default ResourceSelectorModal; | |||
| @@ -0,0 +1,91 @@ | |||
| .subject-drawer { | |||
| :global { | |||
| label { | |||
| width: 100%; | |||
| &::after { | |||
| display: none; | |||
| } | |||
| } | |||
| } | |||
| &__title { | |||
| display: flex; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 43px; | |||
| margin-bottom: 20px; | |||
| padding: 0 24px; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| background: #f8fbff; | |||
| } | |||
| &__ref-row { | |||
| display: flex; | |||
| align-items: center; | |||
| &__select-button { | |||
| display: flex; | |||
| flex: none; | |||
| align-items: center; | |||
| justify-content: flex-start; | |||
| margin-left: 10px; | |||
| padding-right: 0; | |||
| padding-left: 0; | |||
| } | |||
| } | |||
| .subject-drawer{ | |||
| height:58px; | |||
| background:#ffffff; | |||
| border:1px solid; | |||
| border-color:#e6e6e6; | |||
| border-radius:6px; | |||
| display: flex; | |||
| align-items: center; | |||
| &__left{ | |||
| width: 40px; | |||
| height: 100%; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| &__circle{ | |||
| width:17px; | |||
| height:17px; | |||
| border-radius: 50%; | |||
| background:#f98e1b; | |||
| } | |||
| } | |||
| &__slice{ | |||
| width:0px; | |||
| height:57px; | |||
| border:1px solid; | |||
| border-color:#e6e6e6; | |||
| } | |||
| &__right{ | |||
| padding: 0 10px; | |||
| display: flex; | |||
| align-items: center; | |||
| &__circle{ | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| width:18px; | |||
| height:18px; | |||
| background:#fff; | |||
| margin-right: 8px; | |||
| border-radius: 50%; | |||
| cursor: pointer; | |||
| border:1px solid; | |||
| &__center{ | |||
| width: 14px; | |||
| height: 14px; | |||
| border-radius: 50%; | |||
| background-color: #164fff; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,234 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { | |||
| subjectEdgeModelSerialize, | |||
| subjectGlobalParam, | |||
| subjectNodeModel, | |||
| subjectNodeModelParameter, | |||
| } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { INode } from '@antv/g6'; | |||
| import { Drawer, Form, Input, MenuProps } from 'antd'; | |||
| import { NamePath } from 'antd/es/form/interface'; | |||
| import { forwardRef, useImperativeHandle, useState } from 'react'; | |||
| import PropsLabel from '../PropsLabel'; | |||
| import { ResourceSelectorType, selectorTypeConfig } from '../ResourceSelectorModal'; | |||
| import styles from './index.less'; | |||
| const { TextArea } = Input; | |||
| export type ServiceData = { | |||
| name: string; | |||
| isMultivalued: string; | |||
| type: string; | |||
| flag: boolean; | |||
| }; | |||
| type subjectNodeParameterProps = { | |||
| onFormChange: (data: subjectEdgeModelSerialize) => void; | |||
| }; | |||
| const subjectNodeParameter = forwardRef(({ onFormChange }: subjectNodeParameterProps, ref) => { | |||
| const [form] = Form.useForm(); | |||
| const [stagingItem, setStagingItem] = useState<subjectEdgeModelSerialize>( | |||
| {} as subjectEdgeModelSerialize, | |||
| ); | |||
| const [open, setOpen] = useState(false); | |||
| const [typeList, setTypeList] = useState([]); | |||
| const [singleTypeList, setSingleTypeList] = useState([]); | |||
| const [doubleTypeList, setDoubleTypeList] = useState([]); | |||
| const [tableData, setTableData] = useState([]); | |||
| const [formId, setFormId] = useState(''); | |||
| const [circleColor, setCircleColor] = useState(0); | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 | |||
| const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | |||
| const afterOpenChange = async () => { | |||
| if (!open) { | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_values, error] = await to(form.validateFields()); | |||
| console.log(_values); | |||
| const fields = form.getFieldsValue(); | |||
| console.log(fields); | |||
| // const out_parameters = JSON.stringify(fields.out_parameters); | |||
| // console.log('getFieldsValue', fields); | |||
| const res = { | |||
| ...stagingItem, | |||
| ...fields, | |||
| id: formId, | |||
| }; | |||
| console.log('res', res); | |||
| onFormChange(res); | |||
| } | |||
| }; | |||
| const onClose = () => { | |||
| setOpen(false); | |||
| }; | |||
| useImperativeHandle(ref, () => ({ | |||
| showDrawer( | |||
| model: subjectNodeModel, | |||
| params: subjectGlobalParam[], | |||
| parentNodes: INode[], | |||
| validate: boolean = false, | |||
| ) { | |||
| try { | |||
| const edgeData: subjectEdgeModelSerialize = { | |||
| ...model, | |||
| label: model.label, | |||
| id: model.id, | |||
| source: model.source, | |||
| labelCfg: model.labelCfg, | |||
| curveOffset: model.curveOffset, | |||
| curvePosition: model.curvePosition, | |||
| endPoint: model.endPoint, | |||
| sourceAnchor: model.sourceAnchor, | |||
| startPoint: model.startPoint, | |||
| style: model.style, | |||
| target: model.target, | |||
| type: model.type, | |||
| // color: JSON.parse(model.color), | |||
| // control_strategy: JSON.parse(model.control_strategy), | |||
| }; | |||
| // console.log('model', edgeData); | |||
| // setStagingItem({ | |||
| // ...edgeData, | |||
| // }); | |||
| console.log(edgeData); | |||
| setCircleColor(model.color || 0); | |||
| form.resetFields(); | |||
| form.setFieldsValue({ | |||
| ...edgeData, | |||
| }); | |||
| console.log(form.getFieldsValue()); | |||
| setTableData(model.properties); | |||
| setFormId(model.id); | |||
| console.log(form.getFieldsValue()); | |||
| if (validate) { | |||
| form.validateFields(); | |||
| } | |||
| } catch (error) { | |||
| console.error('JSON.parse error: ', error); | |||
| } | |||
| setOpen(true); | |||
| // 参数下拉菜单 | |||
| // setMenuItems(createMenuItems(params, parentNodes)); | |||
| }, | |||
| close: () => { | |||
| onClose(); | |||
| }, | |||
| validateFields: async () => { | |||
| if (!open) { | |||
| return; | |||
| } | |||
| const [values, error] = await to(form.validateFields()); | |||
| if (!error && values) { | |||
| return values; | |||
| } else { | |||
| form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' }); | |||
| return Promise.reject(error); | |||
| } | |||
| }, | |||
| })); | |||
| // 获取选择数据集、模型后面按钮 icon | |||
| const getSelectBtnIcon = (item: { item_type: string }) => { | |||
| const type = item.item_type; | |||
| if (type === 'code') { | |||
| return <KFIcon type="icon-xuanzedaimapeizhi" />; | |||
| } | |||
| let selectorType: ResourceSelectorType; | |||
| if (type === 'dataset') { | |||
| selectorType = ResourceSelectorType.Dataset; | |||
| } else if (type === 'model') { | |||
| selectorType = ResourceSelectorType.Model; | |||
| } else { | |||
| selectorType = ResourceSelectorType.Mirror; | |||
| } | |||
| return <KFIcon type={selectorTypeConfig[selectorType].buttonIcon} />; | |||
| }; | |||
| // 参数回填 | |||
| const handleParameterClick = (name: NamePath, value: any) => { | |||
| form.setFieldValue(name, value); | |||
| }; | |||
| // form item label | |||
| const getLabel = ( | |||
| item: { key: string; value: subjectNodeModelParameter }, | |||
| namePrefix: string, | |||
| ) => { | |||
| return item.value.type === 'select' ? ( | |||
| item.value.label + '(' + item.key + ')' | |||
| ) : ( | |||
| <PropsLabel | |||
| menuItems={menuItems} | |||
| title={item.value.label + '(' + item.key + ')'} | |||
| onClick={(value) => { | |||
| handleParameterClick([namePrefix, item.key], { | |||
| ...item.value, | |||
| value, | |||
| fromSelect: true, | |||
| showValue: value, | |||
| }); | |||
| }} | |||
| /> | |||
| ); | |||
| }; | |||
| return ( | |||
| <Drawer | |||
| title="关系" | |||
| placement="right" | |||
| rootStyle={{ marginTop: '52px' }} | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| onClose={onClose} | |||
| afterOpenChange={afterOpenChange} | |||
| open={open} | |||
| width={500} | |||
| className={styles['subject-drawer']} | |||
| > | |||
| <Form | |||
| name="form" | |||
| form={form} | |||
| style={{ | |||
| maxWidth: 600, | |||
| height: '100%', | |||
| }} | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| > | |||
| {/* <div className={styles['subject-drawer__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/static-message.png')} | |||
| title="基本信息" | |||
| ></SubAreaTitle> | |||
| </div> */} | |||
| <Form.Item | |||
| label="关系名称" | |||
| name="label" | |||
| labelCol={{ span: 6 }} | |||
| wrapperCol={{ span: 18 }} | |||
| layout="horizontal" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入概念名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入概念名称" allowClear /> | |||
| </Form.Item> | |||
| </Form> | |||
| </Drawer> | |||
| ); | |||
| }); | |||
| export default subjectNodeParameter; | |||
| @@ -0,0 +1,91 @@ | |||
| .subject-drawer { | |||
| :global { | |||
| label { | |||
| width: 100%; | |||
| &::after { | |||
| display: none; | |||
| } | |||
| } | |||
| } | |||
| &__title { | |||
| display: flex; | |||
| align-items: center; | |||
| width: 100%; | |||
| height: 43px; | |||
| margin-bottom: 20px; | |||
| padding: 0 24px; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| background: #f8fbff; | |||
| } | |||
| &__ref-row { | |||
| display: flex; | |||
| align-items: center; | |||
| &__select-button { | |||
| display: flex; | |||
| flex: none; | |||
| align-items: center; | |||
| justify-content: flex-start; | |||
| margin-left: 10px; | |||
| padding-right: 0; | |||
| padding-left: 0; | |||
| } | |||
| } | |||
| .subject-drawer{ | |||
| height:58px; | |||
| background:#ffffff; | |||
| border:1px solid; | |||
| border-color:#e6e6e6; | |||
| border-radius:6px; | |||
| display: flex; | |||
| align-items: center; | |||
| &__left{ | |||
| width: 40px; | |||
| height: 100%; | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| &__circle{ | |||
| width:17px; | |||
| height:17px; | |||
| border-radius: 50%; | |||
| background:#f98e1b; | |||
| } | |||
| } | |||
| &__slice{ | |||
| width:0px; | |||
| height:57px; | |||
| border:1px solid; | |||
| border-color:#e6e6e6; | |||
| } | |||
| &__right{ | |||
| padding: 0 10px; | |||
| display: flex; | |||
| align-items: center; | |||
| &__circle{ | |||
| display: flex; | |||
| justify-content: center; | |||
| align-items: center; | |||
| width:18px; | |||
| height:18px; | |||
| background:#fff; | |||
| margin-right: 8px; | |||
| border-radius: 50%; | |||
| cursor: pointer; | |||
| border:1px solid; | |||
| &__center{ | |||
| width: 14px; | |||
| height: 14px; | |||
| border-radius: 50%; | |||
| background-color: #164fff; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,635 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { getSubjectType } from '@/services/subject'; | |||
| import { | |||
| subjectGlobalParam, | |||
| subjectNodeModel, | |||
| subjectNodeModelParameter, | |||
| subjectNodeModelSerialize, | |||
| } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { INode } from '@antv/g6'; | |||
| import { Button, Drawer, Form, Input, MenuProps, Select, Table, type TableProps } from 'antd'; | |||
| import { NamePath } from 'antd/es/form/interface'; | |||
| import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; | |||
| import PropsLabel from '../PropsLabel'; | |||
| import { ResourceSelectorType, selectorTypeConfig } from '../ResourceSelectorModal'; | |||
| import styles from './index.less'; | |||
| const { TextArea } = Input; | |||
| export type ServiceData = { | |||
| name: string; | |||
| isMultivalued: string; | |||
| type: string; | |||
| flag: boolean; | |||
| }; | |||
| type subjectNodeParameterProps = { | |||
| onFormChange: (data: subjectNodeModelSerialize) => void; | |||
| }; | |||
| const subjectNodeParameter = forwardRef(({ onFormChange }: subjectNodeParameterProps, ref) => { | |||
| const isMultivaluedList = [ | |||
| { label: '单值', value: 'kg_entity_property_single_value' }, | |||
| { label: '双值', value: 'kg_entity_property_multi_value' }, | |||
| ]; | |||
| const [form] = Form.useForm(); | |||
| const [stagingItem, setStagingItem] = useState<subjectNodeModelSerialize>( | |||
| {} as subjectNodeModelSerialize, | |||
| ); | |||
| const [open, setOpen] = useState(false); | |||
| const [typeList, setTypeList] = useState([]); | |||
| const [singleTypeList, setSingleTypeList] = useState([]); | |||
| const [doubleTypeList, setDoubleTypeList] = useState([]); | |||
| const [tableData, setTableData] = useState([]); | |||
| const [formId, setFormId] = useState(''); | |||
| const [circleColor, setCircleColor] = useState(0); | |||
| const [resourceStandardList, filterResourceStandard] = useComputingResource(); // 资源规模 | |||
| const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); | |||
| useEffect(() => { | |||
| getSubjectType('kg_entity_property_single_value').then((ret) => { | |||
| setSingleTypeList(ret.data); | |||
| }); | |||
| getSubjectType('kg_entity_property_multi_value').then((ret) => { | |||
| setDoubleTypeList(ret.data); | |||
| }); | |||
| }, []); | |||
| const columns: TableProps<ServiceData>['columns'] = [ | |||
| { | |||
| title: '名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '20%', | |||
| render: (text, record, index) => { | |||
| if (record.flag) { | |||
| return ( | |||
| <Input | |||
| onChange={(e) => { | |||
| console.log(e, index, record); | |||
| const newData = [...tableData]; | |||
| newData[index].name = e.target.value; | |||
| setTableData(newData); | |||
| }} | |||
| defaultValue={record.name} | |||
| autoFocus | |||
| /> | |||
| ); | |||
| } else { | |||
| return record.name; | |||
| } | |||
| }, | |||
| }, | |||
| { | |||
| title: '单值/多值', | |||
| dataIndex: 'isMultivalued', | |||
| key: 'isMultivalued', | |||
| width: '25%', | |||
| render: (text, record, index) => { | |||
| if (record.flag) { | |||
| return ( | |||
| <Select | |||
| allowClear | |||
| onChange={(e) => { | |||
| console.log(e, index, record); | |||
| const newData = [...tableData]; | |||
| newData[index].isMultivalued = e; | |||
| setTableData(newData); | |||
| if (e == 'kg_entity_property_single_value') { | |||
| setTypeList(singleTypeList); | |||
| } else { | |||
| setTypeList(doubleTypeList); | |||
| } | |||
| // getSubjectType(e).then((ret) => { | |||
| // setTypeList(ret.data); | |||
| // }); | |||
| }} | |||
| options={isMultivaluedList} | |||
| fieldNames={{ label: 'label', value: 'value' }} | |||
| optionFilterProp="name" | |||
| showSearch | |||
| /> | |||
| ); | |||
| } else { | |||
| console.log(isMultivaluedList.filter((item) => item.value == record.isMultivalued)[0]); | |||
| return isMultivaluedList.filter((item) => item.value == record.isMultivalued)[0]?.label; | |||
| } | |||
| }, | |||
| }, | |||
| { | |||
| title: '类型', | |||
| dataIndex: 'type', | |||
| key: 'type', | |||
| width: '30%', | |||
| render: (text, record, index) => { | |||
| if (record.flag) { | |||
| return ( | |||
| <Select | |||
| allowClear | |||
| onChange={(e) => { | |||
| console.log(e, index, record); | |||
| const newData = [...tableData]; | |||
| newData[index].type = e; | |||
| setTableData(newData); | |||
| }} | |||
| options={typeList} | |||
| fieldNames={{ label: 'dictLabel', value: 'dictValue' }} | |||
| optionFilterProp="name" | |||
| showSearch | |||
| /> | |||
| ); | |||
| } else { | |||
| return record.isMultivalued && record.isMultivalued == 'kg_entity_property_single_value' | |||
| ? singleTypeList.filter((item) => item.dictValue == record.type)[0]?.dictLabel | |||
| : doubleTypeList.filter((item) => item.dictValue == record.type)[0]?.dictLabel; | |||
| } | |||
| }, | |||
| }, | |||
| { | |||
| title: '操作', | |||
| dataIndex: 'operation', | |||
| width: '30%', | |||
| key: 'operation', | |||
| render: (_: any, record: any, index: any) => | |||
| record.flag ? ( | |||
| <div> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="edit" | |||
| onClick={() => { | |||
| editTable(record, index); | |||
| }} | |||
| icon={<KFIcon type="icon-baocunbingfanhui" />} | |||
| ></Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="run" | |||
| icon={<KFIcon type="icon-fuzhi" />} | |||
| onClick={() => { | |||
| cloneTable(record, index); | |||
| }} | |||
| ></Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="run" | |||
| onClick={() => { | |||
| deleteTable(record, index); | |||
| }} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| ></Button> | |||
| </div> | |||
| ) : ( | |||
| <div> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="edit" | |||
| onClick={() => { | |||
| editTable(record, index); | |||
| }} | |||
| icon={<KFIcon type="icon-bianji" />} | |||
| ></Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="run" | |||
| icon={<KFIcon type="icon-fuzhi" />} | |||
| onClick={() => { | |||
| cloneTable(record, index); | |||
| }} | |||
| ></Button> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="run" | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| onClick={() => { | |||
| deleteTable(record, index); | |||
| }} | |||
| ></Button> | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| const editTable = (val: any, index: any) => { | |||
| tableData[index] = { | |||
| ...tableData[index], | |||
| flag: !val.flag, | |||
| }; | |||
| const newTableData = JSON.parse(JSON.stringify(tableData)); | |||
| setTableData(newTableData); | |||
| console.log(tableData); | |||
| }; | |||
| const addTable = () => { | |||
| setTableData([ | |||
| ...tableData, | |||
| { | |||
| name: '', | |||
| isMultivalued: '', | |||
| type: '', | |||
| flag: false, | |||
| }, | |||
| ]); | |||
| }; | |||
| const cloneTable = (val, index) => { | |||
| setTableData([...tableData, val]); | |||
| }; | |||
| const deleteTable = (val, index) => { | |||
| tableData.splice(index, 1); | |||
| setTableData(JSON.parse(JSON.stringify(tableData))); | |||
| }; | |||
| const afterOpenChange = async () => { | |||
| if (!open) { | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_values, error] = await to(form.validateFields()); | |||
| console.log(_values); | |||
| const fields = form.getFieldsValue(); | |||
| const label = fields.label; | |||
| const color = circleColor; | |||
| // const out_parameters = JSON.stringify(fields.out_parameters); | |||
| // console.log('getFieldsValue', fields); | |||
| const res = { | |||
| ...stagingItem, | |||
| ...fields, | |||
| properties: tableData, | |||
| label, | |||
| color, | |||
| id: formId, | |||
| }; | |||
| console.log('res', res); | |||
| onFormChange(res); | |||
| } | |||
| }; | |||
| const onClose = () => { | |||
| setOpen(false); | |||
| }; | |||
| const changeColor = (val: any) => { | |||
| console.log(val); | |||
| setCircleColor(val); | |||
| form.setFieldsValue({ | |||
| color: circleColor, | |||
| }); | |||
| }; | |||
| useImperativeHandle(ref, () => ({ | |||
| showDrawer( | |||
| model: subjectNodeModel, | |||
| params: subjectGlobalParam[], | |||
| parentNodes: INode[], | |||
| validate: boolean = false, | |||
| ) { | |||
| try { | |||
| const nodeData: subjectNodeModelSerialize = { | |||
| ...model, | |||
| label: model.label, | |||
| id: model.id, | |||
| // color: JSON.parse(model.color), | |||
| // control_strategy: JSON.parse(model.control_strategy), | |||
| }; | |||
| // console.log('model', nodeData); | |||
| // setStagingItem({ | |||
| // ...nodeData, | |||
| // }); | |||
| console.log(model); | |||
| setCircleColor(model.color || 0); | |||
| form.resetFields(); | |||
| form.setFieldsValue({ | |||
| ...nodeData, | |||
| }); | |||
| setTableData(model.properties); | |||
| setFormId(model.id); | |||
| console.log(form.getFieldsValue()); | |||
| if (validate) { | |||
| form.validateFields(); | |||
| } | |||
| } catch (error) { | |||
| console.error('JSON.parse error: ', error); | |||
| } | |||
| setOpen(true); | |||
| // 参数下拉菜单 | |||
| // setMenuItems(createMenuItems(params, parentNodes)); | |||
| }, | |||
| close: () => { | |||
| onClose(); | |||
| }, | |||
| validateFields: async () => { | |||
| if (!open) { | |||
| return; | |||
| } | |||
| const [values, error] = await to(form.validateFields()); | |||
| if (!error && values) { | |||
| return values; | |||
| } else { | |||
| form.scrollToField((error as any)?.errorFields?.[0]?.name, { block: 'center' }); | |||
| return Promise.reject(error); | |||
| } | |||
| }, | |||
| })); | |||
| // ref 类型选择 | |||
| // const selectRefData = ( | |||
| // formItemName: NamePath, | |||
| // item: subjectNodeModelParameter | Pick<subjectNodeModelParameter, 'item_type'>, | |||
| // ) => { | |||
| // if (item.item_type === 'code') { | |||
| // selectCodeConfig(formItemName, item); | |||
| // } else { | |||
| // selectResource(formItemName, item); | |||
| // } | |||
| // }; | |||
| // // 选择代码配置 | |||
| // const selectCodeConfig = ( | |||
| // formItemName: NamePath, | |||
| // item: subjectNodeModelParameter | Pick<subjectNodeModelParameter, 'item_type'>, | |||
| // ) => { | |||
| // const { close } = openAntdModal(CodeSelectorModal, { | |||
| // onOk: (res) => { | |||
| // if (res) { | |||
| // const value = JSON.stringify({ | |||
| // id: res.id, | |||
| // name: res.code_repo_name, | |||
| // code_path: res.git_url, | |||
| // branch: res.git_branch, | |||
| // username: res.git_user_name, | |||
| // password: res.git_password, | |||
| // ssh_private_key: res.ssh_key, | |||
| // }); | |||
| // form.setFieldValue(formItemName, { | |||
| // ...item, | |||
| // value, | |||
| // showValue: res.code_repo_name, | |||
| // fromSelect: true, | |||
| // }); | |||
| // } | |||
| // close(); | |||
| // }, | |||
| // }); | |||
| // }; | |||
| // // 选择数据集、模型、镜像 | |||
| // const selectResource = ( | |||
| // formItemName: NamePath, | |||
| // item: subjectNodeModelParameter | Pick<subjectNodeModelParameter, 'item_type'>, | |||
| // ) => { | |||
| // let type: ResourceSelectorType; | |||
| // switch (item.item_type) { | |||
| // case 'dataset': | |||
| // type = ResourceSelectorType.Dataset; | |||
| // break; | |||
| // case 'model': | |||
| // type = ResourceSelectorType.Model; | |||
| // break; | |||
| // default: | |||
| // type = ResourceSelectorType.Mirror; | |||
| // break; | |||
| // } | |||
| // const fieldValue = form.getFieldValue(formItemName); | |||
| // const activeTab = fieldValue?.activeTab as CommonTabKeys | undefined; | |||
| // const expandedKeys = Array.isArray(fieldValue?.expandedKeys) ? fieldValue?.expandedKeys : []; | |||
| // const checkedKeys = Array.isArray(fieldValue?.checkedKeys) ? fieldValue?.checkedKeys : []; | |||
| // const { close } = openAntdModal(ResourceSelectorModal, { | |||
| // type, | |||
| // defaultExpandedKeys: expandedKeys, | |||
| // defaultCheckedKeys: checkedKeys, | |||
| // defaultActiveTab: activeTab, | |||
| // onOk: (res) => { | |||
| // // if (res) { | |||
| // // if (type === ResourceSelectorType.Mirror) { | |||
| // // const { activeTab, id, version, path } = res; | |||
| // // if (formItemName === 'image') { | |||
| // // // 单独的选择镜像 | |||
| // // form.setFieldValue(formItemName, path); | |||
| // // } else { | |||
| // // // 输入参数选择镜像 | |||
| // // form.setFieldValue(formItemName, { | |||
| // // ...item, | |||
| // // value: path, | |||
| // // showValue: path, | |||
| // // fromSelect: true, | |||
| // // activeTab, | |||
| // // expandedKeys: [id], | |||
| // // checkedKeys: [`${id}-${version}`], | |||
| // // }); | |||
| // // } | |||
| // // } else { | |||
| // // const { activeTab, id, name, version, path, identifier, owner } = res; | |||
| // // const value = JSON.stringify({ | |||
| // // id, | |||
| // // name, | |||
| // // version, | |||
| // // path, | |||
| // // identifier, | |||
| // // owner, | |||
| // // }); | |||
| // // const showValue = `${name}:${version}`; | |||
| // // form.setFieldValue(formItemName, { | |||
| // // ...item, | |||
| // // value, | |||
| // // showValue, | |||
| // // fromSelect: true, | |||
| // // activeTab, | |||
| // // expandedKeys: [id], | |||
| // // checkedKeys: [`${id}-${version}`], | |||
| // // }); | |||
| // // } | |||
| // // } else { | |||
| // // if (type === ResourceSelectorType.Mirror && formItemName === 'image') { | |||
| // // form.setFieldValue(formItemName, undefined); | |||
| // // } else { | |||
| // // form.setFieldValue(formItemName, { | |||
| // // ...item, | |||
| // // value: undefined, | |||
| // // showValue: undefined, | |||
| // // fromSelect: false, | |||
| // // activeTab: undefined, | |||
| // // expandedKeys: [], | |||
| // // checkedKeys: [], | |||
| // // }); | |||
| // // } | |||
| // // } | |||
| // // form.validateFields([formItemName]); | |||
| // // close(); | |||
| // }, | |||
| // }); | |||
| // }; | |||
| // 获取选择数据集、模型后面按钮 icon | |||
| const getSelectBtnIcon = (item: { item_type: string }) => { | |||
| const type = item.item_type; | |||
| if (type === 'code') { | |||
| return <KFIcon type="icon-xuanzedaimapeizhi" />; | |||
| } | |||
| let selectorType: ResourceSelectorType; | |||
| if (type === 'dataset') { | |||
| selectorType = ResourceSelectorType.Dataset; | |||
| } else if (type === 'model') { | |||
| selectorType = ResourceSelectorType.Model; | |||
| } else { | |||
| selectorType = ResourceSelectorType.Mirror; | |||
| } | |||
| return <KFIcon type={selectorTypeConfig[selectorType].buttonIcon} />; | |||
| }; | |||
| // 参数回填 | |||
| const handleParameterClick = (name: NamePath, value: any) => { | |||
| form.setFieldValue(name, value); | |||
| }; | |||
| // form item label | |||
| const getLabel = ( | |||
| item: { key: string; value: subjectNodeModelParameter }, | |||
| namePrefix: string, | |||
| ) => { | |||
| return item.value.type === 'select' ? ( | |||
| item.value.label + '(' + item.key + ')' | |||
| ) : ( | |||
| <PropsLabel | |||
| menuItems={menuItems} | |||
| title={item.value.label + '(' + item.key + ')'} | |||
| onClick={(value) => { | |||
| handleParameterClick([namePrefix, item.key], { | |||
| ...item.value, | |||
| value, | |||
| fromSelect: true, | |||
| showValue: value, | |||
| }); | |||
| }} | |||
| /> | |||
| ); | |||
| }; | |||
| return ( | |||
| <Drawer | |||
| title="概念" | |||
| placement="right" | |||
| rootStyle={{ marginTop: '52px' }} | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| onClose={onClose} | |||
| afterOpenChange={afterOpenChange} | |||
| open={open} | |||
| width={500} | |||
| className={styles['subject-drawer']} | |||
| > | |||
| <Form | |||
| name="form" | |||
| form={form} | |||
| style={{ | |||
| maxWidth: 600, | |||
| height: '100%', | |||
| }} | |||
| autoComplete="off" | |||
| scrollToFirstError | |||
| > | |||
| {/* <div className={styles['subject-drawer__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/static-message.png')} | |||
| title="基本信息" | |||
| ></SubAreaTitle> | |||
| </div> */} | |||
| <Form.Item | |||
| label="概念名称" | |||
| name="label" | |||
| labelCol={{ span: 6 }} | |||
| wrapperCol={{ span: 18 }} | |||
| layout="horizontal" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入概念名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入概念名称" allowClear /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="ICON" | |||
| name="color" | |||
| labelCol={{ span: 6 }} | |||
| wrapperCol={{ span: 18 }} | |||
| layout="horizontal" | |||
| > | |||
| <div className={styles['subject-drawer']}> | |||
| <div className={styles['subject-drawer__left']}> | |||
| <div | |||
| className={styles['subject-drawer__left__circle']} | |||
| style={{ backgroundColor: circleColor == 1 ? '#1664ff' : '#f98e1b' }} | |||
| ></div> | |||
| </div> | |||
| <div className={styles['subject-drawer__slice']}></div> | |||
| <div className={styles['subject-drawer__right']}> | |||
| <div | |||
| onClick={() => { | |||
| changeColor(1); | |||
| }} | |||
| style={{ borderColor: circleColor == 1 ? '#1664ff' : 'transparent' }} | |||
| className={styles['subject-drawer__right__circle']} | |||
| > | |||
| <div className={styles['subject-drawer__right__circle__center']}></div> | |||
| </div> | |||
| <div | |||
| onClick={() => { | |||
| changeColor(2); | |||
| }} | |||
| style={{ | |||
| borderColor: circleColor == 2 ? '#1664ff' : 'transparent', | |||
| }} | |||
| className={styles['subject-drawer__right__circle']} | |||
| > | |||
| <div | |||
| className={styles['subject-drawer__right__circle__center']} | |||
| style={{ background: '#f98e1b' }} | |||
| ></div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="属性" | |||
| // name="properties" | |||
| layout="vertical" | |||
| style={{ position: 'relative' }} | |||
| labelCol={{ span: 24 }} | |||
| wrapperCol={{ span: 24 }} | |||
| > | |||
| <div style={{ position: 'absolute', width: '100%' }}> | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| pagination={false} | |||
| // onChange={handleTableChange} | |||
| rowKey="id" | |||
| /> | |||
| <Button | |||
| type="link" | |||
| size="small" | |||
| key="edit" | |||
| onClick={addTable} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 增加属性 | |||
| </Button> | |||
| </div> | |||
| </Form.Item> | |||
| </Form> | |||
| </Drawer> | |||
| ); | |||
| }); | |||
| export default subjectNodeParameter; | |||
| @@ -0,0 +1,144 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { AvailableRange } from '@/enums'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { editKnowledgeSubject } from '@/services/subject'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Form, Input, message, type FormRule, type ModalProps } from 'antd'; | |||
| import { useMemo, useState } from 'react'; | |||
| export enum VerifyMode { | |||
| Password = 0, // 用户名密码 | |||
| SSH = 1, // SSH Key | |||
| } | |||
| export enum OperationType { | |||
| Create = 0, // 新建 | |||
| Update = 1, // 更新 | |||
| } | |||
| type FormData = Partial<CodeConfigData>; | |||
| interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> { | |||
| opType: OperationType; | |||
| codeConfigData?: CodeConfigData; | |||
| onOk: () => void; | |||
| } | |||
| function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) { | |||
| const [form] = Form.useForm(); | |||
| const [typeList, setTypeList] = useState([]); | |||
| const isPublic = Form.useWatch('code_repo_vis', form) === AvailableRange.Public; | |||
| const urlExample = useMemo( | |||
| () => | |||
| isPublic | |||
| ? 'https://gitlink.org.cn/ci4s/ci4sManagement-cloud.git' | |||
| : 'git@code.gitlink.org.cn:ci4s/ci4sManagement-cloud.git', | |||
| [isPublic], | |||
| ); | |||
| // /^(git@[\w.-]+:[\w./-]+\.git)$/ | |||
| const urlRules: FormRule[] = useMemo( | |||
| () => | |||
| isPublic | |||
| ? [ | |||
| { | |||
| type: 'url', | |||
| message: '请输入正确的 Git 地址', | |||
| }, | |||
| ] | |||
| : ([] as FormRule[]), | |||
| [isPublic], | |||
| ); | |||
| // 创建 | |||
| const createCodeConfig = async (formData: FormData) => { | |||
| console.log(OperationType); | |||
| const params: FormData & { id?: number } = { | |||
| ...formData, | |||
| }; | |||
| // 清除多余的信息 | |||
| // if (formData.code_repo_vis === AvailableRange.Public) { | |||
| // omit(params, ['verify_mode', 'git_user_name', 'git_password', 'ssh_key']); | |||
| // } | |||
| // if (formData.verify_mode === VerifyMode.Password) { | |||
| // omit(params, ['ssh_key']); | |||
| // } else if (formData.verify_mode === VerifyMode.SSH) { | |||
| // omit(params, ['git_user_name', 'git_password']); | |||
| // } | |||
| params.id = codeConfigData?.id; | |||
| const request = editKnowledgeSubject; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| message.success('修改成功'); | |||
| onOk?.(); | |||
| } | |||
| }; | |||
| // 提交 | |||
| const onFinish = (formData: FormData) => { | |||
| createCodeConfig(formData); | |||
| }; | |||
| // 设置初始值 | |||
| const initialValues: FormData = codeConfigData ?? { | |||
| code_repo_vis: AvailableRange.Public, | |||
| verify_mode: VerifyMode.Password, | |||
| }; | |||
| if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) { | |||
| initialValues.verify_mode = VerifyMode.Password; | |||
| } | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title={'修改主体'} | |||
| image={require('@/assets/img/create-experiment.png')} | |||
| width={825} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'form', | |||
| }} | |||
| destroyOnClose | |||
| > | |||
| <Form | |||
| name="form" | |||
| form={form} | |||
| layout="vertical" | |||
| onFinish={onFinish} | |||
| initialValues={initialValues} | |||
| autoComplete="off" | |||
| > | |||
| <Form.Item | |||
| label="主体名称" | |||
| name="name" | |||
| required | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入主体名称', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入主体名称" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="主体描述" | |||
| name="description" | |||
| required | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入主体描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入主体描述" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default AddCodeConfigModal; | |||
| @@ -0,0 +1,93 @@ | |||
| import { request } from '@umijs/max'; | |||
| import mock from 'mock/knowledge' | |||
| // // 添加图谱 | |||
| export function addKnowledgeGraph(data:any) { | |||
| return request(`/api/kg`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| // 修改图谱 | |||
| export function editKnowledgeGraph(data:any) { | |||
| return request(`/api/kg`, { | |||
| method: 'PUT', | |||
| data, | |||
| }); | |||
| } | |||
| // 获取图谱 | |||
| export function getKnowledgeGraph() { | |||
| return request(`/api/kg/list`, { | |||
| method: 'GET', | |||
| }); | |||
| } | |||
| // 删除图谱 | |||
| export function deleteKnowledgeGraph(data:any) { | |||
| return request(`/api/kg/${data}`, { | |||
| method: 'DELETE', | |||
| }); | |||
| } | |||
| // 查看图谱详情 | |||
| export function getKnowledgeGraphById(data:any) { | |||
| return request(`/api/kg`, { | |||
| method: 'GET', | |||
| params:data | |||
| }); | |||
| } | |||
| // 查看图谱版本列表 | |||
| export function getKnowledgeGraphVersionList(data:any) { | |||
| return request(`/api/kg/version/list`, { | |||
| method: 'GET', | |||
| params:data | |||
| }); | |||
| } | |||
| // // 查看元素配置列表 | |||
| export function getConfigurationList(data:any) { | |||
| return request(`/api/kg/configuration/list`, { | |||
| method: 'GET', | |||
| params:data | |||
| }); | |||
| } | |||
| // 获取关联实体 | |||
| export function getEntityGraph() { | |||
| return request(`/api/kg/ontology/getBasicInfo`, { | |||
| method: 'GET', | |||
| data:{} | |||
| }); | |||
| } | |||
| // 图谱版本新增 | |||
| export function postVersion(data:any) { | |||
| return request(`/api/kg/version`, { | |||
| method: 'POST', | |||
| data | |||
| }); | |||
| } | |||
| // 删除图谱版本 | |||
| export function deleteKnowledgeGraphVersion(data:any) { | |||
| return request(`/api/mmp//kg/version/${data}`, { | |||
| method: 'DELETE', | |||
| }); | |||
| } | |||
| // 增量更新 | |||
| export function incrementalUpdate(data:any) { | |||
| return request(`/api/kg/version/incrementalUpdate`, { | |||
| method: 'POST', | |||
| data | |||
| }); | |||
| } | |||
| // 全量更新 | |||
| export function fullUpdate(data:any) { | |||
| return request(`/api/kg/version/fullUpdate`, { | |||
| method: 'POST', | |||
| data | |||
| }); | |||
| } | |||
| // 全量更新 | |||
| export function rollback(data:any) { | |||
| return request(`/api/kg/version/rollback`, { | |||
| method: 'POST', | |||
| data | |||
| }); | |||
| } | |||
| @@ -0,0 +1,64 @@ | |||
| import { request } from '@umijs/max'; | |||
| import mock from 'mock/knowledge' | |||
| // // 添加主体 | |||
| export function addKnowledgeSubject(data:any) { | |||
| return request(`/api/mmp//kg/ontology`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| // 修改主体 | |||
| export function editKnowledgeGraph(data:any) { | |||
| return request(`/api/kg`, { | |||
| method: 'PUT', | |||
| data, | |||
| }); | |||
| } | |||
| // 获取主体 | |||
| export function getKnowledgeSubject(data:any) { | |||
| return request(`/api/kg/ontology/list`, { | |||
| method: 'POST', | |||
| data:data, | |||
| }); | |||
| } | |||
| // 删除单个主体 | |||
| export function deleteSubject(data:any) { | |||
| return request(`/api/kg/ontology/${data}`, { | |||
| method: 'DELETE', | |||
| }); | |||
| } | |||
| // 查看主体详情 | |||
| export function getKnowledgeSubjectById(data:any) { | |||
| return request(`/api/kg/ontology/${data}`, { | |||
| method: 'GET', | |||
| }); | |||
| } | |||
| // 查看主体版本列表 | |||
| export function getKnowledgeGraphVersionList(data:any) { | |||
| return request(`/api/kg/version/list`, { | |||
| method: 'GET', | |||
| params:data | |||
| }); | |||
| } | |||
| // // 查看元素配置列表 | |||
| export function getConfigurationList(data:any) { | |||
| return request(`/api/kg/configuration/list`, { | |||
| method: 'GET', | |||
| params:data | |||
| }); | |||
| } | |||
| // 修改基本主体 | |||
| export function editKnowledgeSubject(data:any) { | |||
| return request(`/api/kg/ontology/info`, { | |||
| method: 'PUT', | |||
| data, | |||
| }); | |||
| } | |||
| // 修改基本主体 | |||
| export function getSubjectType(dictcode:any) { | |||
| return request(`/api/system/dict/data/type/${dictcode}`, { | |||
| method: 'get', | |||
| }); | |||
| } | |||
| @@ -114,3 +114,18 @@ export type ComputingResource = { | |||
| standard: string; | |||
| create_by: string; | |||
| }; | |||
| // 主体边规格 | |||
| export type subjectEdgeModelSerialize = { | |||
| label:string, | |||
| id: number, | |||
| source: string, | |||
| labelCfg: string, | |||
| curveOffset: string, | |||
| curvePosition: string, | |||
| endPoint: string, | |||
| sourceAnchor: string, | |||
| startPoint: string, | |||
| style: string, | |||
| target: string, | |||
| type: string, | |||
| }; | |||