Graphics.vue 26 KB

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