@ithinkdt/naive
Version:
iThinkDT Naive UI
311 lines (286 loc) • 11.9 kB
JSX
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,
})
}
}