Graphics.vue 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. <template>
  2. <div class="graphics">
  3. <div class="input-search">
  4. <div class="btn">
  5. <t-icon name="search" />
  6. </div>
  7. <t-input placeholder="搜索" />
  8. </div>
  9. <div class="groups-panel">
  10. <div class="groups">
  11. <div
  12. v-for="group in groups"
  13. :class="group.name === activedGroup ? 'active' : ''"
  14. @click="groupChange(group.name)"
  15. >
  16. <t-icon :name="group.icon" />
  17. {{ group.name }}
  18. </div>
  19. </div>
  20. <div class="list" :class="groupType ? 'two-columns' : ''">
  21. <div v-if="activedGroup === '我的'" class="px-16 mt-12 mb-8 ml-4">
  22. <a @click="onCreateFolder">+ 新建文件夹</a>
  23. </div>
  24. <t-collapse
  25. v-if="groupType < 2"
  26. v-model:value="activedPanel"
  27. @change="onChangeGroupPanel"
  28. >
  29. <t-collapse-panel
  30. :value="item.name"
  31. v-for="item in subGroups"
  32. :key="item.name"
  33. >
  34. <template #header>
  35. <div class="flex middle mr-8">
  36. <div v-if="item.edited" @click.stop>
  37. <t-input
  38. v-model="item.label"
  39. style="width: 140px"
  40. @blur="createFoder"
  41. @enter="createFoder"
  42. @keyup="onKeyHeader"
  43. />
  44. </div>
  45. <div v-else class="ellipsis" style="width: 140px">
  46. {{ item.name }}
  47. </div>
  48. </div>
  49. </template>
  50. <template #headerRightContent v-if="item.canEdited">
  51. <t-space size="small" @click.stop>
  52. <t-icon
  53. name="edit"
  54. class="hover mr-4"
  55. @click="onEditHeader(item)"
  56. />
  57. <t-popconfirm
  58. content="确认删除该文件夹吗"
  59. placement="left"
  60. @confirm=""
  61. >
  62. <t-icon name="delete" class="hover" />
  63. </t-popconfirm>
  64. </t-space>
  65. </template>
  66. <div v-if="item.loading">
  67. <t-loading
  68. text="加载中..."
  69. size="small"
  70. style="margin-left: 32px; margin-bottom: 4px"
  71. />
  72. </div>
  73. <template v-else>
  74. <div
  75. class="graphic"
  76. v-for="elem in item.list"
  77. :draggable="true"
  78. @dragstart="dragStart($event, elem)"
  79. @drag="drag($event, elem)"
  80. @dragend="dragEnd()"
  81. @click.stop="dragStart($event, elem)"
  82. @dblclick.stop="open(elem)"
  83. @contextmenu="onContextMenu($event, item, elem)"
  84. >
  85. <t-image
  86. v-if="elem.image"
  87. :src="elem.image"
  88. :lazy="true"
  89. fit="contain"
  90. />
  91. <div class="svg-box" v-else-if="elem.svg" v-html="elem.svg" />
  92. <svg v-else class="l-icon" aria-hidden="true">
  93. <use :xlink:href="'#' + elem.icon"></use>
  94. </svg>
  95. <p :title="elem.name">{{ elem.name }}</p>
  96. <div class="price" v-if="elem.price > 0">
  97. ¥{{ elem.price }}
  98. </div>
  99. </div>
  100. <div
  101. v-if="!item.list || !item.list.length"
  102. class="gray center"
  103. style="white-space: nowrap; margin-left: 32px"
  104. >
  105. 暂无数据,待更新
  106. </div>
  107. </template>
  108. </t-collapse-panel>
  109. </t-collapse>
  110. <div v-else class="t-collapse-panel__content" style="padding: 8px">
  111. <div
  112. class="graphic"
  113. v-for="elem in subGroups"
  114. :draggable="true"
  115. @dragstart="dragStart($event, elem)"
  116. @drag="drag($event, elem)"
  117. @dragend="dragEnd()"
  118. @click.stop="dragStart($event, elem)"
  119. @dblclick.stop="open(elem)"
  120. >
  121. <t-image
  122. v-if="elem.image"
  123. :src="elem.image"
  124. :lazy="true"
  125. fit="contain"
  126. />
  127. <div class="svg-box" v-else-if="elem.svg" v-html="elem.svg" />
  128. <svg v-else class="l-icon" aria-hidden="true">
  129. <use :xlink:href="'#' + elem.icon"></use>
  130. </svg>
  131. <p :title="elem.name">{{ elem.name }}</p>
  132. <div class="price" v-if="elem.price > 0">¥{{ elem.price }}</div>
  133. </div>
  134. <div
  135. v-if="!subGroups.length"
  136. class="gray center"
  137. style="white-space: nowrap; margin-left: 32px"
  138. >
  139. 暂无数据,待更新
  140. </div>
  141. </div>
  142. </div>
  143. </div>
  144. <div
  145. class="context-menu-box"
  146. ref="contextmenuDom"
  147. v-if="contextmenu.visible"
  148. tabindex="0"
  149. :style="contextmenu.style"
  150. @blur="contextmenu.visible = false"
  151. >
  152. <t-menu class="context-menu" @change="onMenu" expandType="popup">
  153. <t-submenu
  154. value="move"
  155. title="移动到"
  156. v-if="contextmenu.subMenus.length"
  157. :disabled="!contextmenu.component.component"
  158. >
  159. <t-menu-item
  160. v-for="subMenu in contextmenu.subMenus"
  161. :value="'move:' + subMenu.name"
  162. >
  163. {{ subMenu.name }}
  164. </t-menu-item>
  165. </t-submenu>
  166. <t-menu-item value="edit"> 编辑 </t-menu-item>
  167. <t-menu-item value="del" :disabled="!contextmenu.component.component">
  168. 删除
  169. </t-menu-item>
  170. </t-menu>
  171. </div>
  172. <t-dialog
  173. v-if="delDialog.show"
  174. theme="danger"
  175. header="删除"
  176. :visible="true"
  177. @close="delDialog.show = false"
  178. @confirm="delComponet"
  179. >
  180. 确定删除该数据吗?删除后不可恢复!
  181. </t-dialog>
  182. </div>
  183. </template>
  184. <script lang="ts" setup>
  185. import { onMounted, onUnmounted, reactive, ref } from 'vue';
  186. import { useRouter } from 'vue-router';
  187. import axios from 'axios';
  188. import { cases, shapes, charts, formComponents } from '@/services/defaults';
  189. import { getFolders, getFiles, getIcons } from '@/services/png';
  190. import {
  191. getComponents,
  192. getComponentsList,
  193. getLe5leV,
  194. updateCollection,
  195. } from '@/services/api';
  196. import { convertPen } from '@/services/upgrade';
  197. import { deepClone } from '@meta2d/core';
  198. import { isGif } from '@/services/utils';
  199. import { autoSave, delAttrs } from '@/services/common';
  200. import { MessagePlugin } from 'tdesign-vue-next';
  201. const router = useRouter();
  202. const activedGroup = ref('');
  203. const groups = reactive([
  204. {
  205. icon: 'desktop',
  206. name: '场景',
  207. key: '',
  208. class: 'tow',
  209. },
  210. {
  211. icon: 'root-list',
  212. name: '模板',
  213. key: '',
  214. },
  215. {
  216. icon: 'chart',
  217. name: '图表',
  218. key: 'chart',
  219. },
  220. {
  221. icon: 'image',
  222. name: '素材',
  223. key: '',
  224. },
  225. {
  226. icon: 'control-platform',
  227. name: '图元',
  228. key: '',
  229. },
  230. {
  231. icon: 'relativity',
  232. name: '控件',
  233. key: '',
  234. },
  235. {
  236. icon: 'chart-bubble',
  237. name: '图形',
  238. key: 'shape',
  239. },
  240. {
  241. icon: 'app',
  242. name: '我的',
  243. key: '',
  244. },
  245. ]);
  246. const subGroups = ref<any[]>([]);
  247. const groupType = ref(0);
  248. const activedPanel = ref([]);
  249. const caseCaches = reactive<any>({});
  250. const templates = ref([]);
  251. const materials = ref([]);
  252. const pngs = ref([]);
  253. const icons = ref([]);
  254. const groupChange = async (name: string) => {
  255. activedPanel.value = [];
  256. activedGroup.value = name;
  257. groupType.value = 0;
  258. switch (name) {
  259. case '场景':
  260. groupType.value = 1;
  261. subGroups.value = cases;
  262. subGroups.value[0].loading = true;
  263. if (!caseCaches[name + cases[0].name]) {
  264. caseCaches[name + cases[0].name] = await getCaseProjects(
  265. name,
  266. cases[0].name
  267. );
  268. }
  269. subGroups.value[0].list = caseCaches[name + cases[0].name];
  270. subGroups.value[0].loading = false;
  271. break;
  272. case '模板':
  273. groupType.value = 2;
  274. if (!templates.value.length) {
  275. templates.value = await getCaseProjects(name);
  276. }
  277. subGroups.value = templates.value;
  278. break;
  279. case '图表':
  280. subGroups.value = charts;
  281. break;
  282. case '控件':
  283. subGroups.value = formComponents;
  284. break;
  285. case '素材':
  286. groupType.value = 2;
  287. if (!materials.value.length) {
  288. materials.value = await getFiles('material/');
  289. }
  290. subGroups.value = materials.value;
  291. break;
  292. case '图元':
  293. subGroups.value = [];
  294. if (!pngs.value.length) {
  295. pngs.value = await getFolders('png');
  296. }
  297. subGroups.value.push(...pngs.value);
  298. if (!icons.value.length) {
  299. icons.value = await getFolders('svg');
  300. }
  301. subGroups.value.push(...icons.value);
  302. onChangeGroupPanel([subGroups.value[0].name]);
  303. break;
  304. case '图形':
  305. subGroups.value = shapes;
  306. break;
  307. case '我的':
  308. subGroups.value = await getPrivateGroups();
  309. groupType.value = 1;
  310. onChangeGroupPanel([subGroups.value[0].name]);
  311. break;
  312. }
  313. activedPanel.value = [subGroups.value[0].name];
  314. };
  315. const getCaseProjects = async (name: string, group?: string) => {
  316. const query: any = { tags: name };
  317. if (group) {
  318. query.case = group;
  319. }
  320. const ret: any = await axios.post(
  321. '/api/data/le5leV/list?current=1&pageSize=100',
  322. {
  323. query,
  324. shared: 'true',
  325. projection: { _id: 1, name: 1, image: 1, price: 1 },
  326. }
  327. );
  328. if (!ret) {
  329. return [];
  330. }
  331. for (const item of ret.list) {
  332. item.draggable = false;
  333. }
  334. return ret.list;
  335. };
  336. const getPrivateGroups = async () => {
  337. const list = [
  338. {
  339. name: '我的组件',
  340. list: [],
  341. },
  342. ];
  343. const config = {
  344. params: {
  345. current: 1,
  346. pageSize: 1000,
  347. },
  348. };
  349. let ret: any = await axios.post(
  350. '/api/data/folders/list',
  351. {
  352. projection: {
  353. image: 1,
  354. _id: 1,
  355. name: 1,
  356. list: 1,
  357. },
  358. query: {
  359. type: `le5leV-components`,
  360. },
  361. sort: { createdAt: 1 },
  362. },
  363. config
  364. );
  365. if (!ret) {
  366. ret = { list: [] };
  367. }
  368. for (const item of ret.list) {
  369. item.canEdited = true;
  370. }
  371. list.push(...ret.list);
  372. list.push({
  373. name: '3D',
  374. list: [],
  375. });
  376. return list;
  377. };
  378. const dragStart = async (event: DragEvent | MouseEvent, item: any) => {
  379. event.stopPropagation();
  380. let data = null;
  381. if (!item || (event instanceof DragEvent && !event.dataTransfer)) {
  382. return;
  383. }
  384. if (!item.draggable) {
  385. data = item.data;
  386. } else if (item['3d']) {
  387. data = {
  388. name: 'iframe',
  389. width: 400,
  390. height: 300,
  391. externElement: true,
  392. iframe: 'https://view3d.le5le.com/?id=' + (item._id || item.id),
  393. };
  394. } else {
  395. if (item._id && !item.componentDatas) {
  396. let res: any = await getComponents(item._id);
  397. item.component = true;
  398. item.componentDatas = res.componentDatas;
  399. item.componentData = res.componentData;
  400. }
  401. if (!item.data && !item.component && item.image) {
  402. let target: any = event.target;
  403. target.children[0] && (target = target.children[0].children[0]);
  404. // firefox naturalWidth svg 图片 可能是 0
  405. const normalWidth = target.naturalWidth || target.width;
  406. const normalHeight = target.naturalHeight || target.height;
  407. const [name, lockedOnCombine] = isGif(item.image)
  408. ? ['gif', 0]
  409. : ['image', undefined];
  410. data = {
  411. name,
  412. width: 100,
  413. height: 100 * (normalHeight / normalWidth),
  414. image: item.image,
  415. imageRatio: true,
  416. lockedOnCombine,
  417. };
  418. } else if (item.component) {
  419. if (item.componentData) {
  420. const pens = convertPen([item.componentData]);
  421. data = deepClone(pens);
  422. } else if (item.componentDatas) {
  423. data = deepClone(item.componentDatas);
  424. }
  425. } else {
  426. data = item.componentDatas || item.data;
  427. }
  428. }
  429. if (event instanceof DragEvent) {
  430. meta2d.canvas.addCaches = [];
  431. event.dataTransfer?.setData('Meta2d', JSON.stringify(data));
  432. } else {
  433. if (!Array.isArray(data)) {
  434. data = deepClone([data]);
  435. }
  436. meta2d.canvas.addCaches = data;
  437. }
  438. };
  439. const drag = (event: DragEvent, item: any) => {};
  440. const dragEnd = () => {};
  441. const dragstart = (event: any) => {
  442. event.target.style.opacity = 0.5;
  443. };
  444. const dragend = (event: any) => {
  445. event.target.style.opacity = 1;
  446. };
  447. const open = async (item: any) => {
  448. if (item.draggable !== false) {
  449. return;
  450. }
  451. router.push({
  452. path: '/',
  453. query: {
  454. r: Date.now() + '',
  455. },
  456. });
  457. const ret: any = await getLe5leV(item._id || item.id);
  458. for (const k of delAttrs) {
  459. delete (ret as any)[k];
  460. }
  461. meta2d.open(ret);
  462. autoSave(true);
  463. };
  464. const onChangeGroupPanel = async (val: string[]) => {
  465. if (val?.length) {
  466. for (const name of val) {
  467. switch (activedGroup.value) {
  468. case '场景':
  469. if (
  470. !caseCaches[activedGroup.value + name] ||
  471. !caseCaches[activedGroup.value + name].length
  472. ) {
  473. for (const item of subGroups.value) {
  474. if (item.name === name) {
  475. item.loading = true;
  476. }
  477. }
  478. caseCaches[activedGroup.value + name] = await getCaseProjects(
  479. activedGroup.value,
  480. name
  481. );
  482. for (const item of subGroups.value) {
  483. if (item.name === name) {
  484. item.list = caseCaches[activedGroup.value + name];
  485. item.loading = false;
  486. }
  487. }
  488. }
  489. break;
  490. case '图元':
  491. for (const item of subGroups.value) {
  492. if (item.name === name && !item.list.length) {
  493. item.loading = true;
  494. if (item.svg) {
  495. item.list = await getIcons(item.folder);
  496. } else {
  497. item.list = await getFiles(item.folder + item.name);
  498. }
  499. item.loading = false;
  500. }
  501. }
  502. break;
  503. case '我的':
  504. for (const item of subGroups.value) {
  505. if (!item.list.length) {
  506. item.loading = true;
  507. if (item.name === '我的组件') {
  508. const data = {
  509. query: { folder: '' },
  510. projection: {
  511. image: 1,
  512. _id: 1,
  513. name: 1,
  514. component: 1,
  515. },
  516. };
  517. const config = {
  518. params: {
  519. current: 1,
  520. pageSize: 1000,
  521. },
  522. };
  523. const res: any = await getComponentsList(data, config);
  524. if (res?.list) {
  525. item.list = res.list;
  526. }
  527. } else if (item.name === '3D') {
  528. const data = {
  529. projection: {
  530. image: 1,
  531. _id: 1,
  532. name: 1,
  533. },
  534. };
  535. const config = {
  536. params: {
  537. current: 1,
  538. pageSize: 1000,
  539. },
  540. };
  541. const res: any = await axios.post(
  542. '/api/data/le5le3d/list',
  543. data,
  544. config
  545. );
  546. if (res?.list) {
  547. for (const item of res.list) {
  548. item['3d'] = true;
  549. }
  550. item.list = res.list;
  551. }
  552. }
  553. item.loading = false;
  554. }
  555. }
  556. break;
  557. }
  558. }
  559. }
  560. };
  561. const editedFolder = ref<any>(undefined);
  562. const onCreateFolder = () => {
  563. editedFolder.value = {
  564. _id: '',
  565. name: '',
  566. label: '新建文件夹',
  567. list: [],
  568. edited: true,
  569. canEdited: true,
  570. };
  571. subGroups.value.splice(subGroups.value.length - 1, 0, editedFolder.value);
  572. };
  573. const createFoder = async () => {
  574. if (!editedFolder.value.label) {
  575. return;
  576. }
  577. if (editedFolder.value.label === editedFolder.value.name) {
  578. editedFolder.value.edited = false;
  579. return;
  580. }
  581. const found = subGroups.value.findIndex(
  582. (group: any) => group.name === editedFolder.value.label
  583. );
  584. if (found >= 0) {
  585. MessagePlugin.error('已经存在相同名称文件夹');
  586. return;
  587. }
  588. if (editedFolder.value._id) {
  589. const ret: any = await axios.post('/api/data/folders/update', {
  590. _id: editedFolder.value._id,
  591. name: editedFolder.value.label,
  592. });
  593. if (ret) {
  594. editedFolder.value.name = editedFolder.value.label;
  595. editedFolder.value.edited = false;
  596. }
  597. } else {
  598. const ret: any = await axios.post('/api/data/folders/add', {
  599. name: editedFolder.value.label,
  600. type: 'le5leV-components',
  601. list: [],
  602. });
  603. if (ret) {
  604. editedFolder.value.name = editedFolder.value.label;
  605. editedFolder.value._id = ret._id;
  606. editedFolder.value.edited = false;
  607. }
  608. }
  609. };
  610. const onEditHeader = (item: any) => {
  611. item.label = item.name;
  612. item.edited = true;
  613. editedFolder.value = item;
  614. };
  615. const onKeyHeader = (text: string, event: any) => {
  616. if (event.e.key === 'Escape') {
  617. editedFolder.value.edited = false;
  618. }
  619. };
  620. // 我的组件右键菜单
  621. const contextmenu = reactive<any>({
  622. visible: false,
  623. style: {},
  624. // 子分类
  625. group: undefined,
  626. // 组件图纸
  627. component: undefined,
  628. // 右键二级子菜单
  629. subMenus: [],
  630. });
  631. const contextmenuDom = ref<any>(null);
  632. const onContextMenu = async (e: MouseEvent, group: string, item: any) => {
  633. e.preventDefault();
  634. e.stopPropagation();
  635. if (activedGroup.value !== '我的') {
  636. return;
  637. }
  638. contextmenu.group = group;
  639. contextmenu.component = item;
  640. if (document.body.clientHeight - e.clientY < 128) {
  641. contextmenu.style = {
  642. left: e.clientX + 'px',
  643. bottom: '4px',
  644. };
  645. } else {
  646. contextmenu.style = {
  647. left: e.clientX + 'px',
  648. top: e.clientY + 'px',
  649. };
  650. }
  651. contextmenu.subMenus = [];
  652. for (const elem of subGroups.value) {
  653. if (elem === group || elem.name === '3D') {
  654. continue;
  655. }
  656. contextmenu.subMenus.push(elem);
  657. }
  658. contextmenu.visible = true;
  659. setTimeout(() => {
  660. if (contextmenuDom.value) {
  661. contextmenuDom.value.focus();
  662. }
  663. }, 500);
  664. };
  665. const delDialog = reactive<any>({});
  666. const onMenu = async (val: string) => {
  667. const id = contextmenu.component._id || contextmenu.component.id;
  668. switch (val) {
  669. case 'edit':
  670. if (contextmenu.component.component) {
  671. autoSave();
  672. router.push({
  673. path: '/',
  674. query: {
  675. id,
  676. c: 1,
  677. r: Date.now() + '',
  678. },
  679. });
  680. } else {
  681. let url = 'https://3d.le5le.com/?id=';
  682. if (import.meta.env.VITE_TRIAL == 0 && (window as any).url3D) {
  683. url = (window as any).url3D;
  684. }
  685. window.open(url + id, '_blank');
  686. }
  687. break;
  688. case 'del':
  689. delDialog.show = true;
  690. break;
  691. default:
  692. if (val.indexOf('move:')) {
  693. return;
  694. }
  695. val = val.replace('move:', '');
  696. const group = contextmenu.subMenus.find(
  697. (element: any) => element.name === val
  698. );
  699. if (!group) {
  700. return;
  701. }
  702. // 前端: 添加组件到目标文件夹
  703. group.list.push(contextmenu.component);
  704. // 前端:从源文件夹移出组件
  705. contextmenu.group.list.forEach((item: any, index: number, arr: any[]) => {
  706. if (id === item._id || id === item.id) {
  707. arr.splice(index, 1);
  708. }
  709. });
  710. // 更新后端组件信息
  711. let ret = await updateCollection('le5leV-components', {
  712. _id: id,
  713. folder: val === '我的组件' ? '' : val,
  714. });
  715. if (!ret) {
  716. return;
  717. }
  718. // 更新后端源文件夹列表
  719. if (contextmenu.group.name !== '我的组件') {
  720. await axios.post('/api/data/folders/update', {
  721. _id: contextmenu.group._id || contextmenu.group.id,
  722. list: contextmenu.group.list,
  723. });
  724. }
  725. // 更新后端目标文件夹列表
  726. if (group.name !== '我的组件') {
  727. await axios.post('/api/data/folders/update', {
  728. _id: group._id || group.id,
  729. list: group.list,
  730. });
  731. }
  732. break;
  733. }
  734. contextmenu.visible = false;
  735. };
  736. const delComponet = async () => {
  737. const id = contextmenu.component._id || contextmenu.component.id;
  738. await axios.post(`/api/data/le5leV-components/delete`, {
  739. id,
  740. });
  741. // 前端:从源文件夹移出组件
  742. contextmenu.group.list.forEach((item: any, index: number, arr: any[]) => {
  743. if (id === item._id || id === item.id) {
  744. arr.splice(index, 1);
  745. }
  746. });
  747. // 更新后端源文件夹列表
  748. if (contextmenu.group.name !== '我的组件') {
  749. await axios.post('/api/data/folders/update', {
  750. _id: contextmenu.group._id || contextmenu.group.id,
  751. list: contextmenu.group.list,
  752. });
  753. }
  754. delDialog.show = false;
  755. };
  756. onMounted(() => {
  757. groupChange('场景');
  758. document.addEventListener('dragstart', dragstart, false);
  759. document.addEventListener('dragend', dragend, false);
  760. setTimeout(() => {
  761. meta2d.on('drop', open);
  762. }, 2000);
  763. });
  764. onUnmounted(() => {
  765. document.removeEventListener('dragstart', dragstart);
  766. document.removeEventListener('dragend', dragend);
  767. meta2d.off('drop', open);
  768. });
  769. </script>
  770. <style lang="postcss" scoped>
  771. .graphics {
  772. display: flex;
  773. flex-direction: column;
  774. .input-search {
  775. flex-shrink: 0;
  776. height: 40px;
  777. }
  778. .groups-panel {
  779. display: grid;
  780. grid-template-columns: 50px 1fr;
  781. border-top: 1px solid var(--color-background-input);
  782. flex-grow: 1;
  783. overflow-y: auto;
  784. font-size: 12px;
  785. z-index: 7;
  786. .groups {
  787. & > div {
  788. display: flex;
  789. flex-direction: column;
  790. align-items: center;
  791. padding: 16px 4px;
  792. line-height: 1;
  793. cursor: pointer;
  794. .t-icon {
  795. font-size: 20px;
  796. margin-bottom: 8px;
  797. }
  798. &:hover {
  799. color: var(--color-primary);
  800. }
  801. }
  802. .active {
  803. background-color: var(--color-background-active);
  804. color: var(--color-primary);
  805. border-left: 2px solid var(--color-primary);
  806. }
  807. }
  808. .list {
  809. overflow-y: auto;
  810. max-height: calc(100vh - 100px);
  811. background-color: var(--color-background-active);
  812. padding-top: 8px;
  813. * {
  814. background-color: var(--color-background-active);
  815. }
  816. :deep(.t-collapse) {
  817. min-height: 100vh;
  818. border: none;
  819. }
  820. :deep(.t-collapse-panel__header) {
  821. border: none;
  822. font-size: 12px;
  823. font-weight: 400;
  824. padding: 8px 16px;
  825. &:hover {
  826. color: var(--color-primary);
  827. }
  828. }
  829. :deep(.t-collapse-panel__body) {
  830. border: none;
  831. }
  832. :deep(.t-collapse-panel__content) {
  833. background-color: var(--color-background-active);
  834. padding: 4px 4px 20px 4px;
  835. display: grid;
  836. grid-template-columns: 1fr 1fr 1fr;
  837. grid-row-gap: 12px;
  838. }
  839. :deep(.t-loading--center) {
  840. width: 100px;
  841. .t-loading__text {
  842. margin-left: 8px;
  843. height: 24px;
  844. }
  845. }
  846. :deep(.t-image__error) {
  847. .t-space-item:last-child {
  848. display: none;
  849. }
  850. }
  851. :deep(.t-image__loading) {
  852. .t-space-item:last-child {
  853. display: none;
  854. }
  855. }
  856. .graphic {
  857. position: relative;
  858. padding: 10px 0;
  859. border-radius: 2px;
  860. border: 1px solid transparent;
  861. &:hover {
  862. cursor: pointer;
  863. border-color: var(--color-primary);
  864. }
  865. p {
  866. margin-top: 10px;
  867. padding: 0 10px;
  868. text-align: center;
  869. font-size: 12px;
  870. height: 12px;
  871. line-height: 1;
  872. overflow: hidden;
  873. text-overflow: ellipsis;
  874. display: -webkit-box;
  875. -webkit-line-clamp: 1;
  876. word-break: break-all;
  877. -webkit-box-orient: vertical;
  878. }
  879. .t-image__wrapper {
  880. height: 32px;
  881. width: 32px;
  882. margin: auto;
  883. :deep(.t-image) {
  884. border-radius: 2px;
  885. }
  886. }
  887. svg {
  888. color: var(--color);
  889. height: 32px;
  890. width: 100%;
  891. margin: auto;
  892. }
  893. .svg-box {
  894. height: 32px;
  895. width: 32px;
  896. margin-left: calc(50% - 16px);
  897. margin-top: 10px;
  898. margin-bottom: 10px;
  899. &:deep(svg) {
  900. height: 100%;
  901. width: 100%;
  902. .cls-1 {
  903. stroke: var(--color) !important;
  904. }
  905. }
  906. }
  907. .price {
  908. position: absolute;
  909. top: 8px;
  910. right: 8px;
  911. display: inline-block;
  912. z-index: 1;
  913. border-radius: 2px;
  914. background-color: #ff400060;
  915. color: var(--color-bland);
  916. font-size: 10px;
  917. line-height: 1;
  918. padding: 3px;
  919. }
  920. }
  921. }
  922. .two-columns {
  923. :deep(.t-collapse-panel__content) {
  924. grid-template-columns: 1fr 1fr;
  925. }
  926. .graphic {
  927. .t-image__wrapper {
  928. width: 100px;
  929. height: 88px;
  930. background-color: var(--color-background);
  931. }
  932. }
  933. }
  934. }
  935. .context-menu-box {
  936. position: fixed;
  937. z-index: 200;
  938. & > div {
  939. width: 140px !important;
  940. }
  941. :deep(.t-menu) {
  942. .t-menu__item {
  943. &.t-is-opened {
  944. background-color: var(--color-background-popup-hover);
  945. transition: none !important;
  946. }
  947. }
  948. .t-fake-arrow {
  949. transform: rotate(-90deg) !important;
  950. }
  951. .t-fake-arrow--active {
  952. transform: rotate(90deg) !important;
  953. }
  954. }
  955. }
  956. }
  957. </style>