treeList.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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. const expanded = ref(!props.model.pid || props.model.pid === 'root'); // 默认只展开一级菜单
  119. // inputRef
  120. const nodeInput = ref(null);
  121. // 是否是文件夹
  122. const isFolder = computed(() => {
  123. return props.model.isFolder;
  124. });
  125. const isSelected = computed(() => props.selected.id === props.model.id);
  126. // 拖拽样式
  127. const treeNodeClass = computed(() => {
  128. return {
  129. 'vtl-node-main': true,
  130. 'vtl-active': isDragEnterNode.value,
  131. 'vtl-active-file': isDragFile.value,
  132. selected: isSelected.value,
  133. };
  134. });
  135. // 最后一个移入的内容保存为了防止重复移入
  136. let lastenter = null;
  137. // 删除目录
  138. const delNode = () => {
  139. emit('deleteNode', {
  140. ...props.model,
  141. eventType: 'delete',
  142. });
  143. };
  144. //下载目录
  145. const downNode = () => {
  146. emit('downloadNode', {
  147. ...props.model,
  148. eventType: 'download',
  149. });
  150. };
  151. // 选中effect
  152. watchEffect(() => {
  153. const $input = nodeInput.value;
  154. if ($input) {
  155. // 获取焦点
  156. $input.focus();
  157. // 设置光标位置
  158. $input.setSelectionRange(0, $input.value.length);
  159. }
  160. });
  161. // 编辑目录名字
  162. const setEditable = () => {
  163. editable.value = true;
  164. props.model.isAdd = false; //lxh
  165. };
  166. // 修改目录名字
  167. const setUnEditable = (e) => {
  168. if (props.model.isAdd) {
  169. console.log('新增文档失去焦点');
  170. props.model.isAdd = false;
  171. emit('addNode', {
  172. id: props.model.id,
  173. isFolder: false,
  174. newName: props.model.title,
  175. });
  176. } else if (editable.value) {
  177. console.log('编辑文档失去焦点');
  178. editable.value = false;
  179. props.model.title = e.target.value;
  180. emit('changeName', {
  181. id: props.model.id,
  182. pid: props.model.pid,
  183. isAdd: props.model.isAdd,
  184. newName: e.target.value,
  185. eventType: 'blur',
  186. isFolder: isFolder.value,
  187. });
  188. }
  189. };
  190. // 展开收起
  191. const toggle = () => {
  192. if (isFolder.value) {
  193. expanded.value = !expanded.value;
  194. emit('onClick', {
  195. ...props.model,
  196. }); //lxh
  197. } else {
  198. emit('onClick', {
  199. ...props.model,
  200. });
  201. }
  202. };
  203. // 拖拽结束
  204. const mouseOver = () => {
  205. isHover.value = true;
  206. };
  207. // 移出
  208. const mouseOut = () => {
  209. isHover.value = false;
  210. };
  211. // // 添加目录
  212. // const addChildFolder = () => {
  213. // props.model.isAdd = true; //lxh
  214. // props.model.title = '';//lxh
  215. // emit('addFolder', {
  216. // id: props.model.id,
  217. // isFolder: true,
  218. // });
  219. // };
  220. // 添加文件
  221. const addChildDocument = (node) => {
  222. props.model.title = ''; //lxh
  223. props.model.isAdd = true; //lxh
  224. editable.value = false; //
  225. };
  226. // 拖拽开始
  227. const dragStart = () => {
  228. console.log(0);
  229. emit('dragStart', {
  230. ...props.model,
  231. });
  232. };
  233. const dragOver = (e) => {
  234. e.preventDefault();
  235. return true;
  236. };
  237. const dragEnter = (e) => {
  238. lastenter = e.target;
  239. console.log('进入', props.model.id);
  240. // 由于 dragEnter 发生在 dragLeave 之前,导致必须要使用定时器做一个延时
  241. setTimeout(() => {
  242. if (isFolder.value) {
  243. expanded.value = true;
  244. isDragFile.value = true;
  245. } else {
  246. emit('setDragFile', true);
  247. }
  248. isDragEnterNode.value = true;
  249. emit('setDragEnterNode', true);
  250. });
  251. };
  252. const dragLeave = (e) => {
  253. // 为了防止多次选中问题
  254. if (lastenter == e.target) {
  255. console.log('离开', props.model.id);
  256. if (isFolder.value) {
  257. isDragFile.value = false;
  258. } else {
  259. emit('setDragFile', false);
  260. }
  261. emit('setDragEnterNode', false);
  262. isDragEnterNode.value = false;
  263. }
  264. };
  265. const drop = (e) => {
  266. isDragFile.value = false;
  267. isDragEnterNode.value = false;
  268. emit('setDragEnterNode', false);
  269. emit('setDragFile', false);
  270. // 为了获取路径需要判断是不是文件夹,如果不是文件夹向上找
  271. if (isFolder.value) {
  272. emit('onDrop', props.model);
  273. } else {
  274. if (props.model.pid) {
  275. emit('setDragFolder');
  276. } else {
  277. emit('onDrop', props.model);
  278. }
  279. }
  280. };
  281. const setDragEnterNode = (bol) => {
  282. isDragEnterNode.value = bol;
  283. };
  284. const setDragFile = (bol) => {
  285. isDragFile.value = bol;
  286. };
  287. // 找到文件夹
  288. const setDragFolder = () => {
  289. emit('onDrop', props.model);
  290. };
  291. </script>
  292. <style lang="less">
  293. @import '/@/design/theme.less';
  294. @{theme-deepblue} {
  295. .vtl-node {
  296. --node-select-bg: #3f506a;
  297. }
  298. }
  299. .vtl-node {
  300. --node-select-bg: #1c4869;
  301. .vtl-node-main {
  302. display: flex;
  303. align-items: center;
  304. padding: 2px 0 2px 2px;
  305. cursor: pointer;
  306. &:hover {
  307. .vtl-border-text {
  308. width: 80%;
  309. }
  310. }
  311. .vtl-border-text {
  312. display: flex; //lxh
  313. flex: 1;
  314. align-items: center; //lxh
  315. width: 100%;
  316. padding-left: 5px;
  317. .iconfont {
  318. width: 16px;
  319. height: 16px;
  320. vertical-align: text-bottom;
  321. }
  322. }
  323. &.selected {
  324. // background-color: rgba(45, 113, 134, 0.2);
  325. background-color: var(--node-select-bg);
  326. }
  327. .vtl-input {
  328. border: none;
  329. max-width: 150px;
  330. padding: 5px 0;
  331. padding-left: 5px;
  332. margin-left: 5px;
  333. &:focus {
  334. outline: none;
  335. }
  336. }
  337. .vtl-node-content {
  338. color: #fff;
  339. padding-left: 5px;
  340. font-size: 14px;
  341. width: 80%;
  342. display: inline-block;
  343. vertical-align: bottom;
  344. }
  345. &:hover {
  346. .vtl-node-content {
  347. color: #fff;
  348. overflow: hidden;
  349. }
  350. }
  351. &.vtl-active {
  352. * {
  353. pointer-events: none;
  354. }
  355. }
  356. &.vtl-active-file {
  357. outline: 2px dashed #353f51;
  358. }
  359. .vtl-operation {
  360. padding-right: 10px;
  361. }
  362. }
  363. }
  364. .vtl-tree-margin {
  365. padding-left: 1em;
  366. }
  367. </style>