From 6f2565a9d27c7414f307662901802b33d5d9eed4 Mon Sep 17 00:00:00 2001
From: 1549469775 <1549469775@qq.com>
Date: Mon, 20 Jun 2022 16:07:19 +0800
Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E4=BA=86=E4=B8=80=E5=A0=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 docs/.vitepress/theme/index.js           |   5 +-
 docs/demo-example.vue                    |  24 ++++-
 packages/build/buildComponent.ts         |   5 +-
 packages/build/util.ts                   |   8 +-
 packages/components/button/button.vue    |  22 +++++
 packages/components/button/index.ts      |   7 +-
 packages/components/button/index.vue     |  25 -----
 packages/components/captcha/captcha.vue  |  96 +++++++++++++++++++
 packages/components/captcha/index.ts     |   7 +-
 packages/components/captcha/index.vue    |  96 -------------------
 packages/components/components.ts        |  10 +-
 packages/components/gulpfile.ts          |   0
 packages/components/tree/index.ts        |  14 +++
 packages/components/tree/node.vue        | 154 ++++++++++++++++++++++++++++++
 packages/components/tree/tree-drag.vue   |  34 +++++++
 packages/components/tree/tree.vue        | 127 +++++++++++++++++++++++++
 packages/components/tree/type.ts         |  32 +++++++
 packages/components/tree/util.ts         | 155 +++++++++++++++++++++++++++++++
 packages/playground/src/App.vue          |  34 +++----
 packages/playground/src/dev/captcha.vue  |  27 ++++++
 packages/playground/src/dev/tree.vue     |  31 +++++++
 packages/theme-chalk/src/base.scss       |   3 -
 packages/theme-chalk/src/button.scss     |   6 +-
 packages/theme-chalk/src/captcha.scss    |   2 +-
 packages/theme-chalk/src/common/var.scss |   2 +-
 packages/theme-chalk/src/index.scss      |   1 +
 packages/theme-chalk/src/tree.scss       |  72 ++++++++++++++
 27 files changed, 829 insertions(+), 170 deletions(-)
 create mode 100644 packages/components/button/button.vue
 delete mode 100644 packages/components/button/index.vue
 create mode 100644 packages/components/captcha/captcha.vue
 delete mode 100644 packages/components/captcha/index.vue
 delete mode 100644 packages/components/gulpfile.ts
 create mode 100644 packages/components/tree/index.ts
 create mode 100644 packages/components/tree/node.vue
 create mode 100644 packages/components/tree/tree-drag.vue
 create mode 100644 packages/components/tree/tree.vue
 create mode 100644 packages/components/tree/type.ts
 create mode 100644 packages/components/tree/util.ts
 create mode 100644 packages/playground/src/dev/captcha.vue
 create mode 100644 packages/playground/src/dev/tree.vue
 create mode 100644 packages/theme-chalk/src/tree.scss

diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js
index d16446d..82aa986 100644
--- a/docs/.vitepress/theme/index.js
+++ b/docs/.vitepress/theme/index.js
@@ -1,5 +1,8 @@
 import DefaultTheme from 'vitepress/dist/client/theme-default'
-import PrincessUI from "@princess-ui/components"
+
+import PrincessUI from "princess-ui"
+import "princess-ui/theme-chalk/index.css"
+
 export default {
   ...DefaultTheme,
 
diff --git a/docs/demo-example.vue b/docs/demo-example.vue
index 5a7c228..0160cc9 100644
--- a/docs/demo-example.vue
+++ b/docs/demo-example.vue
@@ -12,18 +12,40 @@
       <span>{{ count }}</span>
     </p>
     <button @click="onClick">count++</button>
+    <ps-tree :list="list"></ps-tree>
   </div>
 </template>
 
 <script setup>
 import { ref } from 'vue'
 // import VTypical from 'vue-typical'
-
+import { convertTreeData } from "princess-ui/lib/tree"
 
 const count = ref(0)
 function onClick() {
   count.value++
 }
+
+const list = ref(convertTreeData([
+    {
+        key: 1,
+        title: "1",
+        children: [
+            {
+                key: 5,
+                title: "5"
+            },
+        ]
+    },
+    {
+        key: 2,
+        title: "2"
+    },
+    {
+        key: 3,
+        title: "3"
+    },
+]))
 </script>
 
 <style>
diff --git a/packages/build/buildComponent.ts b/packages/build/buildComponent.ts
index 7eeb985..5d9a4c0 100644
--- a/packages/build/buildComponent.ts
+++ b/packages/build/buildComponent.ts
@@ -3,11 +3,13 @@ import vue from "@vitejs/plugin-vue"
 import path from "path"
 import dts from "vite-plugin-dts"
 import { replaceCodePlugin } from "vite-plugin-replace";
+import fs from "fs-extra";
 // import libInjectCss from "./libInjectCss"
 import _ from "lodash"
 import { getOutput, getPath, getPkgs } from "@princess-ui/share";
 
 export default function (prefix: string, component: string, name: string, opts?: {}) {
+    const isExistVue = fs.pathExistsSync(`components/${component}/index.vue`)
     return build({
         logLevel: "error",
         plugins: [
@@ -17,7 +19,8 @@ export default function (prefix: string, component: string, name: string, opts?:
                 tsConfigFilePath: getPath("tsconfig.json"),
                 outputDir: getOutput(`lib/${component}`),
                 cleanVueFileName: true,
-                include: [`components/${component}/index.vue`],
+                // include: [isExistVue?`components/${component}/index.vue`:`components/${component}/${component}.vue`, `components/${component}/index.ts`],
+                include: [`components/${component}`],
                 staticImport: true,
             }),
             replaceCodePlugin({
diff --git a/packages/build/util.ts b/packages/build/util.ts
index 0ceefda..e92d777 100644
--- a/packages/build/util.ts
+++ b/packages/build/util.ts
@@ -14,7 +14,7 @@ export async function getComponents() {
 }
 
 export async function generateComponents(components?: string[]) {
-    const componetnsStr: string[] = [`// 该文件为自动生成,请勿修改!!!`]
+    // const componetnsStr: string[] = [`// 该文件为自动生成,请勿修改!!!`]
     const newComp: string[] = []
     const typeArray: string[] = []
     let typeStr = `declare module 'vue' {
@@ -28,12 +28,12 @@ export { }`
     }
     components.forEach(name => {
         const n = "Ps" + _.upperFirst(_.kebabCase(name))
-        componetnsStr.push(`import ${n} from "./${name}"`)
+        // componetnsStr.push(`import ${n} from "./${name}"`)
         newComp.push(n)
         typeArray.push(n + `: typeof import('princess-ui')['${n}']`)
     })
-    componetnsStr.push(`export { \n  ${newComp.join(",\n  ")} \n}`)
+    // componetnsStr.push(`export { \n  ${newComp.join(",\n  ")} \n}`)
     typeStr = typeStr.replace("__placeholder__", typeArray.join(',\n        '))
     await fs.writeFile(getOutput("components.d.ts"), typeStr)
-    await fs.writeFile(getPkgs("components/components.ts"), componetnsStr.join("\n"))
+    // await fs.writeFile(getPkgs("components/components.ts"), componetnsStr.join("\n"))
 }
diff --git a/packages/components/button/button.vue b/packages/components/button/button.vue
new file mode 100644
index 0000000..4adbc9e
--- /dev/null
+++ b/packages/components/button/button.vue
@@ -0,0 +1,22 @@
+<template>
+    <div>ffffffffffff-{{ test }}</div>
+</template>
+
+<script lang="ts">
+export default defineComponent({
+    name: "ps-button"
+})
+</script>
+
+<script lang="ts" setup>
+import { defineComponent, onBeforeUnmount, ref } from "vue"
+const props = defineProps<{
+    test: string
+}>()
+console.log(props);
+
+const emits = defineEmits<{
+
+}>()
+
+</script>
diff --git a/packages/components/button/index.ts b/packages/components/button/index.ts
index 7282283..fe6221e 100644
--- a/packages/components/button/index.ts
+++ b/packages/components/button/index.ts
@@ -1,4 +1,9 @@
-import PsButton from "./index.vue"
+import { App } from "vue"
+import PsButton from "./button.vue"
+
+PsButton.install = function(app: App) {
+    app.component(PsButton.name || "ps-button", PsButton)
+}
 
 export {
     PsButton
diff --git a/packages/components/button/index.vue b/packages/components/button/index.vue
deleted file mode 100644
index 52ce227..0000000
--- a/packages/components/button/index.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-<template>
-    <div>ffffffffffff</div>
-</template>
-
-<script lang="ts">
-export default defineComponent({
-    name: "ps-button"
-})
-</script>
-
-<script lang="ts" setup>
-import { defineComponent, onBeforeUnmount, ref } from "vue"
-// const props = withDefaults(
-//     defineProps<{
-
-//     }>(),
-//     {
-
-//     },
-// )
-// const emits = defineEmits<{
-
-// }>()
-
-</script>
diff --git a/packages/components/captcha/captcha.vue b/packages/components/captcha/captcha.vue
new file mode 100644
index 0000000..67949bf
--- /dev/null
+++ b/packages/components/captcha/captcha.vue
@@ -0,0 +1,96 @@
+<template>
+    <div class="ps-send" :class="{
+        ['ps-send--'+size]: size? true: false,
+        ['ps-send--border']: border
+    }" @click="onClick" :disabled="isDisabled"
+        :loading="isLoading" type="button" size="small">
+        {{ text }}
+    </div>
+</template>
+
+<script lang="ts">
+export default defineComponent({
+    name: "ps-captcha"
+})
+</script>
+
+<script lang="ts" setup>
+import { defineComponent, onBeforeUnmount, ref } from "vue"
+const props = withDefaults(
+    defineProps<{
+        duration?: number
+        initText?: string
+        runText?: string
+        loadingText?: string
+        resetText?: string
+        border?: boolean
+        size?: "small" | "big"
+    }>(),
+    {
+        runText: "{%s}s 后重新发送",
+        initText: "获取验证码",
+        loadingText: "正在发送",
+        resetText: "重新获取",
+        border: false,
+        duration: 60,
+    },
+)
+const emits = defineEmits<{
+    (event: "update:modelValue", show: boolean): void
+    (event: "send", start: () => void, done: (isDone?: boolean) => void): void
+}>()
+
+const text = ref(props.initText)
+const isDisabled = ref(false)
+const isLoading = ref(false)
+let timeID: any
+
+onBeforeUnmount(() => {
+    stop()
+})
+
+function stop() {
+    clearInterval(timeID)
+    text.value = props.resetText
+    isLoading.value = false
+    isDisabled.value = false
+    emits("update:modelValue", false)
+}
+//获取格式化
+function getText(second: string | number): string {
+    return props.runText.replace(/\{([^{]*?)%s(.*?)\}/g, String(second))
+}
+function run() {
+    isLoading.value = false
+    let number = props.duration
+    text.value = getText(number)
+    clearInterval(timeID)
+    timeID = setInterval(() => {
+        number--
+        text.value = getText(number)
+        if (number <= 0) {
+            stop()
+        }
+    }, 1000)
+}
+
+function onClick() {
+    if (isDisabled.value) return
+    if (isLoading.value) return
+    emits(
+        "send",
+        () => {
+            isDisabled.value = true
+            isLoading.value = true
+            text.value = props.loadingText
+        },
+        (isDone: boolean = true) => {
+            if (isDone) {
+                run()
+            } else {
+                stop()
+            }
+        },
+    )
+}
+</script>
diff --git a/packages/components/captcha/index.ts b/packages/components/captcha/index.ts
index e1759b7..e709ec5 100644
--- a/packages/components/captcha/index.ts
+++ b/packages/components/captcha/index.ts
@@ -1,4 +1,9 @@
-import PsCaptcha from "./index.vue"
+import { App } from "vue"
+import PsCaptcha from "./captcha.vue"
+
+PsCaptcha.install = function(app: App) {
+    app.component(PsCaptcha.name || "ps-captcha", PsCaptcha)
+}
 
 export {
     PsCaptcha
diff --git a/packages/components/captcha/index.vue b/packages/components/captcha/index.vue
deleted file mode 100644
index 67949bf..0000000
--- a/packages/components/captcha/index.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-<template>
-    <div class="ps-send" :class="{
-        ['ps-send--'+size]: size? true: false,
-        ['ps-send--border']: border
-    }" @click="onClick" :disabled="isDisabled"
-        :loading="isLoading" type="button" size="small">
-        {{ text }}
-    </div>
-</template>
-
-<script lang="ts">
-export default defineComponent({
-    name: "ps-captcha"
-})
-</script>
-
-<script lang="ts" setup>
-import { defineComponent, onBeforeUnmount, ref } from "vue"
-const props = withDefaults(
-    defineProps<{
-        duration?: number
-        initText?: string
-        runText?: string
-        loadingText?: string
-        resetText?: string
-        border?: boolean
-        size?: "small" | "big"
-    }>(),
-    {
-        runText: "{%s}s 后重新发送",
-        initText: "获取验证码",
-        loadingText: "正在发送",
-        resetText: "重新获取",
-        border: false,
-        duration: 60,
-    },
-)
-const emits = defineEmits<{
-    (event: "update:modelValue", show: boolean): void
-    (event: "send", start: () => void, done: (isDone?: boolean) => void): void
-}>()
-
-const text = ref(props.initText)
-const isDisabled = ref(false)
-const isLoading = ref(false)
-let timeID: any
-
-onBeforeUnmount(() => {
-    stop()
-})
-
-function stop() {
-    clearInterval(timeID)
-    text.value = props.resetText
-    isLoading.value = false
-    isDisabled.value = false
-    emits("update:modelValue", false)
-}
-//获取格式化
-function getText(second: string | number): string {
-    return props.runText.replace(/\{([^{]*?)%s(.*?)\}/g, String(second))
-}
-function run() {
-    isLoading.value = false
-    let number = props.duration
-    text.value = getText(number)
-    clearInterval(timeID)
-    timeID = setInterval(() => {
-        number--
-        text.value = getText(number)
-        if (number <= 0) {
-            stop()
-        }
-    }, 1000)
-}
-
-function onClick() {
-    if (isDisabled.value) return
-    if (isLoading.value) return
-    emits(
-        "send",
-        () => {
-            isDisabled.value = true
-            isLoading.value = true
-            text.value = props.loadingText
-        },
-        (isDone: boolean = true) => {
-            if (isDone) {
-                run()
-            } else {
-                stop()
-            }
-        },
-    )
-}
-</script>
diff --git a/packages/components/components.ts b/packages/components/components.ts
index 7619fd2..4594a74 100644
--- a/packages/components/components.ts
+++ b/packages/components/components.ts
@@ -1,7 +1,3 @@
-// 该文件为自动生成,请勿修改!!!
-import PsButton from "./button"
-import PsCaptcha from "./captcha"
-export { 
-  PsButton,
-  PsCaptcha 
-}
\ No newline at end of file
+export * from "./button"
+export * from "./captcha"
+export * from "./tree"
\ No newline at end of file
diff --git a/packages/components/gulpfile.ts b/packages/components/gulpfile.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/packages/components/tree/index.ts b/packages/components/tree/index.ts
new file mode 100644
index 0000000..8e4d091
--- /dev/null
+++ b/packages/components/tree/index.ts
@@ -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
\ No newline at end of file
diff --git a/packages/components/tree/node.vue b/packages/components/tree/node.vue
new file mode 100644
index 0000000..d8c3bee
--- /dev/null
+++ b/packages/components/tree/node.vue
@@ -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>
\ No newline at end of file
diff --git a/packages/components/tree/tree-drag.vue b/packages/components/tree/tree-drag.vue
new file mode 100644
index 0000000..637cf8c
--- /dev/null
+++ b/packages/components/tree/tree-drag.vue
@@ -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>
diff --git a/packages/components/tree/tree.vue b/packages/components/tree/tree.vue
new file mode 100644
index 0000000..4d920fc
--- /dev/null
+++ b/packages/components/tree/tree.vue
@@ -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>
+
diff --git a/packages/components/tree/type.ts b/packages/components/tree/type.ts
new file mode 100644
index 0000000..2a15d95
--- /dev/null
+++ b/packages/components/tree/type.ts
@@ -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',
+}
\ No newline at end of file
diff --git a/packages/components/tree/util.ts b/packages/components/tree/util.ts
new file mode 100644
index 0000000..75dea1f
--- /dev/null
+++ b/packages/components/tree/util.ts
@@ -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)
+        }
+    }
+}
diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue
index cb52b26..6ecd6a1 100644
--- a/packages/playground/src/App.vue
+++ b/packages/playground/src/App.vue
@@ -1,28 +1,17 @@
 <script setup lang="ts">
-function send(start: () => void, done: (isDone: boolean) => void) {
-    start()
-    setTimeout(() => {
-        done(true)
-    }, 2500);
-}
+import DevCaptcha from "./dev/captcha.vue"
+import DevTree from "./dev/tree.vue"
+// import a, * as b from "@princess-ui/components"
+// import c, * as d from "princess-ui"
+// console.log(a);
+// console.log(b);
+// console.log(c);
+// console.log(d);
 </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>
+    <DevCaptcha></DevCaptcha>
+    <DevTree></DevTree>
 </template>
 
 <style>
@@ -39,11 +28,10 @@ body {
     align-items: center;
     -webkit-font-smoothing: antialiased;
     -moz-osx-font-smoothing: grayscale;
-    text-align: center;
     height: 100vh;
 }
 
-.card{
+.card {
     margin-bottom: 25px;
 }
 </style>
diff --git a/packages/playground/src/dev/captcha.vue b/packages/playground/src/dev/captcha.vue
new file mode 100644
index 0000000..c97e3dc
--- /dev/null
+++ b/packages/playground/src/dev/captcha.vue
@@ -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>
diff --git a/packages/playground/src/dev/tree.vue b/packages/playground/src/dev/tree.vue
new file mode 100644
index 0000000..e62cac0
--- /dev/null
+++ b/packages/playground/src/dev/tree.vue
@@ -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>
diff --git a/packages/theme-chalk/src/base.scss b/packages/theme-chalk/src/base.scss
index 29745e9..e69de29 100644
--- a/packages/theme-chalk/src/base.scss
+++ b/packages/theme-chalk/src/base.scss
@@ -1,3 +0,0 @@
-*{
-    color: gainsboro;
-}
\ No newline at end of file
diff --git a/packages/theme-chalk/src/button.scss b/packages/theme-chalk/src/button.scss
index 981e602..b634ca7 100644
--- a/packages/theme-chalk/src/button.scss
+++ b/packages/theme-chalk/src/button.scss
@@ -1,5 +1 @@
-@use "common/var.scss";
-
-div{
-    color: darkkhaki;
-}
\ No newline at end of file
+@import 'common/var.scss';
\ No newline at end of file
diff --git a/packages/theme-chalk/src/captcha.scss b/packages/theme-chalk/src/captcha.scss
index 1fa1d67..89c9216 100644
--- a/packages/theme-chalk/src/captcha.scss
+++ b/packages/theme-chalk/src/captcha.scss
@@ -1,4 +1,4 @@
-@use 'common/var.scss';
+@import 'common/var.scss';
 
 .ps-send {
     user-select: none;
diff --git a/packages/theme-chalk/src/common/var.scss b/packages/theme-chalk/src/common/var.scss
index 8b13789..7b33766 100644
--- a/packages/theme-chalk/src/common/var.scss
+++ b/packages/theme-chalk/src/common/var.scss
@@ -1 +1 @@
-
+$primary-color: red;//#32b0f877
diff --git a/packages/theme-chalk/src/index.scss b/packages/theme-chalk/src/index.scss
index 7e4c069..6718ebb 100644
--- a/packages/theme-chalk/src/index.scss
+++ b/packages/theme-chalk/src/index.scss
@@ -1,2 +1,3 @@
 @import "./button.scss";
 @import "./captcha.scss";
+@import "./tree.scss";
diff --git a/packages/theme-chalk/src/tree.scss b/packages/theme-chalk/src/tree.scss
new file mode 100644
index 0000000..0677b45
--- /dev/null
+++ b/packages/theme-chalk/src/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;
+}