UNPKG

@ithinkdt/naive

Version:

iThinkDT Naive UI

311 lines (286 loc) 11.9 kB
import { defineComponent, watch, computed, reactive, toRef, ref } from 'vue' import { refDebounced } from '@vueuse/core' import { walkTree } from '@ithinkdt/common' import { NInput, NIcon, NSkeleton, NTree, NButton } from 'ithinkdt-ui' import { $dialog, $msg } from '@ithinkdt/core' import { c, cB, cE, CSS_MOUNT_ANCHOR_META_NAME, CSS_STYLE_PREFIX as p, flexGap, flexDirCol } from '@ithinkdt/core/cssr' import { ISearch } from '../assets' import { useModuleCurd } from './curd' import { useModuleRender } from './render' export const DtModuleTree = defineComponent({ name: 'ModuleTree', props: { data: { type: Array, default: () => [] }, save: { type: Function, default: undefined }, delete: { type: Function, default: undefined }, loading: { type: Boolean, default: false }, forms: { type: [Array, Function], default: undefined }, formWidth: { type: [String, Number], default: undefined }, formHeight: { type: [String, Number], default: undefined }, draggable: { type: Boolean, default: false }, creatable: { type: Boolean, default: false }, modifiable: { type: Boolean, default: false }, deletable: { type: Boolean, default: false }, selectable: { type: Boolean, default: false }, showSubmitBtn: { type: Boolean, default: undefined }, submiting: { type: Boolean, default: false }, selection: { type: Array, default: () => [] }, treeProps: { type: Object, default: () => ({}) }, iconColls: { type: Object, default: () => ({}) }, iconLoader: { type: Function, default: undefined }, apps: { type: Array, default: () => [] }, lcSelectable: { type: Boolean, default: false }, getPages: { type: Function, default: undefined }, }, emits: { refresh: () => true, submit: () => true, 'update:selection': () => true, 'update-selection': () => true, }, setup(props, { emit, slots }) { const cls = `${p}-module-tree` createStyle(cls) const data = computed(() => reactive(props.data)) function save(m) { return props.save(m) } const curd = useModuleCurd(save, (...p) => props.delete(...p), { width: toRef(props, 'formWidth'), height: toRef(props, 'formHeight'), apps: toRef(props, 'apps'), getPages: toRef(props, 'getPages'), lcSelectable: toRef(props, 'lcSelectable'), forms: toRef(props, 'forms'), colls: toRef(props, 'iconColls'), loader: toRef(props, 'iconLoader'), }) // 节点渲染 const { filterModule, renderIcon, renderAction, renderLabel: _renderLabel } = useModuleRender() const renderPrefix = ({ option }) => renderIcon(option) const renderLabel = ({ option }) => _renderLabel(option) const renderSuffix = ({ option }) => renderAction(option, props, curd) // 过滤 const pattern = ref() const debouncedPattern = refDebounced(pattern, 300) // 拖拽 const nodeProps = ({ option }) => ({ ondrop: (e) => { onCustomDrop(e, option) }, }) const saveDrop = (module, target, type) => { $dialog({ title: '提示', content: `确定${type == 'copy' ? '复制' : '移动'}模块 ${module.label}${target.label} ?`, okText: type == 'copy' ? '复 制' : '移 动', onOk() { if (type === 'copy') { module.id = undefined module.key = undefined } return save({ ...module, parentKey: target.key, }) .then(() => { if (module.children?.length) { const ps = [] walkTree(module.children, (it) => { ps.push(save(it)) }) return Promise.all(ps) } }) .then(() => { $msg.success('操作成功') emit('refresh') }) }, }) } const onDragStart = ({ node, event }) => { event.dataTransfer?.setData('other-window-modules', JSON.stringify(node)) } const onCustomDrop = (event, target) => { if (!target) return const data = event.dataTransfer?.getData('other-window-modules') if (!data) return saveDrop(JSON.parse(data), target, 'copy') } const onDrop = ({ node, dragNode, dropPosition, event }) => { event.stopPropagation() let parent = node if (node.parent && dropPosition !== 'inside') { parent = node.parent } if (!parent || dragNode.parentKey === parent.key) return saveDrop(dragNode, parent, 'move') } // 选择 const selection = ref([]) watch( () => props.selection, (data) => { selection.value = [...(data || [])] }, { immediate: true }, ) const emitSelection = (data) => { if (props.selection === undefined) { selection.value = data } else { emit('update:selection', data) emit('update-selection', data) } } // 提交 const showSubmitBtn = computed(() => props.showSubmitBtn !== false && props.selectable) // 展开 let expandedKeys watch( [() => props.treeProps, () => props.data], ([treeProps, data], [_, oldData]) => { if (!treeProps.expandedKeys || treeProps.expandedKeys === expandedKeys) { expandedKeys = treeProps.expandedKeys = expandedKeys ?? reactive([]) treeProps.onUpdateExpandedKeys = (expandedKeys) => { treeProps.expandedKeys.length = 0 treeProps.expandedKeys.splice(0, 0, ...expandedKeys) } if (expandedKeys.length === 0 || oldData?.length !== data.length) { const arr = [] while (data?.length === 1) { arr.push(data[0].id) data = data[0].children } treeProps.onUpdateExpandedKeys(arr) } } }, { immediate: true }, ) return () => ( <div class={cls}> <div class={`${cls}__action`}> <NInput class={`${cls}__search`} v-model:value={pattern.value} placeholder="搜索模块名称/路径/资源"> {{ suffix: () => ( <NIcon size={20}> <ISearch /> </NIcon> ), }} </NInput> <NButton type="primary" ghost loading={props.loading} onClick={() => emit('refresh')}> 刷 新 </NButton> {showSubmitBtn.value ? ( <NButton type="primary" disabled={props.loading} loading={props.submiting} onClick={() => emit('submit')} > 保 存 </NButton> ) : undefined} </div> {slots.default?.()} <div class={`${cls}__content`}> {props.data?.length ? ( <NTree class={`${cls}__tree`} data={data.value} blockLine={props.selectable} expandOnClick pattern={debouncedPattern.value} filter={filterModule} showIrrelevantNodes={false} renderSuffix={renderSuffix} renderPrefix={renderPrefix} renderLabel={renderLabel} draggable={props.draggable} expandOnDragenter allowDrop={() => props.draggable} nodeProps={nodeProps} onDragstart={onDragStart} onDrop={onDrop} checkable={props.selectable} cascade={props.selectable} checkedKeys={selection.value} onUpdateCheckedKeys={emitSelection} {...props.treeProps} /> ) : ( <div class={`${cls}__skeleton`}> <NSkeleton text repeat={2} width="240px" /> <NSkeleton text repeat={4} width="350px" /> <NSkeleton text repeat={5} width="240px" /> <NSkeleton text repeat={3} width="350px" /> <NSkeleton text repeat={5} width="240px" /> </div> )} </div> </div> ) }, }) let style function createStyle(cls) { if (!style) { style = cB( 'module-tree', { ...flexDirCol, }, [ cE('action', { ...flexGap('16px'), position: 'sticky', backgroundColor: `var(--${p}-base-color)`, top: 0, paddingBottom: '16px', zIndex: 1, width: '400px', }), cE('search', { flex: '1 1 auto', }), cE('content', { ...flexDirCol, flex: '1 1 auto', overflow: 'hidden', padding: '0 56px 20px 0', }), cE('skeleton', { ...flexDirCol, ...flexGap('16px'), }), cE('tree', [ c('.n-tree-node-wrapper', { padding: 'unset', }), c('.n-tree-node', { padding: '4px 0', }), c('.n-tree-node-content .n-tree-node-content__suffix', { transition: 'opacity 0.3s ease-in-out', }), c('&.n-tree--block-node .n-tree-node-content .n-tree-node-content__suffix', { position: 'absolute', right: '0', marginLeft: '12px', backdropFilter: 'blur(3px)', }), c('.n-tree-node-content:not(:hover) .n-tree-node-content__suffix', { opacity: 0, }), ]), ], ) style.mount({ id: cls, anchorMetaName: CSS_MOUNT_ANCHOR_META_NAME, }) } }