ElementTree.vue 13 KB

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