ElementTree.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <template>
  2. <div class="elements">
  3. <div class="title" style="margin: 8px 0 0 12px">视图结构</div>
  4. <t-tree
  5. class="flex-grow"
  6. ref="tree"
  7. :data="data.tree"
  8. v-model:actived="data.actived"
  9. activable
  10. :expand-parent="true"
  11. style="padding: 0 4px 8px 8px"
  12. :scroll="{
  13. // rowHeight: 34,
  14. bufferSize: 20,
  15. threshold: 80,
  16. type: 'virtual',
  17. }"
  18. >
  19. <template #label="{ node }: any">
  20. <div class="flex middle" :class="{ gray: node.data.visible === false }">
  21. <template v-if="node.getChildren()">
  22. <folder-open-icon v-if="node.expanded"/>
  23. <folder-icon v-else/>
  24. <!-- <t-icon v-if="node.expanded" name="folder-open" />
  25. <t-icon v-else name="folder" /> -->
  26. </template>
  27. <control-platform-icon v-else/>
  28. <!-- <t-icon v-else name="control-platform" /> -->
  29. <t-input
  30. v-if="node.data.edited"
  31. v-model="node.data.label"
  32. :autofocus="true"
  33. @blur="onDescription(node)"
  34. @enter="onDescription(node)"
  35. />
  36. <span
  37. v-else
  38. @click="onActive(node.value)"
  39. @dblclick="node.data.edited = true"
  40. >
  41. {{ node.label }}
  42. </span>
  43. </div>
  44. </template>
  45. <template #operations="{ node }: any">
  46. <div
  47. class="flex middle operations"
  48. :class="{
  49. gray: node.data.visible === false,
  50. show: node.data.visible === false || node.data.locked,
  51. }"
  52. style="width: 36px; height: 16px"
  53. >
  54. <t-tag theme="primary" variant="light-outline">
  55. {{ node.data.tag }}</t-tag
  56. >
  57. <t-tooltip
  58. class="mr-4"
  59. v-if="!node.data.locked"
  60. content="可编辑"
  61. placement="top"
  62. >
  63. <svg class="l-icon" aria-hidden="true" @click="lock(node, 1)">
  64. <use xlink:href="#l-unlock"></use>
  65. </svg>
  66. </t-tooltip>
  67. <t-tooltip
  68. class="mr-4"
  69. v-else-if="node.data.locked == 1"
  70. content="禁止编辑"
  71. placement="top"
  72. >
  73. <svg class="l-icon" aria-hidden="true" @click="lock(node, 2)">
  74. <use xlink:href="#l-lock"></use>
  75. </svg>
  76. </t-tooltip>
  77. <t-tooltip
  78. class="mr-4"
  79. v-else-if="node.data.locked == 2"
  80. content="禁止编辑和移动"
  81. placement="top"
  82. >
  83. <svg class="l-icon" aria-hidden="true" @click="lock(node, 10)">
  84. <use xlink:href="#l-wufayidong"></use>
  85. </svg>
  86. </t-tooltip>
  87. <t-tooltip
  88. class="mr-4"
  89. v-else-if="node.data.locked == 10"
  90. content="禁止所有事件"
  91. placement="top"
  92. >
  93. <svg class="l-icon" aria-hidden="true" @click="lock(node, 0)">
  94. <use xlink:href="#l-jinyong"></use>
  95. </svg>
  96. </t-tooltip>
  97. <browse-icon v-if="node.data.visible !== false" @click="visible(node, false)"/>
  98. <browse-off-icon v-else @click="visible(node, true)"/>
  99. <!-- <t-icon
  100. v-if="node.data.visible !== false"
  101. name="browse"
  102. @click="visible(node, false)"
  103. />
  104. <t-icon v-else name="browse-off" @click="visible(node, true)" /> -->
  105. </div>
  106. </template>
  107. </t-tree>
  108. <div class="groups-panel" style="padding: 8px 0">
  109. <div class="flex middle between" style="padding: 0 12px">
  110. <div class="title">分组</div>
  111. <a @click="addGroup"> +新建分组</a>
  112. </div>
  113. <div class="groups">
  114. <div
  115. v-for="(item, i) in data.groups"
  116. class="flex middle between hover"
  117. :class="{ primary: i == data.activedGroup }"
  118. >
  119. <span
  120. class="flex-grow"
  121. v-if="i != data.editedGroup"
  122. @click="activeGroup(i)"
  123. @dblclick="
  124. data.activedGroup = data.editedGroup = i;
  125. data.group = item;
  126. "
  127. >
  128. {{ item }}
  129. </span>
  130. <t-input
  131. v-else
  132. v-model="data.group"
  133. :autofocus="true"
  134. @blur="setGroup"
  135. @enter="setGroup"
  136. />
  137. <browse-icon v-if="!data.hiddenGroups.includes(item)" @click="visibleGroup(item, false)"/>
  138. <browse-off-icon v-else @click="visibleGroup(item, true)"/>
  139. <!-- <t-icon
  140. v-if="!data.hiddenGroups.includes(item)"
  141. name="browse"
  142. @click="visibleGroup(item, false)"
  143. />
  144. <t-icon v-else name="browse-off" @click="visibleGroup(item, true)" /> -->
  145. <t-popconfirm
  146. content="确认删除该分组吗?"
  147. @confirm="delGroup"
  148. @cancel="data.deleteGroup = undefined"
  149. >
  150. <delete-icon class="ml-8"
  151. :class="{ block: i == data.deleteGroup }"
  152. @click="data.deleteGroup = i"/>
  153. <!-- <t-icon
  154. name="delete"
  155. class="ml-8"
  156. :class="{ block: i == data.deleteGroup }"
  157. @click="data.deleteGroup = i"
  158. /> -->
  159. </t-popconfirm>
  160. </div>
  161. </div>
  162. </div>
  163. </div>
  164. </template>
  165. <script lang="ts" setup>
  166. import { onBeforeMount, onMounted, onBeforeUnmount, reactive, ref } from 'vue';
  167. import { MessagePlugin } from 'tdesign-vue-next';
  168. import { LockState, Pen } from '@meta2d/core';
  169. import { getPenTree, inTreePanel, setChildrenVisible } from '@/services/common';
  170. import {FolderOpenIcon,FolderIcon,ControlPlatformIcon,BrowseIcon,BrowseOffIcon,DeleteIcon} from 'tdesign-icons-vue-next';
  171. const tree = ref<any>(null);
  172. const data = reactive<any>({
  173. tree: [],
  174. actived: [],
  175. groups: [],
  176. hiddenGroups: [],
  177. });
  178. onMounted(() => {
  179. meta2d.on('opened', getTree);
  180. meta2d.on('add', getTree);
  181. meta2d.on('undo', getTree);
  182. meta2d.on('redo', getTree);
  183. meta2d.on('delete', getTree);
  184. meta2d.on('combine', getTree);
  185. meta2d.on('click', getActived);
  186. meta2d.on('paste', getActived);
  187. if (inTreePanel.timer) {
  188. clearTimeout(inTreePanel.timer);
  189. inTreePanel.timer = undefined;
  190. }
  191. inTreePanel.value = true;
  192. getTree();
  193. getActived();
  194. const d = meta2d.store.data as any;
  195. if (!d.groups) {
  196. d.groups = [];
  197. }
  198. data.groups = d.groups;
  199. getHiddenGroups();
  200. });
  201. const getTree = () => {
  202. data.tree = getPenTree();
  203. };
  204. const getHiddenGroups = () => {
  205. data.groups.forEach((item) => {
  206. if (
  207. meta2d.store.data.pens.some(
  208. (pen) =>
  209. !pen.parentId && pen.tags?.includes(item) && pen.visible === false
  210. )
  211. ) {
  212. data.hiddenGroups.push(item);
  213. }
  214. });
  215. };
  216. const getActived = () => {
  217. data.actived = [];
  218. if (meta2d.store.active) {
  219. for (const pen of meta2d.store.active) {
  220. data.actived.push(pen.id);
  221. }
  222. const element = document.body.querySelector(
  223. `[data-value="${data.actived[0]}"]`
  224. );
  225. if (element) {
  226. element.scrollIntoView({ block: 'center' });
  227. } else {
  228. setTimeout(() => {
  229. const element = document.body.querySelector(
  230. `[data-value="${data.actived[0]}"]`
  231. );
  232. element && element.scrollIntoView({ block: 'center' });
  233. }, 500);
  234. }
  235. }
  236. };
  237. const calcElem = (node: Pen) => {
  238. if (!node) {
  239. return;
  240. }
  241. const elem: any = {
  242. label: (node as any).description || node.name,
  243. value: node.id,
  244. locked: node.locked,
  245. visible: node.visible,
  246. tag: (node as any).tag,
  247. };
  248. if (!node.children) {
  249. return elem;
  250. }
  251. elem.children = [];
  252. for (const id of node.children) {
  253. const child = calcElem(meta2d.store.pens[id]);
  254. child && elem.children.push(child);
  255. }
  256. return elem;
  257. };
  258. const onActive = (value: any) => {
  259. if (!value) {
  260. return;
  261. }
  262. const pens: Pen[] = [];
  263. for (const item of meta2d.store.data.pens) {
  264. // for (const v of value) {
  265. if (item.id === value) {
  266. pens.push(item);
  267. }
  268. // }
  269. }
  270. meta2d.active(pens, true);
  271. if (!pens[0].calculative?.inView) {
  272. meta2d.gotoView(pens[0]);
  273. meta2d.resize();
  274. }
  275. meta2d.render();
  276. };
  277. const lock = (node: any, v: LockState) => {
  278. node.data.locked = v;
  279. meta2d.setValue({
  280. id: node.value,
  281. locked: v,
  282. });
  283. };
  284. const visible = (node: any, v: boolean) => {
  285. node.data.visible = v;
  286. setChildrenVisible(node, v);
  287. const pen = meta2d.findOne(node.value);
  288. pen && meta2d.setVisible(pen, v);
  289. };
  290. const visibleGroup = (item, v: boolean) => {
  291. if (v) {
  292. let index = data.hiddenGroups.indexOf(item);
  293. if (index !== -1) {
  294. data.hiddenGroups.splice(index, 1);
  295. }
  296. } else {
  297. data.hiddenGroups.push(item);
  298. }
  299. let pens = meta2d.store.data.pens.filter(
  300. (pen) => !pen.parentId && pen.tags?.includes(item)
  301. );
  302. pens.forEach((pen) => {
  303. meta2d.setValue(
  304. { id: pen.id, visible: v },
  305. { render: false, doEvent: false }
  306. );
  307. });
  308. meta2d.render();
  309. };
  310. const onDescription = (node: any) => {
  311. node.data.edited = false;
  312. node.setData({ label: node.data.label });
  313. meta2d.setValue({
  314. id: node.value,
  315. description: node.data.label,
  316. });
  317. };
  318. const addGroup = () => {
  319. const i = data.groups.length + 1;
  320. data.group = '组' + i;
  321. data.groups.push(data.group);
  322. data.activedGroup = data.editedGroup = i;
  323. };
  324. const activeGroup = (i: number) => {
  325. data.activedGroup = i;
  326. const group = data.groups[i];
  327. const pens: Pen[] = [];
  328. for (const item of meta2d.store.data.pens) {
  329. if (item.tags?.includes(group)) {
  330. pens.push(item);
  331. }
  332. }
  333. meta2d.active(pens, false);
  334. meta2d.render();
  335. };
  336. const setGroup = () => {
  337. if (data.groups[data.editedGroup] === data.group) {
  338. data.editedGroup = undefined;
  339. return;
  340. }
  341. if (data.groups.includes(data.group)) {
  342. MessagePlugin.error('已经存在相同分组!');
  343. return;
  344. }
  345. for (const item of meta2d.store.data.pens) {
  346. // @ts-ignore
  347. if (item.group === data.groups[data.editedGroup]) {
  348. // @ts-ignore
  349. item.group === data.group;
  350. }
  351. }
  352. data.groups[data.editedGroup] = data.group;
  353. data.editedGroup = undefined;
  354. };
  355. const delGroup = () => {
  356. for (const item of meta2d.store.data.pens) {
  357. // @ts-ignore
  358. if (item.group === data.groups[data.deleteGroup]) {
  359. // @ts-ignore
  360. delete item.group;
  361. }
  362. }
  363. data.groups.splice(data.deleteGroup, 1);
  364. data.deleteGroup = undefined;
  365. };
  366. onBeforeUnmount(() => {
  367. meta2d.off('opened', getTree);
  368. meta2d.off('add', getTree);
  369. meta2d.off('undo', getTree);
  370. meta2d.off('redo', getTree);
  371. meta2d.off('delete', getTree);
  372. meta2d.off('combine', getTree);
  373. meta2d.off('click', getActived);
  374. meta2d.off('paste', getActived);
  375. inTreePanel.timer = setTimeout(() => {
  376. inTreePanel.value = false;
  377. }, 500);
  378. });
  379. </script>
  380. <style lang="postcss" scoped>
  381. .elements {
  382. display: flex;
  383. flex-direction: column;
  384. height: 100%;
  385. & > * {
  386. overflow-y: auto;
  387. width: 100%;
  388. }
  389. .t-tree {
  390. .t-tag {
  391. background-color: #4583ff33;
  392. position: absolute;
  393. right: 45px;
  394. width: 45.6px;
  395. height: 21.6px;
  396. font-size: 10px;
  397. line-height: 14px;
  398. transform: scale(0.83333);
  399. transform-origin: 0 0;
  400. }
  401. }
  402. .groups-panel {
  403. flex-shrink: 0;
  404. height: 240px;
  405. border-top: 1px solid var(--color-border-input);
  406. padding: 8px 12px;
  407. }
  408. .groups {
  409. height: calc(100% - 24px);
  410. overflow-y: auto;
  411. padding: 0 12px 16px 12px;
  412. & > div {
  413. height: 30px;
  414. line-height: 30px;
  415. svg {
  416. display: none;
  417. &.block {
  418. display: block;
  419. }
  420. }
  421. &:hover {
  422. svg {
  423. display: block;
  424. }
  425. }
  426. }
  427. }
  428. }
  429. </style>