treeList.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <template>
  2. <div class="vtl-node" :id="model.id" :class="{ 'vtl-leaf-node': !isFolder, 'vtl-tree-node': isFolder }">
  3. <div
  4. :class="treeNodeClass"
  5. :draggable="draggable"
  6. @dragover="dragOver"
  7. @drop="drop"
  8. @dragstart="dragStart"
  9. @mouseover="mouseOver"
  10. @dragenter="dragEnter"
  11. @dragleave="dragLeave"
  12. @mouseout="mouseOut"
  13. @click.stop="toggle"
  14. >
  15. <div class="vtl-border-text">
  16. <template v-if="isFolder">
  17. <slot v-if="expanded" :item="{ title: model.title, isFolder: true, expanded: true }" name="icon"> </slot>
  18. <slot v-else :item="{ title: model.title, isFolder: true, expanded: false }" name="icon"></slot>
  19. </template>
  20. <slot v-else :item="{ title: model.title, isFolder: false }" name="icon"></slot>
  21. <span class="vtl-node-content ellipsis" v-if="!editable && !model.isAdd">
  22. {{ model.title }}
  23. </span>
  24. <input v-else class="vtl-input" type="text" ref="nodeInput" v-model="model.title" @blur="setUnEditable" />
  25. </div>
  26. <div class="vtl-operation" v-show="isHover && !editable && !model.isAdd">
  27. <!-- <span @click.stop.prevent="addChildFolder" v-if="isFolder">
  28. <slot name="operation" type="addFolder"></slot>
  29. </span> -->
  30. <span @click.stop.prevent="addChildDocument" v-if="isFolder">
  31. <slot name="operation" type="addDocument"></slot>
  32. </span>
  33. <span @click.stop.prevent="setEditable">
  34. <slot name="operation" type="Editable"></slot>
  35. </span>
  36. <span @click.stop.prevent="delNode">
  37. <slot name="operation" type="deleteNode"></slot>
  38. </span>
  39. </div>
  40. </div>
  41. </div>
  42. <div class="vtl-tree-margin" v-show="expanded" v-if="isFolder">
  43. <!-- 这里无法使用$attr来透传属性官方还未解决此bug -->
  44. <treeList
  45. @on-click="(depth) => $emit('onClick', depth)"
  46. @change-name="(depth) => $emit('changeName', depth)"
  47. @delete-node="(depth) => $emit('deleteNode', depth)"
  48. @add-node="(depth) => $emit('addNode', depth)"
  49. @on-drop="(depth) => $emit('onDrop', depth)"
  50. @add-folder="(depth) => $emit('addFolder', depth)"
  51. @dragStart="(depth) => $emit('dragStart', depth)"
  52. @setDragEnterNode="setDragEnterNode"
  53. @setDragFile="setDragFile"
  54. @setDragFolder="setDragFolder"
  55. v-for="newmodel in model.children"
  56. :selected="selected"
  57. :model="newmodel"
  58. :key="newmodel.id"
  59. >
  60. <template #icon="slotProps">
  61. <slot name="icon" v-bind="slotProps"></slot>
  62. </template>
  63. <template #operation="slotProps">
  64. <slot name="operation" v-bind="slotProps"></slot>
  65. </template>
  66. </treeList>
  67. </div>
  68. </template>
  69. <script setup lang="ts">
  70. import { computed, ref, watchEffect } from 'vue';
  71. interface IFileSystem {
  72. id: string;
  73. title: string;
  74. pid: string;
  75. isFolder: boolean;
  76. isAdd: boolean;
  77. children?: IFileSystem[];
  78. }
  79. // 吐出去的事件
  80. const emit = defineEmits([
  81. 'onClick',
  82. 'changeName',
  83. 'deleteNode',
  84. 'addNode',
  85. 'addFolder',
  86. 'onDrop',
  87. 'setDragEnterNode',
  88. 'setDragFile',
  89. 'setDragFolder',
  90. 'dragStart',
  91. ]);
  92. // 拿到传入的值
  93. const props = withDefaults(
  94. defineProps<{
  95. model: IFileSystem;
  96. draggable?: boolean;
  97. selected?: IFileSystem;
  98. }>(),
  99. {
  100. draggable: true,
  101. }
  102. );
  103. //是否移入
  104. const isHover = ref(false);
  105. // 修改目录名字
  106. const editable = ref(false);
  107. // 拖拽移入
  108. const isDragEnterNode = ref(false);
  109. // 是否拖拽文件
  110. const isDragFile = ref(false);
  111. // 是否展开
  112. const expanded = ref(true);
  113. // inputRef
  114. const nodeInput = ref(null);
  115. // 是否是文件夹
  116. const isFolder = computed(() => {
  117. return props.model.isFolder;
  118. });
  119. const isSelected = computed(() => props.selected.id === props.model.id);
  120. // 拖拽样式
  121. const treeNodeClass = computed(() => {
  122. return {
  123. 'vtl-node-main': true,
  124. 'vtl-active': isDragEnterNode.value,
  125. 'vtl-active-file': isDragFile.value,
  126. selected: isSelected.value,
  127. };
  128. });
  129. // 最后一个移入的内容保存为了防止重复移入
  130. let lastenter = null;
  131. // 删除目录
  132. const delNode = () => {
  133. emit('deleteNode', {
  134. ...props.model,
  135. eventType: 'delete',
  136. });
  137. };
  138. // 选中effect
  139. watchEffect(() => {
  140. const $input = nodeInput.value;
  141. if ($input) {
  142. // 获取焦点
  143. $input.focus();
  144. // 设置光标位置
  145. $input.setSelectionRange(0, $input.value.length);
  146. }
  147. });
  148. // 编辑目录名字
  149. const setEditable = () => {
  150. editable.value = true;
  151. props.model.isAdd = false; //lxh
  152. };
  153. // 修改目录名字
  154. const setUnEditable = (e) => {
  155. if (props.model.isAdd) {
  156. console.log('新增文档失去焦点');
  157. props.model.isAdd = false;
  158. emit('addNode', {
  159. id: props.model.id,
  160. isFolder: false,
  161. newName: props.model.title,
  162. });
  163. } else if (editable.value) {
  164. console.log('编辑文档失去焦点');
  165. editable.value = false;
  166. props.model.title = e.target.value;
  167. emit('changeName', {
  168. id: props.model.id,
  169. pid: props.model.pid,
  170. isAdd: props.model.isAdd,
  171. newName: e.target.value,
  172. eventType: 'blur',
  173. isFolder: isFolder.value,
  174. });
  175. }
  176. };
  177. // 展开收起
  178. const toggle = () => {
  179. if (isFolder.value) {
  180. expanded.value = !expanded.value;
  181. emit('onClick', {
  182. ...props.model,
  183. }); //lxh
  184. } else {
  185. emit('onClick', {
  186. ...props.model,
  187. });
  188. }
  189. };
  190. // 拖拽结束
  191. const mouseOver = () => {
  192. isHover.value = true;
  193. };
  194. // 移出
  195. const mouseOut = () => {
  196. isHover.value = false;
  197. };
  198. // // 添加目录
  199. // const addChildFolder = () => {
  200. // props.model.isAdd = true; //lxh
  201. // props.model.title = '';//lxh
  202. // emit('addFolder', {
  203. // id: props.model.id,
  204. // isFolder: true,
  205. // });
  206. // };
  207. // 添加文件
  208. const addChildDocument = (node) => {
  209. props.model.title = ''; //lxh
  210. props.model.isAdd = true; //lxh
  211. editable.value = false; //
  212. };
  213. // 拖拽开始
  214. const dragStart = () => {
  215. console.log(0);
  216. emit('dragStart', {
  217. ...props.model,
  218. });
  219. };
  220. const dragOver = (e) => {
  221. e.preventDefault();
  222. return true;
  223. };
  224. const dragEnter = (e) => {
  225. lastenter = e.target;
  226. console.log('进入', props.model.id);
  227. // 由于 dragEnter 发生在 dragLeave 之前,导致必须要使用定时器做一个延时
  228. setTimeout(() => {
  229. if (isFolder.value) {
  230. expanded.value = true;
  231. isDragFile.value = true;
  232. } else {
  233. emit('setDragFile', true);
  234. }
  235. isDragEnterNode.value = true;
  236. emit('setDragEnterNode', true);
  237. });
  238. };
  239. const dragLeave = (e) => {
  240. // 为了防止多次选中问题
  241. if (lastenter == e.target) {
  242. console.log('离开', props.model.id);
  243. if (isFolder.value) {
  244. isDragFile.value = false;
  245. } else {
  246. emit('setDragFile', false);
  247. }
  248. emit('setDragEnterNode', false);
  249. isDragEnterNode.value = false;
  250. }
  251. };
  252. const drop = (e) => {
  253. isDragFile.value = false;
  254. isDragEnterNode.value = false;
  255. emit('setDragEnterNode', false);
  256. emit('setDragFile', false);
  257. // 为了获取路径需要判断是不是文件夹,如果不是文件夹向上找
  258. if (isFolder.value) {
  259. emit('onDrop', props.model);
  260. } else {
  261. if (props.model.pid) {
  262. emit('setDragFolder');
  263. } else {
  264. emit('onDrop', props.model);
  265. }
  266. }
  267. };
  268. const setDragEnterNode = (bol) => {
  269. isDragEnterNode.value = bol;
  270. };
  271. const setDragFile = (bol) => {
  272. isDragFile.value = bol;
  273. };
  274. // 找到文件夹
  275. const setDragFolder = () => {
  276. emit('onDrop', props.model);
  277. };
  278. </script>
  279. <style lang="less">
  280. .vtl-node {
  281. .vtl-node-main {
  282. display: flex;
  283. align-items: center;
  284. padding: 2px 0 2px 1rem;
  285. cursor: pointer;
  286. &:hover {
  287. .vtl-border-text {
  288. width: 80%;
  289. }
  290. }
  291. .vtl-border-text {
  292. display: flex; //lxh
  293. flex: 1;
  294. align-items: center; //lxh
  295. width: 100%;
  296. .iconfont {
  297. width: 16px;
  298. height: 16px;
  299. vertical-align: text-bottom;
  300. }
  301. }
  302. &.selected {
  303. background-color: rgba(53, 147, 255, 0.2);
  304. }
  305. .vtl-input {
  306. border: none;
  307. max-width: 150px;
  308. padding: 5px 0;
  309. padding-left: 5px;
  310. margin-left: 5px;
  311. &:focus {
  312. outline: none;
  313. }
  314. }
  315. .vtl-node-content {
  316. color: #fff;
  317. padding-left: 5px;
  318. font-size: 14px;
  319. width: 80%;
  320. display: inline-block;
  321. vertical-align: bottom;
  322. }
  323. &:hover {
  324. .vtl-node-content {
  325. color: #fff;
  326. overflow: hidden;
  327. }
  328. }
  329. &.vtl-active {
  330. * {
  331. pointer-events: none;
  332. }
  333. }
  334. &.vtl-active-file {
  335. outline: 2px dashed #353f51;
  336. }
  337. .vtl-operation {
  338. padding-right: 10px;
  339. }
  340. }
  341. }
  342. .vtl-tree-margin {
  343. padding-left: 1em;
  344. }
  345. </style>