Graphics.vue 31 KB

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