Commit 19e3afba by zhaochengxiang

增加主数据

parent e438fccc
......@@ -77,6 +77,6 @@
"last 1 safari version"
]
},
"proxy": "http://139.198.126.96:8089",
"proxy": "http://192.168.0.111:8189",
"homepage": "http://myhost/data-govern"
}
......@@ -27,6 +27,8 @@ const AssetTree = loadable(()=> import('./view/Manage/AssetManage/Component/Asse
const DataService = loadable(()=> import('./view/Manage/Model'));
const DataServiceDetail = loadable(()=> import('./view/Manage/Model/Component/ServiceDetail'));
const GrantedDataServiceList = loadable(()=> import('./view/Manage/Model/Component/GrantedList'));
const DataMasterDefine = loadable(()=> import('./view/Manage/DataMaster/Define'));
const DataMasterManage = loadable(()=> import('./view/Manage/DataMaster/Manage'));
export class App extends React.Component {
constructor() {
......@@ -177,6 +179,8 @@ export class App extends React.Component {
<Route path={'/center-home/menu/asset-resource-browse'} component={AssetResourceBrowse} exact />
<Route path={'/center-home/menu/asset-browse'} component={AssetBrowse} exact />
<Route path={'/center-home/menu/asset-recycle'} component={AssetRecycle} exact />
<Route path={'/center-home/menu/msd-define'} component={DataMasterDefine} exact />
<Route path={'/center-home/menu/msd-manage'} component={DataMasterManage} exact />
<Route path={'/center-home/asset-detail'} component={AssetDetailPage} exact />
</Switch>
</Router>
......
......@@ -35,6 +35,14 @@ export const routes = [
name: 'data-service',
text: '服务管理',
},
{
name: 'msd-define',
text: '主数据定义'
},
{
name: 'msd-manage',
text: '主数据管理'
},
]
}
];
......
import { useEffect, useMemo, useState } from 'react';
import { Space, Button, Input, Pagination, Tooltip } from 'antd';
import { useContextMenu, Menu as RcMenu, Item as RcItem } from "react-contexify";
import ResizeableTable from '../../../ResizeableTable';
import DebounceInput from '../../../Model/Component/DebounceInput';
import { UpdateTemplateModal } from './UpdateTemplateModal';
import { inputWidth, showMessage } from '../../../../../util';
import './DefineTable.less';
import 'react-contexify/dist/ReactContexify.css';
import { dispatch } from '../../../../../model';
const InputDebounce = DebounceInput(300)(Input);
const DefineTable = (props) => {
const {nodeId} = props;
const [keyword, setKeyword] = useState('');
const [checkedKeys, setCheckedKeys] = useState([]);
const [isTemplateModalVisible, setIsTemplateModalVisible] = useState(false);
const [currentTemplate, setCurrentTemplate] = useState({});
const [action, setAction] = useState('');
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [tableData, setTableData] = useState([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState({pageNum: 1, pageSize: 20});
const {pageNum, pageSize} = pagination;
useEffect(() => {
setCheckedKeys([]);
setPagination({...pagination, pageNum: 1});
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodeId])
useEffect(() => {
if (!nodeId || nodeId==='') {
setTableData([]);
setTotal(0);
} else {
getTemplates();
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [keyword, pagination])
const columns = useMemo(() => {
return ([
{
title: '序号',
dataIndex: 'key',
render: (text, record, index) => {
return (index+1).toString();
},
width: 60,
ellipsis: true,
},
{
title: '模版名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
render: (text, record, index) => {
return <a onClick={() => {onItemClick(record);}}>{text}</a>
},
},
{
title: '模版中文名称',
dataIndex: 'cnName',
width: 200,
ellipsis: true,
},
{
title: '修改时间',
dataIndex: 'lastUpdateTime',
width: 200,
ellipsis: true,
},
{
title: '模版描述',
dataIndex: 'comment',
width: 200,
ellipsis: true,
}
]);
}, []);
const MENU_ID = 'data-master-table-contextmenu';
const { show } = useContextMenu({
id: MENU_ID,
});
const getTemplates = () => {
setLoading(true);
dispatch({
type: 'msd.getModels',
payload: {
params: {
nodeId,
keyword,
pageNum,
pageSize
}
},
callback: (data) => {
setLoading(false);
setTableData(data?.content||[]);
setTotal(data?.totalElements||0);
},
error: () => {
setLoading(false);
}
});
}
const onAddClick = () => {
setAction('add');
setCurrentTemplate(null);
setIsTemplateModalVisible(true);
}
const onItemClick = (record) => {
setAction('detail');
setCurrentTemplate(record);
setIsTemplateModalVisible(true);
}
const onTemplateModalCancel = (refresh = false) => {
setIsTemplateModalVisible(false);
if (refresh) {
getTemplates();
}
}
const onBatchDeleteClick = () => {
setDeleteLoading(true);
dispatch({
type: 'msd.deleteModels',
payload: {
params: {
modelIds: (checkedKeys||[]).join(',')
}
},
callback: (data) => {
setDeleteLoading(false);
showMessage('success', '删除成功');
getTemplates();
},
error: () => {
setDeleteLoading(false);
}
});
}
const onSearchInputChange = (value) => {
setKeyword(value);
}
const onTableSelectChange = keys => {
setCheckedKeys(keys);
};
const handleItemClick = ({ event, props, data, triggerEvent }) => {
const key = event.currentTarget.id;
if (key === 'update') {
setAction('update');
setIsTemplateModalVisible(true);
}
}
const changeCurrent = (page,size) => {
setCheckedKeys([]);
setPagination({ pageNum: page, pageSize: size });
}
const rowSelection = {
selectedRowKeys: checkedKeys,
onChange: onTableSelectChange,
};
return (
<div className='data-master-table'>
<div className='data-master-header px-3'>
<Space>
<Space>
<Button onClick={onAddClick}>新建</Button>
</Space>
<Space>
<Tooltip title={(checkedKeys||[]).length===0?'请先选择模版':''}>
<Button onClick={onBatchDeleteClick} disabled={(checkedKeys||[]).length===0} loading={deleteLoading}>删除</Button>
</Tooltip>
</Space>
</Space>
<InputDebounce
placeholder="请输入关键字"
allowClear
value={keyword}
onChange={onSearchInputChange}
style={{ width: inputWidth, marginLeft: 'auto' }}
/>
</div>
<div className='data-master-content p-3'>
<ResizeableTable
loading={loading}
rowKey='_id'
rowSelection={rowSelection}
columns={columns}
dataSource={tableData}
onRow={(record) => {
return {
onContextMenu: event => {
setCurrentTemplate(record);
show(event);
},
};
}}
pagination={false}
scroll={{ y: 'calc(100vh - 94px - 32px - 57px - 24px - 39px - 36px)' }}
/>
<Pagination
size="small"
className="text-center mt-3"
showSizeChanger
showQuickJumper
onChange={changeCurrent}
onShowSizeChange={changeCurrent}
current={pageNum}
pageSize={pageSize}
defaultCurrent={1}
total={total}
showTotal={total => `共 ${total} 条`}
/>
</div>
<UpdateTemplateModal
action={action}
nodeId={nodeId}
template={currentTemplate}
visible={isTemplateModalVisible}
onCancel={onTemplateModalCancel}
/>
<RcMenu id={MENU_ID}>
<RcItem id="update" onClick={handleItemClick}>
编辑
</RcItem>
</RcMenu>
</div>
);
}
export default DefineTable;
\ No newline at end of file
.data-master-table {
height: 100%;
.data-master-header {
display: flex;
flex: none;
height: 57px;
border-bottom: 1px solid #EFEFEF;
justify-content: space-between;
align-items: center;
}
}
\ No newline at end of file
import {useEffect, useMemo, useState, useRef} from 'react';
import {Tooltip, Spin, AutoComplete, Tree, Modal} from 'antd';
import {PlusOutlined, ReloadOutlined} from '@ant-design/icons';
import { useContextMenu, Menu as RcMenu, Item as RcItem } from "react-contexify";
import { dispatch } from '../../../../../model';
import {highlightSearchContentByTerms, showMessage} from '../../../../../util';
import UpdateDefineTreeNodeModal from './UpdateDefineTreeNodeModal';
import 'react-contexify/dist/ReactContexify.css';
import './DefineTree.less';
const {Option} = AutoComplete;
const DefineTree = (props) => {
const {onClick} = props;
const [loading, setLoading] = useState(false);
const [data, setData] = useState();
const [options, setOptions] = useState([]);
const [keyword, setKeyword] = useState('');
const [expandedKeys, setExpandedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(false);
const [updateNodeModalVisible, setUpdateNodeModalVisible] = useState(false);
const [updateNodeType, setUpdateNodeType] = useState('');
const [rightSelectNode, setRightSelectNode] = useState({});
const selectedKeysRef = useRef([]);
const [modal, contextHolder] = Modal.useModal();
const MENU_ID = 'msd-define-tree';
const { show } = useContextMenu({
id: MENU_ID,
});
useEffect(() => {
getTreeNodes();
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const treeData = useMemo(() => {
const loop = (data) =>
(data||[]).map((item) => {
if (item.children) {
return {
title: item.name||'',
key: item._id||'',
origin: item,
children: loop(item.children),
};
}
return {
title: item.name||'',
key: item._id||'',
};
});
return loop(data);
}, [data])
const treeList = useMemo(() => {
const generateList = (data, list, path = null) => {
(data||[]).forEach(node => {
const {_id, name} = node;
const currentPath = path ? `${path}/${name}` : name;
list.push({key: _id, title: currentPath});
if (node.children) {
generateList(node.children, list, currentPath);
}
});
};
const newTreeList = [];
generateList(data, newTreeList);
return newTreeList;
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
const selectNode = useMemo(() => {
const generateList = (data, list) => {
(data||[]).forEach(node => {
list.push({...node});
if (node.children) {
generateList(node.children, list);
}
});
};
const newTreeList = [];
generateList(data, newTreeList);
const filterNodes = newTreeList.filter(item => selectedKeys.indexOf(item._id)!==-1);
return (filterNodes||[]).length>0 ? filterNodes[0]:{};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, selectedKeys])
const getTreeNodes = () => {
setLoading(true);
dispatch({
type: 'msd.getTreeNodes',
callback: (data) => {
setLoading(false);
setData(data||[]);
if ((selectedKeysRef.current||[]).length===0 && (data||[]).length>0) {
onTreeSelect([data[0]._id]);
}
if ((expandedKeys||[]).length===0 && (data||[]).length>0) {
setExpandedKeys([data[0]._id]);
} else {
setExpandedKeys(Array.from(new Set([...expandedKeys, ...selectedKeysRef.current])));
setAutoExpandParent(true);
}
},
error: () => {
setLoading(false);
}
})
}
const onAddClick = () => {
setUpdateNodeType('add');
setUpdateNodeModalVisible(true);
}
const onRefreshClick = () => {
getTreeNodes();
}
const updateNode = () => {
setUpdateNodeType('update');
setUpdateNodeModalVisible(true);
}
const upOrDownNode = (steps = 1) => {
setLoading(true);
dispatch({
type: 'msd.treeNodeSeq',
payload: {
params: {
id: rightSelectNode?._id||'',
steps
}
},
callback: () => {
rightSelectNode?._id && onTreeSelect([rightSelectNode?._id]);
getTreeNodes();
},
error: () => {
setLoading(false);
}
})
}
const deleteNode = () => {
modal.confirm({
title: '提示!',
content: '删除目录会删除相关的模版,您确定删除吗?',
onOk: () => {
setLoading(true);
dispatch({
type: 'msd.deleteTreeNode',
payload: {
params: {
nodeId: rightSelectNode?._id
}
},
callback: () => {
showMessage('success', '删除目录成功');
setLoading(false);
if (selectedKeysRef.current.indexOf(rightSelectNode?._id)!==-1) {
setSelectedKeys([]);
selectedKeysRef.current = [];
}
getTreeNodes();
},
error: () => {
setLoading(false);
}
});
}
});
}
const onAutoCompleteSearch = (searchText) => {
setKeyword(searchText);
setOptions(treeList.filter(item => item.title.indexOf(searchText)!==-1));
};
const onAutoCompleteSelect = (value, option) => {
const paths = value.split('/');
setKeyword(paths[paths.length-1]);
onTreeSelect([option.key]);
setExpandedKeys(Array.from(new Set([...expandedKeys, option.key])));
setAutoExpandParent(true);
};
const onAutoCompleteClear = () => {
setKeyword('');
}
const onTreeExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
}
const onTreeSelect = (selectedKeys) => {
if (selectedKeys.length === 0) {
return;
}
setSelectedKeys(selectedKeys);
selectedKeysRef.current = selectedKeys;
onClick && onClick(selectedKeys[0]);
}
const onUpdateNodeModalCancel = (refresh = false, nodeId = null) => {
setUpdateNodeModalVisible(false);
if (refresh) {
nodeId && onTreeSelect([nodeId]);
getTreeNodes();
}
}
return (
<div className='data-master-tree'>
<div className='header p-3'>
<Tooltip title="新增目录">
<PlusOutlined
className='default'
onClick={onAddClick}
style={{
fontSize:16,
cursor:'pointer',
flex: 1
}}
/>
</Tooltip>
<Tooltip title="刷新目录" className='ml-2'>
<ReloadOutlined
className='default'
onClick={onRefreshClick}
style={{
fontSize:16,
cursor:'pointer',
flex: 1
}}
/>
</Tooltip>
<div style={{flex: 3}} />
</div>
<div className='content p-3'>
<Spin spinning={loading}>
<AutoComplete
className='content-search'
allowClear
value={keyword}
onSelect={onAutoCompleteSelect}
onSearch={onAutoCompleteSearch}
onClear={onAutoCompleteClear}
>
{
(options||[]).map((item, index) => {
return (
<Option key={item.key} value={item.title}>
<div style={{ whiteSpace: 'normal' }}>
{highlightSearchContentByTerms(item.title, [keyword])}
</div>
</Option>
);
})
}
</AutoComplete>
<Tree
className='content-tree'
showLine
showIcon={false}
autoExpandParent={autoExpandParent}
treeData={treeData}
onExpand={onTreeExpand}
onSelect={onTreeSelect}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
onRightClick={({event, node}) => {
setRightSelectNode(node?.origin);
show(event, {
position: {
x: event.clientX + 30,
y: event.clientY - 10
}
});
}}
/>
</Spin>
</div>
<UpdateDefineTreeNodeModal
visible={updateNodeModalVisible}
onCancel={onUpdateNodeModalCancel}
type={updateNodeType}
node={(updateNodeType==='add')?selectNode:rightSelectNode}
/>
<RcMenu id={MENU_ID}>
<RcItem id="edit" onClick={updateNode}>
修改目录
</RcItem>
<RcItem id="up" onClick={() => { upOrDownNode(-1); }}>
上移目录
</RcItem>
<RcItem id="down" onClick={() => { upOrDownNode(1); }}>
下移目录
</RcItem>
<RcItem id="delete" onClick={deleteNode}>
删除目录
</RcItem>
</RcMenu>
{contextHolder}
</div>
);
}
export default DefineTree;
\ No newline at end of file
@import '../../../../../variables.less';
.data-master-tree {
height: 100%;
.header {
display: inline-flex;
flex: none;
width: 100%;
height: 57px;
justify-content: flex-start;
align-items: center;
border-bottom: 1px solid #EFEFEF;
}
.content-search {
margin-bottom: 10px;
width: 100%;
}
.content-tree {
height: calc(100vh - @header-height - @breadcrumb-height - 25px - 57px - 66px);
overflow: auto;
}
}
\ No newline at end of file
import { Form, Input, Row, Col, Descriptions } from "antd";
const { TextArea } = Input;
const UpdateBasicInfo = (props) => {
const {form, editable, template} = props;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 18 },
},
};
return (
<div>
<h3>基本信息</h3>
{
editable ? <Form
form={form}
{...formItemLayout}
>
<Row gutter={10}>
<Col xs={24} sm={24} lg={12}>
<Form.Item label='中文名称' name='cnName' rules={[{ required: true, message: '请填写模版中文名称' }]}>
<Input />
</Form.Item>
</Col>
<Col xs={24} sm={24} lg={12}>
<Form.Item label='英文名称' name='name' rules={[{ required: true, message: '请填写模版名称' }]}>
<Input />
</Form.Item>
</Col>
<Col xs={24} sm={24} lg={12}>
<Form.Item label='模版描述' name='comment'>
<TextArea rows={4} />
</Form.Item>
</Col>
</Row>
</Form> : <Descriptions column={2}>
<Descriptions.Item label={<div style={{ textAlign: 'right', width: 85 }}>中文名称</div>} >{template?.cnName||''}</Descriptions.Item>
<Descriptions.Item label={<div style={{ textAlign: 'right', width: 85 }}>英文名称</div>}>{template?.name||''}</Descriptions.Item>
<Descriptions.Item label={<div style={{ textAlign: 'right', width: 85 }}>模版描述</div>}>{template?.comment||''}</Descriptions.Item>
</Descriptions>
}
</div>
);
}
export default UpdateBasicInfo;
\ No newline at end of file
import { useState, useEffect } from 'react';
import { Modal, Form } from 'antd';
import UpdateNodeForm from './UpdateNodeForm';
import { dispatch } from '../../../../../model';
const UpdateDefineTreeNodeModal = (props) => {
const {visible, onCancel, type, node} = props;
const [confirmLoading, setConfirmLoading] = useState(false);
const [form] = Form.useForm();
useEffect(() => {
if (visible) {
if (type === 'add') {
form.setFieldsValue({action: node ? 'sub' : 'root', name: '', comment: ''});
} else {
form.setFieldsValue({action: '', name: node?.name||'', comment: node?.comment||''});
}
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, node, type])
const handleCancel = () => {
setConfirmLoading(false);
form.setFields([{ name: 'name', errors: [] }, { name: 'comment', errors: [] }]);
onCancel && onCancel();
}
const handleOk = async () => {
setConfirmLoading(true);
try {
const values = await form.validateFields();
let payload = null;
if (type === 'add') {
payload = {
name: values.name||'',
comment: values.comment||'',
parentId: (values.action==='root')?'root':node?._id
};
} else {
payload = {
...node,
name: values.name||'',
comment: values.comment||'',
}
}
let url = (type === 'add')?'msd.addTreeNode':'msd.updateTreeNode';
dispatch({
type: url,
payload: {
data: payload
},
callback: id => {
setConfirmLoading(false);
onCancel && onCancel(true, id);
},
error: () => {
setConfirmLoading(false);
}
});
} catch (errInfo) {
setConfirmLoading(false);
}
}
return (
<Modal
confirmLoading={confirmLoading}
visible={visible}
title={type==='add'?"新增目录":"更新目录"}
onOk={handleOk}
onCancel={handleCancel}
>
<UpdateNodeForm type={type} node={node} form={form} />
</Modal>
);
}
export default UpdateDefineTreeNodeModal;
\ No newline at end of file
import React, { useState, useRef, useEffect, useMemo, useCallback, useContext } from "react";
import { Tooltip, Table, Space, Popover, Input, Button, Form, Checkbox } from "antd";
import { QuestionCircleOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import { useClickAway } from 'ahooks';
import update from 'immutability-helper';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import DebounceInput from "../../../Model/Component/DebounceInput";
import { inputWidth } from "../../../../../util";
import { DragableBodyRow, DatatypeInput } from "../../../Model/Component/ImportActionTable";
import { dispatch } from "../../../../../model";
import { EditTemplateContext } from "./UpdateTemplateModal";
const InputDebounce = DebounceInput(300)(Input);
const FieldTypeComp = (props) => {
const {dataType = {}} = props;
const [value, setValue] = useState('');
useEffect(() => {
try {
let dataTypeJson = JSON.parse(dataType);
if ((dataTypeJson?.name==='Char'||dataTypeJson?.name==='Varchar') && dataTypeJson?.parameterValues?.length>0) {
setValue(`${dataTypeJson?.name||''}(${(dataTypeJson?.parameterValues[0]?dataTypeJson?.parameterValues[0]:0)})`);
} else if ((dataTypeJson?.name==='Decimal'||dataTypeJson?.name==='Numeric') && dataTypeJson?.parameterValues?.length>1) {
setValue(`${dataTypeJson?.name||''}(${(dataTypeJson?.parameterValues[0]?dataTypeJson?.parameterValues[0]:0)},${(dataTypeJson?.parameterValues[1]?dataTypeJson?.parameterValues[1]:0)})`);
} else {
setValue(dataTypeJson?.name||'');
}
} catch(error) {
setValue('');
}
}, [dataType])
return (
<span>{value}</span>
)
}
export const EditableCell = ({
editing,
dataIndex,
colTitle,
inputType,
record,
index,
datatypes,
require,
children,
...restProps
}) => {
let editingComponent = null;
if (editing) {
if (dataIndex !== 'dataType') {
const inputNode = inputType === 'check' ? <Checkbox /> : <InputDebounce />
editingComponent = (
<Form.Item
name={dataIndex}
style={{
margin: 0,
}}
valuePropName={(inputType==='check')? 'checked': 'value'}
rules={[
{
required: (require===null)?false:require,
message: `请输入${colTitle}!`,
},
]}
>
{ inputNode }
</Form.Item>
);
} else {
editingComponent = (
<Form.Item
name={dataIndex}
style={{
margin: 0,
}}
valuePropName={'value'}
rules={[
{
required: (require===null)?false:require,
message: `请输入${colTitle}!`,
},
]}
>
<DatatypeInput datatypes={datatypes} />
</Form.Item>
)
}
}
return (
<td {...restProps}>
{editing ? (
editingComponent
) : (
children
)}
</td>
);
};
const UpdateField = (props) => {
const {editable = true, template, visible, onChange} = props;
const [data, setData] = useState([]);
const [keyword, setKeyword] = useState('');
const [editingKey, setEditingKey] = useState(null);
const [supportedDatatypes, setSupportedDatatypes] = useState([]);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const tableRef = useRef(null);
const dataRef = useRef([]);
const editingKeyRef = useRef(null);
const { attrIsEditingFunction } = useContext(EditTemplateContext);
const cols = [
{
title: '序号',
dataIndex: 'key',
editable: false,
width: 60,
render: (_, __, index) => {
return (index+1).toString();
}
},
{
title: '中文名称',
width: 200,
dataIndex: 'cnName',
editable: true,
ellipsis: true,
require: true,
render: (text, _, __) => {
return (
<Tooltip title={text||''}>
<span>{text}</span>
</Tooltip>
)
}
},
{
title: '英文名称',
width: 200,
dataIndex: 'name',
editable: true,
ellipsis: true,
require: true,
render: (text, record, index) => {
return (
<Tooltip title={text||''}>
<span style={{ fontWeight: 'bold' }} >{text}</span>
</Tooltip>
)
}
},
{
title: '类型',
width: (editingKey!==null)?250:150,
dataIndex: 'dataType',
editable: true,
ellipsis: true,
require: true,
render: (_, record, __) => {
return <FieldTypeComp dataType={record?.dataType} />;
}
},
{
title: '业务含义',
dataIndex: 'comment',
editable: true,
ellipsis: true,
require: true,
width: 200,
render: (text, _, __) => {
return (
<Tooltip title={text||''}>
<span>{text}</span>
</Tooltip>
)
}
}
];
const actionCol = {
title: '操作',
dataIndex: 'action',
width: 90,
fixed: 'right',
render: (_, record) => {
return (
<React.Fragment>
{
editable && <React.Fragment>
{
<React.Fragment>
<Button
className='mr-3'
size='small'
type='text'
icon={<DeleteOutlined style={{ color: 'red' }} />}
onClick={(event) => {
event.stopPropagation();
removeItem(record);
}}
/>
</React.Fragment>
}
</React.Fragment>
}
</React.Fragment>
)
},
};
useEffect(() => {
if (visible) {
setEditingKey(null);
editingKeyRef.current = null;
if (!template) {
setData([]);
dataRef.current = [];
} else {
getFileds();
}
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, template])
const columns = useMemo(() => {
const newColumns = [...cols];
if (editable) {
newColumns.push(actionCol);
}
return newColumns;
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [editable, data, editingKey]);
useClickAway(() => {
save();
}, tableRef);
useEffect(() => {
getSupportedDatatypes();
}, [])
useEffect(() => {
attrIsEditingFunction && attrIsEditingFunction(editingKey!=null);
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [editingKey])
const filterData = useMemo(() => {
return (data||[]).filter(item => (item?.name||'').indexOf(keyword)!==-1 || (item.cnName).indexOf(keyword)!==-1);
}, [keyword, data])
const isEditing = (record) => record?.name === editingKey;
const getSupportedDatatypes = () => {
dispatch({
type: 'msd.getTypes',
callback: data => {
setSupportedDatatypes(data||[]);
}
});
}
const getFileds = () => {
setLoading(true);
dispatch({
type: 'msd.getColumns',
payload: {
params: {
modelId: template?._id||''
}
},
callback: data => {
setLoading(false);
function compare(val1, val2) {
var a = val1.seq;
var b = val2.seq;
return (a - b);
}
(data||[]).sort(compare);
setData(data||[]);
dataRef.current = data||[];
onChange && onChange(data||[]);
},
error: () => {
setLoading(false);
}
});
}
const onAddClick = (event) => {
event.stopPropagation();
save().then(result => {
if (result) {
setKeyword('');
const newData = [...dataRef.current, {name: ''}];
setData(newData);
dataRef.current = newData;
editItem(newData[newData.length-1]);
setTimeout(() => {
document.getElementById(`field-`)?.scrollIntoView();
}, 200)
}
})
}
const removeItemLogic = (record) => {
const newData = [...data];
const index = newData.findIndex((item) => record.name === item.name);
if (index !== -1) {
newData.splice(index, 1);
}
setData(newData);
dataRef.current = newData;
onChange && onChange(newData);
}
const removeItem = (record) => {
if (record.name !== editingKey) {
removeItemLogic(record);
} else {
setEditingKey(null);
editingKeyRef.current = null;
removeItemLogic(record);
}
}
const onSearchInputChange = (value) => {
save().then(result => {
if (result) {
setKeyword(value);
}
});
}
const editItemLogic = (record) => {
form.resetFields();
try {
let dataTypeJson = JSON.parse(record?.dataType);
form.setFieldsValue({...record, dataType: dataTypeJson});
setEditingKey(record?.name);
editingKeyRef.current = record?.name;
} catch {
form.setFieldsValue({...record, dataType: {}});
setEditingKey(record?.name);
editingKeyRef.current = record?.name;
}
}
const editItem = (record) => {
save().then((result) => {
if (result) {
editItemLogic(record);
}
});
};
const save = async() => {
try {
if (editingKeyRef.current !== null) {
const row = await form.validateFields();
if ((row.dataType.name||'')==='') {
form.setFields([{ name: 'dataType', errors: ['必须选择类型'] }]);
return;
}
(row.dataType.parameterNames||[]).forEach((parameterName, index) => {
if (!row.dataType.parameterValues[index] || row.dataType.parameterValues[index]==='') {
row.dataType.parameterValues[index] = 0;
}
})
const newData = [...dataRef.current];
const index = newData.findIndex((item) => editingKeyRef.current === item.name);
const newDataExcludeSelf = [...dataRef.current];
newDataExcludeSelf.splice(index, 1);
//判断字段名称是否唯一
let _index = (newDataExcludeSelf||[]).findIndex(item => item.name === row.name);
if (_index !== -1) {
form.setFields([{ name: 'name', errors: ['字段名称不能重复'] }]);
return false;
}
row.dataType = JSON.stringify(row.dataType);
const item = newData[index];
let attribute = { ...item, ...row};
newData.splice(index, 1, attribute);
setEditingKey(null);
editingKeyRef.current = null;
setData(newData);
dataRef.current = newData;
onChange && onChange(newData);
}
return true;
} catch (errInfo) {
console.log('Validate Failed:', errInfo);
return false;
}
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
const dragRow = data[dragIndex];
const newData = update(data, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragRow],
],
});
setData(newData);
dataRef.current = newData;
onChange && onChange(newData);
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
const onTableRow = (record, index) => {
let rowParams = {
index,
id: `field-${record.name}`,
};
if (editable) {
if (!isEditing(record)) {
rowParams = {...rowParams, onClick: (event) => {
event.stopPropagation();
editItem(record);
}
}
if (keyword.length===0) {
rowParams = {...rowParams, moveRow};
}
}
}
return rowParams;
}
const mergedColumns = columns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record) => ({
record,
dataIndex: col.dataIndex,
colTitle: col.title,
editing: isEditing(record),
datatypes: supportedDatatypes,
require: col.require
}),
};
});
return (
<div>
<div className='d-flex mb-3' style={{ justifyContent: 'space-between' }}>
<Space>
<h3 style={{ marginBottom: 0 }}>字段信息</h3>
{
editable && <Popover content='点击行进行编辑,表格可以通过拖拽来排序'>
<QuestionCircleOutlined className='pointer' />
</Popover>
}
</Space>
<Space>
{
editable && <Button onClick={onAddClick}>新建</Button>
}
<div className='d-flex' style={{ alignItems: 'center' }}>
<InputDebounce
placeholder="请输入中文名称或者英文名称"
allowClear
value={keyword}
onChange={onSearchInputChange}
style={{ width: inputWidth }}
/>
</div>
</Space>
</div>
<div id="containerId" ref={tableRef}>
<DndProvider backend={HTML5Backend} >
<Form form={form} component={false}>
<Table
rowKey='_id'
loading={loading}
dataSource={filterData}
columns={mergedColumns}
pagination={false}
components={{
body: {
cell: EditableCell,
//编辑或者搜索状态下不允许拖动
row: (editable&&editingKey===null&&keyword==='')?DragableBodyRow:null,
},
}}
onRow={onTableRow}
/>
</Form>
</DndProvider>
</div>
</div>
);
}
export default UpdateField;
\ No newline at end of file
import { useMemo } from "react";
import { Form, Input, Radio } from 'antd';
const UpdateNodeForm = (props) => {
const {form, type, node} = props;
const radioDisable = useMemo(() => {
return !node;
}, [node])
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 18 },
},
};
return (
<Form
{...formItemLayout}
form={form}
>
{
type==='add'&&<Form.Item label="目录类型" name="action">
<Radio.Group disabled={radioDisable} >
<Radio value='root'>根目录</Radio>
<Radio value='sub'>子目录</Radio>
</Radio.Group>
</Form.Item>
}
<Form.Item
label="名称"
name="name"
rules={[{ required: true, message: '请输入名称!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="描述"
name="comment"
rules={[{ required: true, message: '请输入描述!' }]}
>
<Input />
</Form.Item>
</Form>
);
}
export default UpdateNodeForm;
\ No newline at end of file
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Modal, Form, Button } from "antd";
import UpdateBasicInfo from "./UpdateBasicInfo";
import UpdateField from "./UpdateField";
import { showMessage } from '../../../../../util';
import { dispatch } from '../../../../../model';
import './UpdateTemplateModal.less';
export const EditTemplateContext = React.createContext({
attrIsEditingFunction: null,
});
export const UpdateTemplateModal = (props) => {
const { visible, onCancel, action = 'add', template, nodeId } = props;
const [form] = Form.useForm();
const [fields, setFields] = useState([]);
const [confirmLoading, setConfirmLoading] = useState(false);
const attrIsEditingRef = useRef(false);
useEffect(() => {
if (visible) {
form?.setFieldsValue({name: template?.name||'', cnName: template?.cnName||'', comment: template?.comment||''});
setFields([]);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible])
const title = useMemo(() => {
if (action === 'add') {
return '新建模版';
}
if (action === 'detail') {
return '模版详情';
}
if (action === 'update') {
return '编辑模版';
}
return '';
}, [action])
const attrIsEditingFunction = (value) => {
attrIsEditingRef.current = value;
}
const onFieldChange = (values) => {
setFields(values);
}
const save = (e) => {
e.stopPropagation();
if (attrIsEditingRef.current) {
showMessage("warn", '还有字段正在编辑,需先保存该字段!');
} else {
saveLogic();
}
}
const saveLogic = async () => {
try {
const row = await form.validateFields();
if ((fields||[]).length === 0) {
showMessage('warn', '请新增字段');
return;
}
setConfirmLoading(true);
let url = '', newTemplate = {};
if (action === 'add') {
url = 'msd.addModel';
newTemplate = {...row, nodeId: nodeId||''};
} else {
url = 'msd.updateModel';
newTemplate = {...template, ...row}
}
dispatch({
type: url,
payload: {
data: newTemplate
},
callback: id => {
(fields||[]).forEach((field, index) => {
field.parentId = id;
field.seq = index;
});
dispatch({
type: 'msd.saveColumns',
payload: {
params: {
modelId: id
},
data: fields
},
callback: () => {
setConfirmLoading(false);
onCancel && onCancel(true);
},
error: () => {
setConfirmLoading(false);
}
});
},
error: () => {
setConfirmLoading(false);
}
});
} catch (errInfo) {
console.log('Validate Failed:', errInfo);
}
}
return (
<EditTemplateContext.Provider value={{
attrIsEditingFunction,
}}>
<Modal
forceRender
className='update-template-modal'
width='80%'
title={title}
visible={visible}
onCancel={() => {onCancel && onCancel();}}
onOk={save}
footer={[
<Button key="cancel" onClick={() => {onCancel && onCancel();}}>
取消
</Button>,
action!=='detail' && <Button
key="ok"
type="primary"
loading={confirmLoading}
onClick={save}
>
确定
</Button>,
]}
>
<UpdateBasicInfo form={form} editable={action!=='detail'} template={template} />
<UpdateField editable={action!=='detail'} template={template} onChange={onFieldChange} visible={visible} />
</Modal>
</EditTemplateContext.Provider>
);
}
.update-template-modal {
.yy-modal-body {
max-height: 70vh;
overflow: auto;
}
.dynamic-delete-button {
position: relative;
top: 4px;
margin: 0 8px;
color: #999;
font-size: 24px;
cursor: pointer;
transition: all 0.3s;
}
.dynamic-delete-button:hover {
color: #777;
}
}
\ No newline at end of file
import { useMemo, useState } from 'react';
import classNames from 'classnames';
import { ResizableBox } from 'react-resizable';
import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
import DefineTree from './Component/DefineTree';
import DefineTable from './Component/DefineTable';
import './index.less';
const DataMasterDefine = (props) => {
const [collapse, setCollapse] = useState(false);
const [nodeId, setNodeId] = useState('');
const classes = useMemo(() => {
return classNames('data-master', {
'data-master-collapse': collapse,
});
}, [collapse]);
const onTreeClick = (value) => {
setNodeId(value);
}
const onCollapseClick = () => {
setCollapse(!collapse);
}
return (
<div className={classes}>
<ResizableBox
className='left-wrap'
width={230}
height={Infinity}
axis='x'
minConstraints={[230, Infinity]} maxConstraints={[Infinity, Infinity]}
>
<DefineTree onClick={onTreeClick} />
</ResizableBox>
<div className='left-collapse-wrap'>
<div className='left-collapse' onClick={onCollapseClick}>
{ collapse ? <CaretRightOutlined /> : <CaretLeftOutlined /> }
</div>
</div>
<div className='right-wrap'>
<DefineTable nodeId={nodeId} />
</div>
</div>
)
}
export default DataMasterDefine;
\ No newline at end of file
.data-master {
display: flex;
flex: auto;
height: 100%;
background-color: #fff;
.left-wrap {
flex: none;
overflow: hidden;
height: 100%;
border-right: 1px solid #EFEFEF;
}
.left-collapse-wrap {
flex: none;
position: relative;
width: 20px;
height: 100%;
.left-collapse {
display: inline-flex;
justify-content: center;
align-items: center;
left: 0;
right: 0;
background: #f2f5fc;
position: absolute;
top: calc(50% - 40px);
width: 12px;
height: 80px;
border-radius: 0 12px 12px 0;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
cursor: pointer;
}
}
.right-wrap {
flex: 1;
overflow: hidden;
height: 100%;
}
}
.data-master-collapse {
.left-wrap {
width: 0 !important;
}
}
\ No newline at end of file
import React, { useState, useEffect } from 'react';
import { Button, Upload, Drawer, Pagination, Form, Spin } from 'antd';
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import ResizeableTable from '../../../ResizeableTable';
import { dispatch, dispatchLatest } from '../../../../../model';
import { showMessage, formatDate } from '../../../../../util';
const ExpandedRow = (props) => {
const {data} = props;
const [loading, setLoading] = useState(false);
const [log, setLog] = useState({});
useEffect(() => {
getLog();
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
const getLog = () => {
setLoading(true);
dispatch({
type: 'msd.importLog',
payload: {
logId: data?.id
},
callback: data => {
setLoading(false);
setLog(data||{});
},
error: () => {
setLoading(false);
}
})
}
return <Spin spinning={loading}>
<p style={{ margin: 0 }}>{log?.message||''}</p>
</Spin>
}
const ImportDataDrawer = (props) => {
const { onCancel, onSuccess, visible, nodeId } = props;
const [ fileList, setFileList ] = useState([]);
const [ confirmLoading, setConfirmLoading ] = useState(false);
const [ loading, setLoading ] = useState(false);
const [ logs, setLogs ] = useState([]);
const [ pagination, setPagination ] = useState( { pageNum: 1, pageSize: 20 } );
const { pageNum, pageSize } = pagination;
const [ total, setTotal ] = useState(0);
const columns = [
{
title: '序号',
dataIndex: 'key',
render: (text, record, index) => {
return (index+1).toString();
},
width: 60,
ellipsis: true,
},
{
title: '开始时间',
dataIndex: 'startTime',
width: 170,
ellipsis: true,
render: (_, record, __) => {
return formatDate(record.startTime);
}
},
{
title: '结束时间',
dataIndex: 'endTime',
width: 170,
ellipsis: true,
render: (_, record, __) => {
return formatDate(record.endTime);
}
},
{
title: '耗时',
dataIndex: 'costTime',
width: 100,
ellipsis: true,
render: (_, record, __) => {
return record.costTime?`${Number(record.costTime/1000)}秒`:'';
}
},
{
title: '导入人',
dataIndex: 'operator',
width: 100,
ellipsis: true,
render: (text, record, __) => {
return text.split(':')[0];
}
},
{
title: '导入状态',
dataIndex: 'state',
width: 100,
ellipsis: true,
}
]
useEffect(() => {
if (visible) {
setPagination({ pageNum: 1, pageSize: 20 });
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible])
useEffect(() => {
if (visible) {
getLogs();
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [pagination])
const getLogs = () => {
setLoading(true);
dispatch({
type: 'msd.importLogs',
payload: {
modelId: nodeId,
page: pageNum,
size: pageSize
},
callback: data => {
setLoading(false);
setTotal(data.totalElements);
setLogs(data.content||[]);
},
error: () => {
setLoading(false);
}
})
}
const onRefreshClick = () => {
getLogs();
}
const changeCurrent = (page,size) => {
setPagination({ pageNum: page, pageSize: size });
}
const downloadTemplate = () => {
window.open(`/api/metadatarepo/rest/msdMgr/exportTemplate?modelId=${nodeId}`);
}
const uploadProps = {
onRemove: file => {
const index = fileList.indexOf(file);
const newFileList = fileList.slice();
newFileList.splice(index, 1);
setFileList(newFileList);
},
beforeUpload: file => {
setFileList([file]);
return false;
},
fileList: fileList || [],
accept: ".xlsx",
};
const handleOk = () => {
if ((fileList || []).length === 0) {
showMessage('info', '请先选择Excel文件上传');
return;
}
setConfirmLoading(true);
dispatchLatest({
type: 'msd.importData',
payload: {
params: {
modelId: nodeId,
},
fileList
},
callback: data => {
setConfirmLoading(false);
setFileList([]);
getLogs();
onSuccess && onSuccess();
},
error: () => {
setConfirmLoading(false);
}
})
}
const reset = () => {
setConfirmLoading(false);
setFileList([]);
}
return (
<Drawer
forceRender
visible={ visible }
title='主数据导入'
width={1000}
placement="right"
closable={ true }
onClose={() => {
reset();
onCancel && onCancel();
}}
>
<div className='mt-3'>
<Form layout='inline'>
<Form.Item label='Excel上传:'>
<Upload style={{ display: 'inline' }} {...uploadProps }>
<Button icon={
<UploadOutlined />}>
选择文件上传
</Button>
</Upload>
</Form.Item>
<Form.Item>
<Button type='primary' onClick={handleOk} loading={confirmLoading}>确定导入</Button>
</Form.Item>
<Form.Item>
<Button icon={<DownloadOutlined />} onClick={downloadTemplate }>
模版下载
</Button>
</Form.Item>
</Form>
</div>
<div className='d-flex my-3' style={{ justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ marginBottom: 0 }}>导入日志</h3>
<Button onClick={onRefreshClick}>刷新</Button>
</div>
<ResizeableTable
className='mt-3'
columns={columns}
rowKey={'id'}
dataSource={logs||[]}
pagination={false}
loading={loading}
expandable={{
expandedRowRender: record => <ExpandedRow data={record} />
}}
sticky
/>
<Pagination
className="text-center mt-3"
showSizeChanger
showQuickJumper
onChange={changeCurrent}
onShowSizeChange={changeCurrent}
current={pageNum}
pageSize={pageSize}
defaultCurrent={1}
total={total}
pageSizeOptions={[10,20]}
showTotal={total => `共 ${total} 条`}
/>
</Drawer>
)
}
export default ImportDataDrawer;
\ No newline at end of file
import { useMemo, useState, useEffect } from 'react';
import { Space, Button, Input, Pagination, Tooltip } from 'antd';
import { useContextMenu, Menu as RcMenu, Item as RcItem } from "react-contexify";
import ResizeableTable from '../../../ResizeableTable';
import DebounceInput from '../../../Model/Component/DebounceInput';
import UpdateDataMasterModal from './UpdateDataMasterModal';
import ImportDataDrawer from './ImportDataDrawer';
import { inputWidth, showMessage } from '../../../../../util';
import { dispatch } from '../../../../../model';
import '../../Define/Component/DefineTable.less';
import 'react-contexify/dist/ReactContexify.css';
const InputDebounce = DebounceInput(300)(Input);
const ManageTable = (props) => {
const {nodeId} = props;
const [keyword, setKeyword] = useState('');
const [checkedKeys, setCheckedKeys] = useState([]);
const [isDataMasterModalVisible, setIsDataMasterModalVisible] = useState(false);
const [currentData, setCurrentData] = useState(null);
const [action, setAction] = useState('');
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [fields, setFields] = useState([]);
const [tableData, setTableData] = useState([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState({pageNum: 1, pageSize: 20});
const [importDataDrawerVisible, setImportDataDrawerVisible] = useState(false);
const {pageNum, pageSize} = pagination;
const columns = useMemo(() => {
const newColumns = [
{
title: '序号',
dataIndex: 'key',
render: (text, record, index) => {
return (index+1).toString();
},
width: 60,
ellipsis: true,
}
];
(fields||[]).forEach(item => {
newColumns.push({
title: item.name,
dataIndex: item.name,
width: 200,
ellipsis: true,
});
})
return newColumns;
}, [fields]);
const MENU_ID = 'data-master-manage-table-contextmenu';
const { show } = useContextMenu({
id: MENU_ID,
});
useEffect(() => {
setCheckedKeys([]);
setPagination({...pagination, pageNum: 1});
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodeId])
useEffect(() => {
if (!nodeId || nodeId==='') {
setFields([]);
setTableData([]);
setTotal(0);
} else {
getFiledsAndDatas();
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [keyword, pagination])
const getFiledsAndDatas = () => {
setLoading(true);
dispatch({
type: 'msd.getColumns',
payload: {
params: {
modelId: nodeId
}
},
callback: (data) => {
setFields(data||[]);
const newKeyList = [];
(data||[]).forEach(item => {
newKeyList.push(item.name||'');
});
dispatch({
type: 'msd.getDatas',
payload: {
params: {
modelId: nodeId,
keyList: newKeyList.join(','),
keyword,
pageNum,
pageSize
}
},
callback: (data) => {
setLoading(false);
setTableData(data?.content||[]);
setTotal(data?.totalElements||0);
},
error: () => {
setLoading(false);
}
});
},
error: () => {
setLoading(false);
}
});
}
const onAddClick = () => {
setAction('add');
setCurrentData(null);
setIsDataMasterModalVisible(true);
}
const onImportClick = () => {
setImportDataDrawerVisible(true);
}
const onDataMasterModalCancel = (refresh = false) => {
setIsDataMasterModalVisible(false);
if (refresh) {
getFiledsAndDatas();
}
}
const onBatchDeleteClick = () => {
setDeleteLoading(true);
dispatch({
type: 'msd.deleteDatas',
payload: {
data: checkedKeys||[]
},
callback: (data) => {
setDeleteLoading(false);
showMessage('success', '删除成功');
getFiledsAndDatas();
},
error: () => {
setDeleteLoading(false);
}
});
}
const onSearchInputChange = (value) => {
setKeyword(value);
}
const onTableSelectChange = keys => {
setCheckedKeys(keys);
};
const handleItemClick = ({ event, props, data, triggerEvent }) => {
const key = event.currentTarget.id;
if (key === 'detail') {
setAction('detail');
setIsDataMasterModalVisible(true);
} if (key === 'update') {
setAction('update');
setIsDataMasterModalVisible(true);
}
}
const changeCurrent = (page,size) => {
setCheckedKeys([]);
setPagination({ pageNum: page, pageSize: size });
}
const onImportDataDrawerCancel = () => {
setImportDataDrawerVisible(false);
}
const onImportDataSuccess = () => {
getFiledsAndDatas();
}
const rowSelection = {
selectedRowKeys: checkedKeys,
onChange: onTableSelectChange,
};
return (
<div className='data-master-table'>
<div className='data-master-header px-3'>
<Space>
<Space>
<Button onClick={onAddClick}>新建</Button>
</Space>
<Space>
<Button onClick={onImportClick}>导入</Button>
</Space>
<Space>
<Tooltip title={(checkedKeys||[]).length===0?'请先选择主数据':''}>
<Button onClick={onBatchDeleteClick} disabled={(checkedKeys||[]).length===0} loading={deleteLoading}>删除</Button>
</Tooltip>
</Space>
</Space>
<InputDebounce
placeholder="请输入关键字"
allowClear
value={keyword}
onChange={onSearchInputChange}
style={{ width: inputWidth, marginLeft: 'auto' }}
/>
</div>
<div className='data-master-content p-3'>
<ResizeableTable
rowKey='_id'
loading={loading}
rowSelection={rowSelection}
columns={columns}
dataSource={tableData}
onRow={(record) => {
return {
onContextMenu: event => {
setCurrentData(record);
show(event);
},
};
}}
pagination={false}
scroll={{ y: 'calc(100vh - 94px - 32px - 57px - 24px - 39px - 36px)' }}
/>
<Pagination
size="small"
className="text-center m-3"
showSizeChanger
showQuickJumper
onChange={changeCurrent}
onShowSizeChange={changeCurrent}
current={pageNum}
pageSize={pageSize}
defaultCurrent={1}
total={total}
showTotal={total => `共 ${total} 条`}
/>
</div>
<UpdateDataMasterModal
action={action}
data={currentData}
fields={fields}
nodeId={nodeId}
visible={isDataMasterModalVisible}
onCancel={onDataMasterModalCancel}
/>
<ImportDataDrawer
visible={importDataDrawerVisible}
nodeId={nodeId}
onCancel={onImportDataDrawerCancel}
onSuccess={onImportDataSuccess}
/>
<RcMenu id={MENU_ID}>
<RcItem id="detail" onClick={handleItemClick}>
详情
</RcItem>
<RcItem id="update" onClick={handleItemClick}>
编辑
</RcItem>
</RcMenu>
</div>
);
}
export default ManageTable;
\ No newline at end of file
import {useEffect, useMemo, useState, useRef} from 'react';
import {Tooltip, Spin, AutoComplete, Tree} from 'antd';
import {ReloadOutlined} from '@ant-design/icons';
import { dispatch } from '../../../../../model';
import {highlightSearchContentByTerms} from '../../../../../util';
import '../../Define/Component/DefineTree.less';
const {Option} = AutoComplete;
const ManageTree = (props) => {
const {onClick} = props;
const [loading, setLoading] = useState(false);
const [data, setData] = useState();
const [options, setOptions] = useState([]);
const [keyword, setKeyword] = useState('');
const [expandedKeys, setExpandedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(false);
const selectedKeysRef = useRef([]);
useEffect(() => {
getTreeNodes();
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const treeData = useMemo(() => {
const loop = (data) =>
(data||[]).map((item) => {
if (item.children) {
return {
title: item.name||'',
key: item._id||'',
origin: item,
children: loop(item.children),
};
}
return {
title: item.name||'',
key: item._id||'',
};
});
return loop(data);
}, [data])
const treeList = useMemo(() => {
const generateList = (data, list, path = null) => {
(data||[]).forEach(node => {
const {_id, name} = node;
const currentPath = path ? `${path}/${name}` : name;
list.push({key: _id, title: currentPath});
if (node.children) {
generateList(node.children, list, currentPath);
}
});
};
const newTreeList = [];
generateList(data, newTreeList);
return newTreeList;
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
const getTreeNodes = () => {
setLoading(true);
dispatch({
type: 'msd.getMgrTreeNodes',
callback: (data) => {
setLoading(false);
setData(data||[]);
const firstModelNodeId = findFirstModelNode(data||[]);
if ((selectedKeysRef.current||[]).length===0 && (data||[]).length>0) {
onTreeSelect([firstModelNodeId]);
}
if ((expandedKeys||[]).length===0 && (data||[]).length>0) {
setExpandedKeys([firstModelNodeId]);
setAutoExpandParent(true);
} else {
setExpandedKeys(Array.from(new Set([...expandedKeys, ...selectedKeysRef.current])));
setAutoExpandParent(true);
}
},
error: () => {
setLoading(false);
}
})
}
const onRefreshClick = () => {
getTreeNodes();
}
const onAutoCompleteSearch = (searchText) => {
setKeyword(searchText);
setOptions(treeList.filter(item => item.title.indexOf(searchText)!==-1));
};
const onAutoCompleteSelect = (value, option) => {
const paths = value.split('/');
setKeyword(paths[paths.length-1]);
if (isModelNode(option.key)) {
onTreeSelect([option.key]);
}
setExpandedKeys(Array.from(new Set([...expandedKeys, option.key])));
setAutoExpandParent(true);
};
const onAutoCompleteClear = () => {
setKeyword('');
}
const onTreeExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
}
const onTreeSelect = (selectedKeys, e = null) => {
if (selectedKeys.length === 0) {
return;
}
if (e && e?.node && !isModelNode(e?.node?.key)) {
return;
}
setSelectedKeys(selectedKeys);
selectedKeysRef.current = selectedKeys;
onClick && onClick(selectedKeys[0]);
}
const isModelNode = (value) => {
return ((value||'').indexOf('Model=') !== -1);
}
const findFirstModelNode = (values) => {
const generateList = (data, list) => {
(data||[]).forEach(node => {
list.push(node);
if (node.children) {
generateList(node.children, list);
}
});
};
const newTreeList = [];
generateList(values, newTreeList);
let firstModelNodeId = '';
newTreeList.some(item => {
if (isModelNode(item._id)) {
firstModelNodeId = item._id;
}
return isModelNode(item._id);
})
return firstModelNodeId;
}
return (
<div className='data-master-tree'>
<div className='header p-3'>
<Tooltip title="刷新目录" className='ml-2'>
<ReloadOutlined
className='default'
onClick={onRefreshClick}
style={{
fontSize:16,
cursor:'pointer',
flex: 1
}}
/>
</Tooltip>
<div style={{flex: 3}} />
</div>
<div className='content p-3'>
<Spin spinning={loading}>
<AutoComplete
className='content-search'
allowClear
value={keyword}
onSelect={onAutoCompleteSelect}
onSearch={onAutoCompleteSearch}
onClear={onAutoCompleteClear}
>
{
(options||[]).map((item, index) => {
return (
<Option key={item.key} value={item.title}>
<div style={{ whiteSpace: 'normal' }}>
{highlightSearchContentByTerms(item.title, [keyword])}
</div>
</Option>
);
})
}
</AutoComplete>
<Tree
className='content-tree'
showLine
showIcon={false}
autoExpandParent={autoExpandParent}
treeData={treeData}
onExpand={onTreeExpand}
onSelect={onTreeSelect}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
/>
</Spin>
</div>
</div>
);
}
export default ManageTree;
\ No newline at end of file
import { useEffect, useState, useMemo } from "react";
import { Modal, Form, Input, Button, Descriptions } from "antd";
import { dispatch } from "../../../../../model";
import './UpdateDataMasterModal.less';
const UpdateDataMasterModal = (props) => {
const {visible, onCancel, data, fields, action = 'add', nodeId} = props;
const [confirmLoading, setConfirmLoading] = useState(false);
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 18 },
},
};
useEffect(() => {
if (visible) {
let newFieldsValue = {};
(fields||[]).forEach(item => {
newFieldsValue[item.name] = data?data[item.name]:'';
});
form?.setFieldsValue(newFieldsValue);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, data, fields])
const title = useMemo(() => {
if (action === 'add') {
return '新建主数据';
}
if (action === 'detail') {
return '主数据详情';
}
if (action === 'update') {
return '编辑主数据';
}
return '';
}, [action])
const onFormFinish = async() => {
try {
const row = await form.validateFields();
setConfirmLoading(true);
let url = '', newData = {};
if (action === 'add') {
url = 'msd.addData';
newData = row;
} else {
url = 'msd.updateData';
newData = {...data, ...row};
}
dispatch({
type: url,
payload: {
params: {
modelId: nodeId,
},
data: newData
},
callback: () => {
setConfirmLoading(false);
onCancel && onCancel(true);
},
error: () => {
setConfirmLoading(false);
}
});
} catch (errInfo) {
console.log('Validate Failed:', errInfo);
}
}
return (
<Modal
className='update-data-master-modal'
title={title}
visible={visible}
onCancel={() => { onCancel && onCancel(false); }}
onOk={onFormFinish}
footer={[
<Button key="cancel" onClick={() => {onCancel && onCancel();}}>
取消
</Button>,
action!=='detail' && <Button
key="ok"
type="primary"
loading={confirmLoading}
onClick={onFormFinish}
>
确定
</Button>,
]}
>
{
(action!=='detail') ? <Form
form={form}
{...formItemLayout}
>
{
(fields||[]).map((item, index) => {
return (
<Form.Item key={index} label={item.name} name={item.name} rules={[{ required: false }]}>
<Input />
</Form.Item>
)
})
}
</Form> : <Descriptions column={1}>
{
(fields||[]).map((item, index) => {
return (
<Descriptions.Item label={<div style={{ textAlign: 'right', width: 85 }}>{item.name||''}</div>} >{data?data[item.name||'']:''}</Descriptions.Item>
);
})
}
</Descriptions>
}
</Modal>
);
}
export default UpdateDataMasterModal;
\ No newline at end of file
.update-data-master-modal {
.yy-modal-body {
max-height: 70vh;
overflow: auto;
}
}
\ No newline at end of file
import { useMemo, useState } from 'react';
import classNames from 'classnames';
import { ResizableBox } from 'react-resizable';
import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
import ManageTree from './Component/ManageTree';
import ManageTable from './Component/ManageTable';
import '../Define/index.less';
const DataMasterManage = (props) => {
const [collapse, setCollapse] = useState(false);
const [nodeId, setNodeId] = useState('');
const classes = useMemo(() => {
return classNames('data-master', {
'data-master-collapse': collapse,
});
}, [collapse]);
const onTreeClick = (value) => {
setNodeId(value);
}
const onCollapseClick = () => {
setCollapse(!collapse);
}
return (
<div className={classes}>
<ResizableBox
className='left-wrap'
width={230}
height={Infinity}
axis='x'
minConstraints={[230, Infinity]} maxConstraints={[Infinity, Infinity]}
>
<ManageTree onClick={onTreeClick} />
</ResizableBox>
<div className='left-collapse-wrap'>
<div className='left-collapse' onClick={onCollapseClick}>
{ collapse ? <CaretRightOutlined /> : <CaretLeftOutlined /> }
</div>
</div>
<div className='right-wrap'>
<ManageTable nodeId={nodeId} />
</div>
</div>
)
}
export default DataMasterManage;
\ No newline at end of file
......@@ -11,6 +11,8 @@ import AssetResourceBrowse from './AssetResourceBrowse';
import AssetBrowse from './AssetBrowse';
import AssetRecycle from './AssetRecycle';
import DataService from './Model';
import DataMasterDefine from "./DataMaster/Define";
import DataMasterManage from "./DataMaster/Manage";
class Manage extends Component {
......@@ -31,6 +33,8 @@ class Manage extends Component {
<Route path={`${match.path}/asset-browse`} component={AssetBrowse} />
<Route path={`${match.path}/asset-recycle`} component={AssetRecycle} />
<Route path={`${match.path}/data-service`} component={DataService} />
<Route path={`${match.path}/msd-define`} component={DataMasterDefine} />
<Route path={`${match.path}/msd-manage`} component={DataMasterManage} />
</Switch>
) : (
<GetSession {...this.props} />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment