27 changed files with 829 additions and 170 deletions
@ -1,7 +1,3 @@ |
|||
// 该文件为自动生成,请勿修改!!!
|
|||
import PsButton from "./button" |
|||
import PsCaptcha from "./captcha" |
|||
export { |
|||
PsButton, |
|||
PsCaptcha |
|||
} |
|||
export * from "./button" |
|||
export * from "./captcha" |
|||
export * from "./tree" |
@ -0,0 +1,14 @@ |
|||
import { App } from "vue" |
|||
import PsTree from "./tree.vue" |
|||
|
|||
export * from "./util" |
|||
export * from "./type" |
|||
|
|||
PsTree.install = function(app: App) { |
|||
app.component(PsTree.name || "ps-tree", PsTree) |
|||
} |
|||
|
|||
export { |
|||
PsTree, |
|||
} |
|||
export default PsTree |
@ -0,0 +1,154 @@ |
|||
<template> |
|||
<div class="ps-tree-node component" :class="[{ draging: status === ENiuTreeStatus.DragIng }]"> |
|||
<div class="ps-tree-node-wrapper" ref="nodeEL" :draggable="draggable" :onDragstart="onDragstart" |
|||
:onDragend="onDragend" :onDrop="onDrop" :onDragover="onDragover" :onDragleave="onDragleave" |
|||
@click.stop="emit('click', data)"> |
|||
<slot :data="data" :deep="level" :dataSourceKey="dataSourceKey" :status="status"> |
|||
<div :style="{ |
|||
marginLeft: level * 10 + 'px', |
|||
'position': 'relative', |
|||
zIndex: 10 |
|||
}"> |
|||
{{ data.title }} - {{ level * 10 + 'px' }} |
|||
</div> |
|||
<NiuTreeDrag v-bind="props" :status="status" :style="{ |
|||
marginLeft: level * 10 + 'px', |
|||
}"></NiuTreeDrag> |
|||
</slot> |
|||
</div> |
|||
<div class="ps-tree-sub-node" v-if="(opts.justOpen || data.isExpand) && data.children && data.children.length"> |
|||
<template v-for="(item, index) in data.children" :key="item.key"> |
|||
<node @onDragstart="(e: INiuTreeKey) => emit('onDragstart', e)" |
|||
@onDragend="(e: INiuTreeKey) => emit('onDragend', e)" |
|||
@onDrop="(e: INiuTreeKey, s?: ENiuTreeStatus) => emit('onDrop', e, s)" |
|||
@onDragover="(e: INiuTreeKey) => emit('onDragover', e)" |
|||
@onDragleave="(e: INiuTreeKey) => emit('onDragleave', e)" |
|||
@click="(e: INiuTreeData) => emit('click', e)" :data-source-key="dataSourceKey" :data="item" |
|||
:list="list" :level="level + 1"> |
|||
<template |
|||
#default="{ data, deep, dataSourceKey, status }: { data: INiuTreeData, deep: number, dataSourceKey: INiuTreeKey, status: ENiuTreeStatus }"> |
|||
<slot :data="data" :deep="deep" :dataSourceKey="dataSourceKey" :status="status"> |
|||
<div :style="{ |
|||
marginLeft: deep * 10 + 'px', |
|||
'position': 'relative', |
|||
zIndex: 10 |
|||
}"> |
|||
{{ data.title }} - {{ deep * 10 + 'px' }} |
|||
</div> |
|||
<NiuTreeDrag v-bind="props" :status="status" :style="{ |
|||
marginLeft: deep * 10 + 'px', |
|||
}"></NiuTreeDrag> |
|||
</slot> |
|||
</template> |
|||
</node> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { isChildOf } from './util' |
|||
import NiuTreeDrag from './tree-drag.vue'; |
|||
import { ENiuTreeStatus, INiuTreeData, INiuTreeKey } from './type'; |
|||
const props = withDefaults( |
|||
defineProps<{ |
|||
data: INiuTreeData |
|||
list: INiuTreeData[] |
|||
dataSourceKey?: INiuTreeKey |
|||
level?: number |
|||
}>(), |
|||
{ |
|||
level: 0, |
|||
} |
|||
) |
|||
const opts = inject("tree:opts", { |
|||
justOpen: false |
|||
}) |
|||
const emit = defineEmits<{ |
|||
(e: 'click', data: INiuTreeData): void |
|||
(e: 'onDragstart', key: INiuTreeKey): void |
|||
(e: 'onDragend', key: INiuTreeKey): void |
|||
(e: 'onDrop', key: INiuTreeKey, status?: ENiuTreeStatus): void |
|||
(e: 'onDragover', key: INiuTreeKey): void |
|||
(e: 'onDragleave', key: INiuTreeKey): void |
|||
}>() |
|||
|
|||
const draggable = ref(true) |
|||
const status = ref<ENiuTreeStatus>() |
|||
const nodeEL = ref<HTMLDivElement>() |
|||
provide("draggable", draggable) |
|||
function onDragstart(event: DragEvent) { |
|||
// 开始拖拽 |
|||
if (event.dataTransfer) { |
|||
event.dataTransfer.dropEffect = 'move' |
|||
event.dataTransfer.effectAllowed = 'move' |
|||
if (nodeEL.value) { |
|||
let clone = nodeEL.value.cloneNode(true) as HTMLDivElement |
|||
clone.id = 'dragging_node' |
|||
clone.style.display = 'inline-block' |
|||
clone.style.position = 'absolute' |
|||
clone.style.zIndex = '100000' |
|||
clone.style.width = '100px' |
|||
clone.style.top = '-100000px' |
|||
document.body.append(clone) |
|||
event.dataTransfer.setDragImage(clone, -4, -4) |
|||
} |
|||
emit('onDragstart', props.data.key) |
|||
status.value = ENiuTreeStatus.DragIng |
|||
} |
|||
} |
|||
function onDragend() { |
|||
// 结束拖拽 |
|||
let clone = document.getElementById('dragging_node') |
|||
clone?.remove() |
|||
status.value = undefined |
|||
emit('onDragend', props.data.key) |
|||
} |
|||
function onDrop(e: DragEvent) { |
|||
e.preventDefault() |
|||
emit('onDrop', props.data.key, status.value) |
|||
status.value = undefined |
|||
} |
|||
|
|||
function onDragover(event: DragEvent) { |
|||
event.preventDefault() |
|||
if (!props.dataSourceKey) return |
|||
if (props.dataSourceKey === props.data.key) return |
|||
if ( |
|||
props.dataSourceKey && |
|||
isChildOf(props.data.key, props.dataSourceKey, props.list) |
|||
) |
|||
return |
|||
const y = event.offsetY |
|||
const h = (event.currentTarget as HTMLDivElement).offsetHeight |
|||
if (y < h / 3) { |
|||
status.value = ENiuTreeStatus.DragUp |
|||
} else if (y <= (h * 2) / 3 && y >= h / 3 && props.data.children) { |
|||
status.value = ENiuTreeStatus.DragIn |
|||
} else if (y > (h * 2) / 3) { |
|||
status.value = ENiuTreeStatus.DragDown |
|||
} else { |
|||
status.value = undefined |
|||
} |
|||
emit('onDragover', props.data.key) |
|||
} |
|||
function onDragleave() { |
|||
if (!props.dataSourceKey) return |
|||
if (props.dataSourceKey === props.data.key) return |
|||
if ( |
|||
props.dataSourceKey && |
|||
isChildOf(props.data.key, props.dataSourceKey, props.list) |
|||
) |
|||
return |
|||
|
|||
// 拖拽离开元素上 |
|||
status.value = undefined |
|||
emit('onDragleave', props.data.key) |
|||
} |
|||
</script> |
|||
<script lang="ts"> |
|||
import { defineComponent, inject, ref, provide } from 'vue' |
|||
export default defineComponent({ |
|||
name: 'ps-tree-node', |
|||
}) |
|||
</script> |
@ -0,0 +1,34 @@ |
|||
<template> |
|||
<div :class="[ |
|||
{ |
|||
'ps-tree-drag-up': |
|||
dataSourceKey != data.key && |
|||
dataSourceKey != undefined && |
|||
!isChildOf(data.key, dataSourceKey, list) && |
|||
status === ENiuTreeStatus.DragUp, |
|||
'ps-tree-drag-in': |
|||
dataSourceKey != data.key && |
|||
dataSourceKey && |
|||
!isChildOf(data.key, dataSourceKey, list) && |
|||
status === ENiuTreeStatus.DragIn, |
|||
'ps-tree-drag-down': |
|||
dataSourceKey != data.key && |
|||
dataSourceKey && |
|||
!isChildOf(data.key, dataSourceKey, list) && |
|||
status === ENiuTreeStatus.DragDown, |
|||
}, |
|||
]"></div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { isChildOf } from "./util"; |
|||
import { ENiuTreeStatus, INiuTreeData, INiuTreeKey } from "./type"; |
|||
const props = withDefaults( |
|||
defineProps<{ |
|||
data: INiuTreeData |
|||
list: INiuTreeData[] |
|||
dataSourceKey?: INiuTreeKey |
|||
status?: ENiuTreeStatus |
|||
}>(), |
|||
{}, |
|||
) |
|||
</script> |
@ -0,0 +1,127 @@ |
|||
<template> |
|||
<div class="ps-tree component"> |
|||
<template v-for="(item, index) in list" :key="item.key"> |
|||
<node @onDragstart="onDragstart" @onDragEnd="onDragEnd" @onDrop="onDrop" :data-source-key="dataSourceKey" |
|||
:data="item" :list="list" :level="level" @click="(item) => clickNode(item)"> |
|||
<template |
|||
#default="{ data, deep, dataSourceKey, status }: { data: INiuTreeData, deep: number, dataSourceKey: INiuTreeKey, status: ENiuTreeStatus }"> |
|||
<slot :data="data" :deep="deep" :dataSourceKey="dataSourceKey" :status="status"></slot> |
|||
</template> |
|||
</node> |
|||
</template> |
|||
</div> |
|||
</template> |
|||
<script lang="ts" setup> |
|||
import { INiuTreeData, INiuTreeKey, ENiuTreeStatus } from './type' |
|||
import { |
|||
findByKey, |
|||
forEachTree, |
|||
insertAfterByKey, |
|||
insertBeforeByKey, |
|||
isChildOf, |
|||
removeByKey, |
|||
} from './util' |
|||
|
|||
const props = withDefaults( |
|||
defineProps<{ |
|||
list: INiuTreeData[] |
|||
justOpen?: boolean |
|||
autoExpand?: boolean |
|||
justOpenOne?: boolean |
|||
level?: number |
|||
}>(), |
|||
{ |
|||
justOpenOne: false, |
|||
justOpen: false, |
|||
autoExpand: false, |
|||
level: 0, |
|||
} |
|||
) |
|||
|
|||
provide("tree:opts", props) |
|||
|
|||
function clickNode(item: INiuTreeData) { |
|||
if (props.justOpenOne) { |
|||
forEachTree(props.list, (node: INiuTreeData) => { |
|||
node.isExpand = false |
|||
}) |
|||
} |
|||
if (item.isFolder) { |
|||
item.isExpand = !item.isExpand |
|||
emit("change") |
|||
} |
|||
|
|||
} |
|||
|
|||
const emit = defineEmits<{ |
|||
(e: 'change'): void |
|||
}>() |
|||
|
|||
const dataSourceKey = ref() |
|||
function onDragstart(key: INiuTreeKey) { |
|||
dataSourceKey.value = key |
|||
} |
|||
function onDragEnd(key: INiuTreeKey) { |
|||
dataSourceKey.value = undefined |
|||
} |
|||
function onDrop(key: INiuTreeKey, status?: ENiuTreeStatus) { |
|||
if (!dataSourceKey.value) return |
|||
if (!key) return |
|||
let data = findByKey(dataSourceKey.value, props.list) |
|||
let targetData = findByKey(key, props.list) |
|||
switch (status) { |
|||
case ENiuTreeStatus.DragIn: |
|||
if ( |
|||
data && |
|||
targetData && |
|||
dataSourceKey.value != key && |
|||
!isChildOf(key, dataSourceKey.value, props.list) && |
|||
targetData.children |
|||
) { |
|||
removeByKey(dataSourceKey.value, props.list) |
|||
targetData.children.push(data) |
|||
if (props.autoExpand) { |
|||
targetData.isExpand = true |
|||
} |
|||
emit("change") |
|||
} |
|||
break |
|||
case ENiuTreeStatus.DragDown: |
|||
// 按索引往列表添加节点 |
|||
if ( |
|||
data && |
|||
targetData && |
|||
dataSourceKey.value != key && |
|||
!isChildOf(key, dataSourceKey.value, props.list) |
|||
) { |
|||
removeByKey(dataSourceKey.value, props.list) |
|||
insertAfterByKey(key, data, props.list) |
|||
emit("change") |
|||
} |
|||
break |
|||
case ENiuTreeStatus.DragUp: |
|||
// 按索引往列表添加节点 |
|||
if ( |
|||
data && |
|||
targetData && |
|||
dataSourceKey.value != key && |
|||
!isChildOf(key, dataSourceKey.value, props.list) |
|||
) { |
|||
removeByKey(dataSourceKey.value, props.list) |
|||
insertBeforeByKey(key, data, props.list) |
|||
emit("change") |
|||
} |
|||
break |
|||
} |
|||
dataSourceKey.value = undefined |
|||
} |
|||
</script> |
|||
<script lang="ts"> |
|||
import { defineComponent, provide, ref } from "vue" |
|||
import node from './node.vue' |
|||
|
|||
export default defineComponent({ |
|||
name: 'ps-tree', |
|||
}) |
|||
</script> |
|||
|
@ -0,0 +1,32 @@ |
|||
export interface INiuTreeProps { |
|||
key: INiuTreeKey // 唯一键值
|
|||
title: string // 标题
|
|||
isExpand?: boolean // 标题
|
|||
isEdit?: boolean |
|||
isDel?: boolean // 是否被删除了
|
|||
isNew?: boolean, |
|||
data?: any // 节点数据
|
|||
children?: INiuTreeProps[] // 子节点
|
|||
} |
|||
|
|||
export type INiuTreeKey = string | number |
|||
|
|||
export interface INiuTreeData<T = any> { |
|||
key: INiuTreeKey |
|||
title: string |
|||
isFolder: boolean |
|||
isExpand: boolean |
|||
isFile: boolean |
|||
isNew?: boolean, |
|||
isDel?: boolean, |
|||
data?: T |
|||
isEdit: boolean |
|||
children?: INiuTreeData[] |
|||
} |
|||
|
|||
export enum ENiuTreeStatus { |
|||
DragUp = 'drag-up', |
|||
DragIn = 'drag-in', |
|||
DragDown = 'drag-down', |
|||
DragIng = 'drag-ing', |
|||
} |
@ -0,0 +1,155 @@ |
|||
import { INiuTreeData, INiuTreeKey, INiuTreeProps } from "./type" |
|||
|
|||
export function convertTreeData(items: INiuTreeProps[]) { |
|||
return convertData<INiuTreeProps, INiuTreeData>(items) |
|||
} |
|||
export function convert(item: INiuTreeProps) { |
|||
return convertData<INiuTreeProps, INiuTreeData>([item])[0] |
|||
} |
|||
|
|||
export function convertData< |
|||
T extends { children?: T[] }, |
|||
S extends { children?: S[] } |
|||
>(data: T[] | S[]): S[] { |
|||
const transformData = data.map((item: any) => { |
|||
const children = item.children && convertData(item.children) |
|||
const result = { |
|||
...item, |
|||
isEdit: item?.isEdit?item.isEdit:false, |
|||
isNew: item?.isNew?item.isNew:false, |
|||
isFolder: !!children, |
|||
isExpand: item?.isExpand?item.isExpand:false, // 默认全部收缩
|
|||
isFile: !children, |
|||
children, |
|||
} as S |
|||
return result |
|||
}) |
|||
return transformData |
|||
} |
|||
export function flatTreeData(treeData: INiuTreeData[]): INiuTreeData[] { |
|||
let res: INiuTreeData[] = [] |
|||
treeData.forEach((data) => { |
|||
res.push(data) |
|||
if (data.children) { |
|||
res = res.concat(flatTreeData(data.children)) |
|||
} |
|||
}) |
|||
return res |
|||
} |
|||
export function isChildOf( |
|||
a_key: INiuTreeKey, |
|||
b_key: INiuTreeKey, |
|||
treeData: INiuTreeData[] |
|||
) { |
|||
if (a_key === b_key) return false |
|||
|
|||
let target_node = findByKey(b_key, treeData) |
|||
if (!target_node || !Array.isArray(target_node.children)) return false |
|||
|
|||
return ( |
|||
flatTreeData(target_node.children).findIndex((i) => i.key === a_key) > |
|||
-1 |
|||
) |
|||
} |
|||
|
|||
export function forEachTree(tree: INiuTreeData[], cb: (node: INiuTreeData)=>void) { |
|||
tree.forEach(v=>{ |
|||
cb && cb(v) |
|||
if(v.children && v.children.length){ |
|||
forEachTree(v.children, cb) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
export function findByKey( |
|||
key: INiuTreeKey, |
|||
treeData: INiuTreeData[] |
|||
): INiuTreeData | undefined { |
|||
for (let i = 0; i < treeData.length; i++) { |
|||
const data = treeData[i] |
|||
if (data.key === key) { |
|||
return data |
|||
} |
|||
if (data.children && data.children.length) { |
|||
let result = findByKey(key, data.children) |
|||
if (result) { |
|||
return result |
|||
} |
|||
} |
|||
} |
|||
} |
|||
export function findByKeyParent( |
|||
key: INiuTreeKey, |
|||
treeData: INiuTreeData[] |
|||
): INiuTreeData | undefined { |
|||
for (let i = 0; i < treeData.length; i++) { |
|||
const data = treeData[i] |
|||
if (data.children?.map(v=>v.key).includes(key)) { |
|||
return data |
|||
} |
|||
if (data.children && data.children.length) { |
|||
let result = findByKeyParent(key, data.children) |
|||
if (result) { |
|||
return result |
|||
} |
|||
} |
|||
} |
|||
} |
|||
export function insertBeforeByKey( |
|||
key: INiuTreeKey, |
|||
node: INiuTreeData, |
|||
treeData: INiuTreeData[] |
|||
) { |
|||
if (!treeData || !treeData.length) { |
|||
return |
|||
} |
|||
for (let i = 0; i < treeData.length; i++) { |
|||
let data = treeData[i] |
|||
console.log(key) |
|||
|
|||
if (data.key === key) { |
|||
console.log(node) |
|||
treeData.splice(i, 0, node) |
|||
break |
|||
} |
|||
if (data && data.children) { |
|||
insertBeforeByKey(key, node, data.children) |
|||
} |
|||
} |
|||
} |
|||
export function insertAfterByKey( |
|||
key: INiuTreeKey, |
|||
node: INiuTreeData, |
|||
treeData: INiuTreeData[] |
|||
) { |
|||
if (!treeData || !treeData.length) { |
|||
return |
|||
} |
|||
for (let i = 0; i < treeData.length; i++) { |
|||
let data = treeData[i] |
|||
if (data.key === key) { |
|||
treeData.splice(i + 1, 0, node) |
|||
break |
|||
} |
|||
if (data && data.children) { |
|||
insertAfterByKey(key, node, data.children) |
|||
} |
|||
} |
|||
} |
|||
// https://blog.csdn.net/baidu_36095053/article/details/121649810
|
|||
export function removeByKey(key: INiuTreeKey, treeData: INiuTreeData[], cb?:(node: INiuTreeData)=>void) { |
|||
if (!treeData || !treeData.length) { |
|||
return |
|||
} |
|||
for (let i = 0; i < treeData.length; i++) { |
|||
let data = treeData[i] |
|||
if (data.key === key) { |
|||
cb && cb(data) |
|||
treeData.splice(i, 1) |
|||
break |
|||
} |
|||
if (data && data.children) { |
|||
removeByKey(key, data.children) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,27 @@ |
|||
<script setup lang="ts"> |
|||
function send(start: () => void, done: (isDone: boolean) => void) { |
|||
start() |
|||
setTimeout(() => { |
|||
done(true) |
|||
}, 2500); |
|||
} |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<Panel name="验证码"> |
|||
<div class="bg-white px-10px py-5px rounded-8px flex"> |
|||
<input type="text" class="outline-0 flex-1" placeholder="请输入验证码"> |
|||
<ps-captcha @send="send" :duration="5">sada</ps-captcha> |
|||
</div> |
|||
</Panel> |
|||
<Panel name="带边框的验证码" desc="带了个小边框"> |
|||
<div class="bg-white px-10px py-5px rounded-8px flex"> |
|||
<input type="text" class="outline-0 flex-1" placeholder="请输入验证码"> |
|||
<ps-captcha @send="send" border :duration="5">sada</ps-captcha> |
|||
</div> |
|||
</Panel> |
|||
<Panel name="按钮"> |
|||
<ps-button></ps-button> |
|||
</Panel> |
|||
</template> |
@ -0,0 +1,31 @@ |
|||
<script setup lang="ts"> |
|||
import { ref } from "vue" |
|||
import PsTree, { convertTreeData } from "@princess-ui/components/tree" |
|||
|
|||
const list = ref(convertTreeData([ |
|||
{ |
|||
key: 1, |
|||
title: "1", |
|||
children: [ |
|||
{ |
|||
key: 5, |
|||
title: "5" |
|||
}, |
|||
] |
|||
}, |
|||
{ |
|||
key: 2, |
|||
title: "2" |
|||
}, |
|||
{ |
|||
key: 3, |
|||
title: "3" |
|||
}, |
|||
])) |
|||
</script> |
|||
|
|||
<template> |
|||
<Panel name="Tree"> |
|||
<ps-tree :list="list"></ps-tree> |
|||
</Panel> |
|||
</template> |
@ -1,3 +0,0 @@ |
|||
*{ |
|||
color: gainsboro; |
|||
} |
@ -1,5 +1 @@ |
|||
@use "common/var.scss"; |
|||
|
|||
div{ |
|||
color: darkkhaki; |
|||
} |
|||
@import 'common/var.scss'; |
@ -1 +1 @@ |
|||
|
|||
$primary-color: red;//#32b0f877 |
|||
|
@ -1,2 +1,3 @@ |
|||
@import "./button.scss"; |
|||
@import "./captcha.scss"; |
|||
@import "./tree.scss"; |
|||
|
@ -0,0 +1,72 @@ |
|||
@import 'common/var.scss'; |
|||
|
|||
.ps-tree-node.component { |
|||
position: relative; |
|||
|
|||
&.draging { |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
.ps-tree-node-wrapper { |
|||
position: relative; |
|||
} |
|||
} |
|||
|
|||
.ps-tree-drag-up { |
|||
background-color: $primary-color !important; |
|||
position: absolute; |
|||
top: -1px; |
|||
left: 5px; |
|||
right: 8px; |
|||
height: 2px; |
|||
pointer-events: none; |
|||
z-index: 9; |
|||
|
|||
&::before { |
|||
content: ''; |
|||
width: 6px; |
|||
height: 6px; |
|||
background-color: $primary-color; |
|||
border-radius: 50%; |
|||
position: absolute; |
|||
left: 0; |
|||
top: 50%; |
|||
margin-top: -3px; |
|||
margin-left: -3px; |
|||
} |
|||
} |
|||
|
|||
.ps-tree-drag-down { |
|||
background-color: $primary-color !important; |
|||
position: absolute; |
|||
bottom: -1px; |
|||
left: 5px; |
|||
right: 8px; |
|||
height: 2px; |
|||
pointer-events: none; |
|||
z-index: 9; |
|||
|
|||
&::before { |
|||
content: ''; |
|||
width: 6px; |
|||
height: 6px; |
|||
background-color: $primary-color; |
|||
border-radius: 50%; |
|||
position: absolute; |
|||
left: 0; |
|||
top: 50%; |
|||
margin-top: -3px; |
|||
margin-left: -3px; |
|||
} |
|||
} |
|||
|
|||
.ps-tree-drag-in { |
|||
background-color: $primary-color !important; |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
top: 0; |
|||
pointer-events: none; |
|||
z-index: 9; |
|||
} |
Loading…
Reference in new issue