treeList.vue 9.7 KB

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