Commit 8c1bcfd2 by zhaochengxiang

资源管理目录

parent c6f67e19
......@@ -24,6 +24,7 @@
"crypto-js": "^4.0.0",
"echarts": "^5.3.1",
"eslint-config-react-app": "^7.0.1",
"immer": "9.0.15",
"immutability-helper": "^3.1.1",
"insert-css": "^2.0.0",
"less": "^4.1.1",
......
......@@ -24,6 +24,10 @@ export const routes = [
text: '模型配置',
},
{
name: 'asset-resource-manage',
text: '资源管理',
},
{
name: 'asset-manage',
text: '资产管理',
},
......
import React from "react"
import { Tree } from "antd"
import { Menu, Item, useContextMenu } from 'react-contexify'
import { generateUUID } from ".."
import 'react-contexify/dist/ReactContexify.css';
const FC = ({ shouldRowContextMenu, menuData, onMenuItemClick, ...restProps }) => {
const MENU_ID = generateUUID()
const { show } = useContextMenu({
id: MENU_ID,
})
const [rightClickNode, setRightClickNode] = React.useState(undefined)
const handleContextMenu = (event, node) => {
show(event, {
position: {
x: event.clientX + 30,
y: event.clientY - 10
}
})
}
return (
<div>
<Tree
onRightClick={({event, node}) => {
setRightClickNode(node)
if (shouldRowContextMenu?.(node)) {
handleContextMenu(event, node)
}
}}
{...restProps}
/>
<Menu id={MENU_ID}>
{
menuData?.map(item => <Item key={item.id} id={item.id} onClick={() => onMenuItemClick?.(item.id, rightClickNode)}>{item.title}</Item>)
}
</Menu>
</div>
)
}
export default FC
\ No newline at end of file
import React from "react"
import classNames from 'classnames'
import { ResizableBox } from 'react-resizable'
import Tree from './tree'
import Separate from '../AssetManage/Component/Separate'
import '../AssetManage/index.less'
const FC = (props) => {
const [collapseTree, setCollapseTree] = React.useState(false)
const rootClasses = classNames('asset-manage', {
'asset-manage-collapse': collapseTree
})
return (
<div className={rootClasses}>
<ResizableBox
className='left'
width={230}
height={Infinity}
axis='x'
minConstraints={[230, Infinity]}
maxConstraints={[Infinity, Infinity]}
>
<Tree />
</ResizableBox>
{
!collapseTree && <Separate width={15} />
}
</div>
)
}
export default FC
\ No newline at end of file
import React, { useEffect, useMemo, useState } from 'react';
import { Tooltip, Spin, Modal, Button, AutoComplete, Menu, Dropdown } from 'antd'
import { PlusOutlined, ReloadOutlined, ImportOutlined, ExportOutlined, SettingOutlined } from '@ant-design/icons'
import produce from 'immer'
import { dispatch } from '../../../model'
import { showMessage, highlightSearchContentByTerms } from '../../../util'
import Tree from '../../../util/Component/Tree'
import PermissionButton from '../../../util/Component/PermissionButton'
import UpdateNode from '../AssetManage/Component/UpdateDirectoryModal'
import ImportNode from '../AssetManage/Component/ImportDirectory'
import CustomNode from '../AssetManage/Component/CustomDirectoryModal'
import './tree.less'
const generateList = (data, list, path = null) => {
for (const node of data??[]) {
if (node.resourceType !== 'custom') {
const newPath = path?`${path}/${node.text}`:node.text
list.push({...node, key: node.nodeId, value: newPath})
if (node.children) {
generateList(node.children, list, newPath)
}
}
}
}
const updateTreeData = (list, key, children) => {
return list.map((node) => {
if (node.nodeId === key) {
return {...node, children}
}
if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
})
}
const FC = (props) => {
const {onClick} = props
const [loading, setLoading] = useState(false)
const [data, setData] = useState(undefined)
const [dataList, setDataList] = useState()
const [keyword, setKeyword] = useState()
const [options, setOptions] = useState()
const [expandedKeys, setExpandedKeys] = useState([])
const [loadedKeys, setLoadedKeys] = useState([])
const [selectedKey, setSelectedKey] = useState('')
const [rightSelectedNode, setRightSelectedNode] = useState()
const [autoExpandParent, setAutoExpandParent] = useState(false)
const [updateNodeParam, setUpdateNodeParam] = useState({
visible: false,
action: undefined,
dirId: undefined,
})
const [importNodeParam, setImportNodeParam] = useState({
visible: false,
dirId: undefined
})
const [customNodeParam, setCustomNodeParam] = useState({
visible: false,
})
const [permissions, setPermissions] = useState()
const [modal, contextHolder] = Modal.useModal()
useEffect(() => {
getTreeData()
getPermissions()
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const havePermission = useMemo(() => {
return (permissions??[]).findIndex(item => item === 'dirManage') !== -1
}, [permissions])
const treeData = useMemo(() => {
if (data) {
const newTreeData = produce(data, draft => {
const setNode = (g) => {
g.key = g.nodeId
g.title = (
<span className={(g.level===1)?'title-color':'text-color'}>
{g.text}
{
//自定义类型栏目不统计资产数
(g.level === 1 && g.resourceType === 'custom') ? null : <span>{` (${g.dataAssetAndSubDirCount})`}</span>
}
</span>
)
g.children?.forEach((child) => {
setNode(child)
})
if (g.resourceType !== 'custom' && g.type !== 'custom' && (g.children??[]).length === 0) {
g.isLeaf = true
}
if ((g.resourceType === 'custom'||g.type === 'custom') && (g.children??[]).length === 0) {
g.children = null
}
}
draft.forEach((child) => {
setNode(child)
})
})
return newTreeData
}
return []
}, [data])
const menuData = useMemo(() => {
if (rightSelectedNode) {
if (rightSelectedNode.level === 1) {
return [
{ id: 'edit', title: '编辑栏目' },
{ id: 'up', title: '上移栏目' },
{ id: 'down', title: '下移栏目' },
{ id: 'delete', title: '删除栏目' },
]
} else if (rightSelectedNode.resourceType === 'custom' || rightSelectedNode.type === 'custom') {
return [
{ id: 'up', title: '上移目录' },
{ id: 'down', title: '下移目录' },
{ id: 'delete', title: '删除目录' },
]
} else {
return [
{ id: 'edit', title: '编辑目录' },
{ id: 'up', title: '上移目录' },
{ id: 'down', title: '下移目录' },
{ id: 'delete', title: '删除目录' },
]
}
}
return []
}, [rightSelectedNode])
const resetSelectedNodeLogic = () => {
setData(prevData => {
setSelectedKey(prevSelectedKey => {
if ((prevData??[]).length === 0) return;
const firstNode = prevData[0]
if (!prevSelectedKey) {
setExpandedKeys(Array.from(new Set([...expandedKeys, firstNode.nodeId])))
setAutoExpandParent(true)
onTreeSelect([firstNode.nodeId], { selectedNodes: [firstNode]})
} else {
setDataList(prevDataList => {
const filterNodes = (prevDataList??[]).filter(item => item.nodeId === prevSelectedKey)
if (filterNodes?.length > 0) {
const newExpandedKeys = [...expandedKeys, prevSelectedKey]
setExpandedKeys(Array.from(new Set(newExpandedKeys)))
setAutoExpandParent(true)
onTreeSelect([prevSelectedKey], {selectedNodes: filterNodes})
}
return prevDataList
})
}
return prevSelectedKey
})
return prevData
})
}
const getTreeData = (resetSelectedNode = true) => {
setLoading(true)
setSelectedKey(prevSelectedKey => {
dispatch({
type: 'assetmanage.queryAllDirectoryAsTree',
callback: data => {
setLoading(false)
setLoadedKeys([])
setData(data)
if ((data??[]).length > 0) {
const newDataList = []
generateList(data, newDataList)
setDataList(newDataList)
if (resetSelectedNode) {
resetSelectedNodeLogic()
}
}
},
error: () => {
setLoading(false)
}
})
return prevSelectedKey
})
}
const getPermissions = () => {
dispatch({
type: 'assetmanage.getPrivilegeByRange',
payload: {
range: 'dataAsset_dataAssetManage',
},
callback: data => {
setPermissions(data);
}
})
}
const onAddClick = () => {
setUpdateNodeParam({
type: 'add',
visible: true,
dirId: selectedKey
})
}
const onImportClick = () => {
setImportNodeParam({
visible: true,
dirId: selectedKey
})
}
const onCustomClick = () => {
setCustomNodeParam({
visible: true
})
}
const onDeleteNodeClick = (node) => {
modal.confirm({
title: '提示',
content: '节点下可能包含资源信息,删除后将把资源从该目录上移除,确定继续吗?',
onOk: () => {
dispatch({
type: 'assetmanage.deleteDirectory',
payload: {data: [ rightSelectedNode?.nodeId ]},
callback: () => {
showMessage("success","删除成功")
if (rightSelectedNode?.nodeId === selectedKey) {
setSelectedKey()
getTreeData()
} else {
getTreeData(false)
}
}
})
}
})
}
const onMoveNodeClick = (steps) => {
setLoading(true);
dispatch({
type: 'assetmanage.upDownDirectory',
payload: {
params: {
dirId: rightSelectedNode?.nodeId,
steps
}
},
callback: () => {
showMessage('success', (steps===1)?'上移目录成功':'下移目录成功');
getTreeData(false)
},
error: () => {
setLoading(false)
}
});
}
const onRefreshClick = () => {
getTreeData(false)
}
const onTreeExpand = (expandedKeys) => {
setExpandedKeys(expandedKeys)
setAutoExpandParent(false)
}
const onTreeSelect = (selectedKeys, { selectedNodes }) => {
if (selectedKeys.length === 0 || selectedNodes.length === 0) {
return
}
setSelectedKey(selectedKeys[0])
onClick && onClick(selectedNodes[0])
}
const onUpdateNodeCancel = (refresh = false, nodeId = undefined) => {
setUpdateNodeParam({
type: undefined,
visible: false,
dirId: undefined
})
if (refresh) {
if (nodeId) {
setSelectedKey(nodeId)
getTreeData()
} else {
getTreeData(false)
}
}
}
const onImportNodeCancel = (refresh = false, resetSelectedNode = false) => {
setImportNodeParam({
visible: false,
dirId: undefined
})
refresh && getTreeData(resetSelectedNode)
}
const onCustomNodeCancel = (refresh = false) => {
setCustomNodeParam({
visible: false
})
if (refresh) {
getTreeData(false)
}
}
const onMenuItemClick = (key, node) => {
if (key === 'delete') {
onDeleteNodeClick(node)
} else if (key === 'edit') {
setUpdateNodeParam({
type: 'edit',
visible: true,
dirId: rightSelectedNode?.nodeId
})
} else if (key === 'up') {
onMoveNodeClick(1)
} else if (key === 'down') {
onMoveNodeClick(-1)
}
}
const onLoadData = ({ key, children }) =>
new Promise((resolve) => {
if (children) {
resolve()
return
}
setLoadedKeys([...loadedKeys, key])
dispatch({
type: 'assetmanage.getDirectoryChild',
payload: {
parentId: key,
},
callback: (_data) => {
let newData = updateTreeData(data, key, _data??[])
setData(newData)
resolve()
},
error: () => {
resolve()
}
})
})
const onAutoCompleteSearch = (searchText) => {
setKeyword(searchText)
setOptions(!searchText?[]:(dataList||[]).filter(item => item.value.indexOf(searchText)!==-1))
}
const onAutoCompleteSelect = (value, option) => {
const paths = value.split('/')
setKeyword(paths[paths.length-1])
setSelectedKey(option.key)
resetSelectedNodeLogic()
}
const exportMenu = (
<Menu>
<Menu.Item>
<div style={{ textAlign: 'center' }} onClick={() => {
window.open('/api/dataassetmanager/directoryApi/export')
}}>
导出所有
</div>
</Menu.Item>
<Menu.Item>
<div style={{ textAlign: 'center' }} onClick={() => {
if(selectedKey){
dispatch({
type: 'assetmanage.getDirectoryById',
payload: {
dirId: selectedKey
},
callback: data => {
window.open(`/api/dataassetmanager/directoryApi/export?parentPath=${data.path}`);
}
})
} else {
showMessage("warn","请选择目录")
}
}}>
导出选中目录
</div>
</Menu.Item>
</Menu>
)
return (
<div className='resource-manage-tree'>
<div className='header p-3'>
<PermissionButton
defaultPermission={havePermission}
tip="新增目录"
type="text"
icon={<PlusOutlined />}
onClick={onAddClick}
/>
<Tooltip title="刷新目录">
<Button type="text" icon={<ReloadOutlined />} onClick={onRefreshClick} />
</Tooltip>
<PermissionButton
defaultPermission={havePermission}
tip="导入目录"
type="text"
icon={<ImportOutlined />}
onClick={onImportClick}
/>
<Dropdown overlay={havePermission?exportMenu:<></>} placement="bottomCenter" >
<PermissionButton
defaultPermission={havePermission}
tip="导出目录"
type="text"
icon={<ExportOutlined />}
/>
</Dropdown>
<PermissionButton
defaultPermission={havePermission}
tip="自定义目录"
type="text"
icon={<SettingOutlined />}
onClick={onCustomClick}
/>
</div>
<div className='p-3'>
<Spin spinning={loading}>
<AutoComplete
allowClear
value={keyword}
style={{ marginBottom: 10, width: '100%' }}
onSelect={onAutoCompleteSelect}
onSearch={onAutoCompleteSearch}
onClear={() => {
setKeyword()
}}
>
{
(options||[]).map((item, index) => {
return (
<AutoComplete.Option key={item.key} value={item.value}>
<div style={{ whiteSpace: 'normal' }}>
{highlightSearchContentByTerms(item.value, [keyword])}
</div>
</AutoComplete.Option>
);
})
}
</AutoComplete>
<Tree
className='tree'
showLine
showIcon={false}
autoExpandParent={autoExpandParent}
treeData={treeData}
loadData={onLoadData}
loadedKeys={loadedKeys}
onExpand={onTreeExpand}
onSelect={onTreeSelect}
expandedKeys={expandedKeys}
selectedKeys={[selectedKey]}
shouldRowContextMenu={(node) => {
setRightSelectedNode(node)
return havePermission
}}
menuData={menuData}
onMenuItemClick={onMenuItemClick}
/>
</Spin>
</div>
<UpdateNode
{...updateNodeParam}
onCancel={onUpdateNodeCancel}
/>
<ImportNode
{...importNodeParam}
onCancel={onImportNodeCancel}
/>
<CustomNode
action='add'
{...customNodeParam}
onCancel={onCustomNodeCancel}
/>
{contextHolder}
</div>
)
}
export default FC
\ No newline at end of file
@import '../../../variables.less';
.resource-manage-tree {
height: 100%;
.header {
display: flex;
flex: none;
width: 100%;
height: 57px;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #EFEFEF;
}
.tree {
height: calc(100vh - @header-height - @breadcrumb-height - 25px - 57px - 62px);
overflow: auto;
}
}
\ No newline at end of file
......@@ -8,6 +8,7 @@ import { ManageLayout } from "../../layout";
import DatasourceManage from './DatasourceManage';
import Model from './Model';
import ModelConfig from './ModelConfig';
import AssetResourceManage from './AssetResourceManage';
import AssetManage from './AssetManage';
import AssetResourceBrowse from './AssetResourceBrowse';
import AssetBrowse from './AssetBrowse';
......@@ -32,6 +33,7 @@ class Manage extends Component {
<Route path={`${match.path}/datasource-manage`} component={DatasourceManage} />
<Route path={`${match.path}/data-model`} component={Model} />
<Route path={`${match.path}/model-config`} component={ModelConfig} />
<Route path={`${match.path}/asset-resource-manage`} component={AssetResourceManage} />
<Route path={`${match.path}/asset-manage`} component={AssetManage} />
<Route path={`${match.path}/asset-resource-browse`} component={AssetResourceBrowse} />
<Route path={`${match.path}/asset-browse`} component={AssetBrowse} />
......
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