ElementTree.vue 11 KB

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