Graphics.vue 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292
  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. x: 0,
  478. y: 0,
  479. tags: ['meta3d'],
  480. zIndex: 1,
  481. width: meta2d.store.data.width || meta2d.store.options.width,
  482. height: meta2d.store.data.height || meta2d.store.options.height,
  483. externElement: true,
  484. iframe: 'https://view3d.le5le.com/?id=' + (item._id || item.id),
  485. };
  486. } else if (item.component) {
  487. // 我的组件
  488. if (!item.componentDatas && !item.componentData) {
  489. isAsync = true;
  490. const ret: any = await axios.post(`/api/data/le5leV-components/get`, {
  491. id,
  492. });
  493. item.componentDatas = ret.componentDatas;
  494. item.componentData = ret.componentData;
  495. }
  496. if (item.componentData) {
  497. const pens = convertPen([item.componentData]);
  498. data = deepClone(pens);
  499. } else if (item.componentDatas) {
  500. data = deepClone(item.componentDatas);
  501. }
  502. } else if (item.data) {
  503. // 普通图元
  504. data = item.data;
  505. } else if (item.image) {
  506. // 拖拽图片
  507. let target: any = event.target;
  508. target.children[0] && (target = target.children[0].children[0]);
  509. // firefox naturalWidth svg 图片 可能是 0
  510. const width = target.naturalWidth || target.width;
  511. const height = target.naturalHeight || target.height;
  512. const [name, lockedOnCombine] = isGif(item.image)
  513. ? ['gif', 0]
  514. : ['image', undefined];
  515. data = {
  516. name,
  517. width,
  518. height,
  519. image: item.image,
  520. imageRatio: true,
  521. ratio: true,
  522. lockedOnCombine,
  523. };
  524. } else {
  525. return;
  526. }
  527. if (!Array.isArray(data)) {
  528. data = deepClone([data]);
  529. }
  530. !dropped && (meta2d.canvas.addCaches = data);
  531. if (event instanceof DragEvent) {
  532. event.dataTransfer.setData(
  533. 'Meta2d',
  534. isAsync ? undefined : JSON.stringify(data)
  535. );
  536. }
  537. };
  538. const dragstart = (event: any) => {
  539. event.target.style.opacity = 0.5;
  540. };
  541. const dragend = (event: any) => {
  542. event.target.style.opacity = 1;
  543. };
  544. const open = async (item: any) => {
  545. if (!item || item.draggable !== false) {
  546. return;
  547. }
  548. const ret: any = await getLe5leV(item._id || item.id);
  549. if (!ret) {
  550. if (item.price > 0) {
  551. chargeDialog.data = item;
  552. chargeDialog.show = true;
  553. }
  554. return;
  555. }
  556. sessionStorage.setItem('opening', '1');
  557. router.push({
  558. path: '/',
  559. query: {
  560. r: Date.now() + '',
  561. },
  562. });
  563. for (const k of delAttrs) {
  564. delete ret[k];
  565. }
  566. autoSave();
  567. meta2d.open(ret);
  568. meta2d.fitView();
  569. };
  570. const getPrivateGraphics = async () => {
  571. for (const item of subGroups.value) {
  572. if (!item.list.length) {
  573. item.loading = true;
  574. if (item.name === '我的组件') {
  575. const data = {
  576. query: { folder: '' },
  577. projection: {
  578. image: 1,
  579. _id: 1,
  580. name: 1,
  581. component: 1,
  582. },
  583. };
  584. const config = {
  585. params: {
  586. current: 1,
  587. pageSize: 1000,
  588. },
  589. };
  590. const res: any = await getComponentsList(data, config);
  591. if (res?.list) {
  592. item.list = res.list;
  593. }
  594. } else if (item.name === '3D') {
  595. const data = {
  596. projection: {
  597. image: 1,
  598. _id: 1,
  599. name: 1,
  600. },
  601. };
  602. const config = {
  603. params: {
  604. current: 1,
  605. pageSize: 1000,
  606. },
  607. };
  608. const res: any = await axios.post(
  609. '/api/data/le5le3d/list',
  610. data,
  611. config
  612. );
  613. if (res?.list) {
  614. for (const item of res.list) {
  615. item['3d'] = true;
  616. item['draggable'] = true;
  617. }
  618. item.list = res.list;
  619. }
  620. }
  621. item.loading = false;
  622. }
  623. }
  624. };
  625. const editedFolder = ref<any>(undefined);
  626. const onCreateFolder = () => {
  627. activedPanels[activedGroup.value].splice(
  628. 0,
  629. activedPanels[activedGroup.value].length
  630. );
  631. editedFolder.value = {
  632. _id: '',
  633. name: '',
  634. label: '新建文件夹',
  635. list: [],
  636. edited: true,
  637. canEdited: true,
  638. };
  639. subGroups.value.splice(subGroups.value.length - 1, 0, editedFolder.value);
  640. };
  641. const createFoder = async () => {
  642. if (!editedFolder.value.label) {
  643. return;
  644. }
  645. if (editedFolder.value.label === editedFolder.value.name) {
  646. editedFolder.value.edited = false;
  647. return;
  648. }
  649. const found = subGroups.value.findIndex(
  650. (group: any) => group.name === editedFolder.value.label
  651. );
  652. if (found >= 0) {
  653. MessagePlugin.error('已经存在相同名称文件夹');
  654. return;
  655. }
  656. if (editedFolder.value._id) {
  657. const ret: any = await axios.post('/api/data/folders/update', {
  658. _id: editedFolder.value._id,
  659. name: editedFolder.value.label,
  660. });
  661. if (ret) {
  662. editedFolder.value.name = editedFolder.value.label;
  663. editedFolder.value.edited = false;
  664. }
  665. } else {
  666. const ret: any = await axios.post('/api/data/folders/add', {
  667. name: editedFolder.value.label,
  668. type: 'le5leV-components',
  669. list: [],
  670. });
  671. if (ret) {
  672. editedFolder.value.name = editedFolder.value.label;
  673. editedFolder.value._id = ret._id;
  674. editedFolder.value.edited = false;
  675. }
  676. }
  677. };
  678. const onEditHeader = (item: any) => {
  679. item.label = item.name;
  680. item.edited = true;
  681. editedFolder.value = item;
  682. };
  683. const onKeyHeader = (text: string, event: any) => {
  684. if (event.e.key === 'Escape') {
  685. editedFolder.value.edited = false;
  686. }
  687. };
  688. // 我的组件右键菜单
  689. const contextmenu = reactive<any>({
  690. visible: false,
  691. style: {},
  692. // 子分类
  693. group: undefined,
  694. // 组件图纸
  695. component: {},
  696. // 右键二级子菜单
  697. subMenus: [],
  698. });
  699. const contextmenuDom = ref<any>(null);
  700. const onContextMenu = async (e: MouseEvent, group: string, item: any) => {
  701. e.preventDefault();
  702. e.stopPropagation();
  703. if (activedGroup.value !== '我的') {
  704. return;
  705. }
  706. contextmenu.group = group;
  707. contextmenu.component = item;
  708. if (document.body.clientHeight - e.clientY < 128) {
  709. contextmenu.style = {
  710. left: e.clientX + 'px',
  711. bottom: '4px',
  712. };
  713. } else {
  714. contextmenu.style = {
  715. left: e.clientX + 'px',
  716. top: e.clientY + 'px',
  717. };
  718. }
  719. contextmenu.subMenus = [];
  720. for (const elem of subGroups.value) {
  721. if (elem === group || elem.name === '3D') {
  722. continue;
  723. }
  724. contextmenu.subMenus.push(elem);
  725. }
  726. contextmenu.visible = true;
  727. setTimeout(() => {
  728. if (contextmenuDom.value) {
  729. contextmenuDom.value.focus();
  730. }
  731. }, 500);
  732. };
  733. const delDialog = reactive<any>({});
  734. const onMenu = async (val: string) => {
  735. const id = contextmenu.component._id || contextmenu.component.id;
  736. setTimeout(() => {
  737. contextmenu.group = '';
  738. contextmenu.component = {};
  739. contextmenu.subMenus = [];
  740. }, 500);
  741. switch (val) {
  742. case 'edit':
  743. if (contextmenu.component.component) {
  744. autoSave();
  745. router.push({
  746. path: '/',
  747. query: {
  748. id,
  749. c: 1,
  750. r: Date.now() + '',
  751. },
  752. });
  753. } else {
  754. let url = 'https://3d.le5le.com/?id=';
  755. if (import.meta.env.VITE_TRIAL == 0 && (window as any).url3D) {
  756. url = (window as any).url3D;
  757. }
  758. window.open(url + id, '_blank');
  759. }
  760. break;
  761. case 'del':
  762. delDialog.show = true;
  763. break;
  764. default:
  765. if (val.indexOf('move:')) {
  766. return;
  767. }
  768. val = val.replace('move:', '');
  769. const group = contextmenu.subMenus.find(
  770. (element: any) => element.name === val
  771. );
  772. if (!group) {
  773. return;
  774. }
  775. // 前端: 添加组件到目标文件夹
  776. group.list.push(contextmenu.component);
  777. // 前端:从源文件夹移出组件
  778. contextmenu.group.list.forEach((item: any, index: number, arr: any[]) => {
  779. if (id === item._id || id === item.id) {
  780. arr.splice(index, 1);
  781. }
  782. });
  783. // 更新后端组件信息
  784. let ret = await updateCollection('le5leV-components', {
  785. _id: id,
  786. folder: val === '我的组件' ? '' : val,
  787. });
  788. if (!ret) {
  789. return;
  790. }
  791. // 更新后端源文件夹列表
  792. if (contextmenu.group.name !== '我的组件') {
  793. await axios.post('/api/data/folders/update', {
  794. _id: contextmenu.group._id || contextmenu.group.id,
  795. list: contextmenu.group.list,
  796. });
  797. }
  798. // 更新后端目标文件夹列表
  799. if (group.name !== '我的组件') {
  800. await axios.post('/api/data/folders/update', {
  801. _id: group._id || group.id,
  802. list: group.list,
  803. });
  804. }
  805. break;
  806. }
  807. };
  808. const hideContextmenu = () => {
  809. contextmenu.visible = false;
  810. };
  811. const delComponet = async () => {
  812. const id = contextmenu.component._id || contextmenu.component.id;
  813. await axios.post(`/api/data/le5leV-components/delete`, {
  814. id,
  815. });
  816. // 前端:从源文件夹移出组件
  817. contextmenu.group.list.forEach((item: any, index: number, arr: any[]) => {
  818. if (id === item._id || id === item.id) {
  819. arr.splice(index, 1);
  820. }
  821. });
  822. // 更新后端源文件夹列表
  823. if (contextmenu.group.name !== '我的组件') {
  824. await axios.post('/api/data/folders/update', {
  825. _id: contextmenu.group._id || contextmenu.group.id,
  826. list: contextmenu.group.list,
  827. });
  828. }
  829. delDialog.show = false;
  830. };
  831. const drop = (obj: any) => {
  832. dropped = true;
  833. if (obj) {
  834. Array.isArray(obj) && open(obj[0]);
  835. } else {
  836. MessagePlugin.warning('正在请求网络数据中,请稍后重试');
  837. }
  838. };
  839. const onSubmitOrder = async () => {
  840. const result: any = await axios.post('/api/order/datas/submit', {
  841. collection: 'le5leV',
  842. id: chargeDialog.data._id,
  843. });
  844. if (result) {
  845. wechatPayDialog.order = result;
  846. wechatPayDialog.show = true;
  847. }
  848. };
  849. const getPayResult = async () => {
  850. const result: { state: number } = await axios.post('/api/order/pay/state', {
  851. id: wechatPayDialog.order.id || wechatPayDialog.order._id,
  852. });
  853. if (result && result.state) {
  854. return true;
  855. }
  856. };
  857. const onChargeSuccess = () => {
  858. MessagePlugin.success('支付成功!');
  859. wechatPayDialog.show = false;
  860. chargeDialog.show = false;
  861. };
  862. const onSearch = () => {
  863. debounce(searchGraphics, 300);
  864. };
  865. const searchGraphics = async () => {
  866. if (search.value) {
  867. activedPanels[activedGroup.value].splice(
  868. 0,
  869. activedPanels[activedGroup.value].length
  870. );
  871. }
  872. for (const group of subGroups.value) {
  873. for (const item of group.list) {
  874. if (search.value) {
  875. item.visible = searchObjectPinyin(item, 'name', search.value);
  876. } else {
  877. item.visible = true;
  878. }
  879. }
  880. if (search.value) {
  881. activedPanels[activedGroup.value].push(group.name);
  882. }
  883. }
  884. };
  885. const onFold = () => {
  886. if (!activedPanels[activedGroup.value]) {
  887. return;
  888. }
  889. if (activedPanels[activedGroup.value].length) {
  890. activedPanels[activedGroup.value] = [];
  891. } else {
  892. activedPanels[activedGroup.value] = [];
  893. for (const item of subGroups.value) {
  894. activedPanels[activedGroup.value].push(item.name);
  895. }
  896. }
  897. };
  898. const loadImage = (elem: any) => {
  899. if (elem.isSvg) {
  900. makeSvg(elem);
  901. if (activedGroup.value === '图元') {
  902. throttle(renderPngGroup, 100);
  903. }
  904. }
  905. };
  906. const renderPngGroup = () => {
  907. subGroups.value = pngs;
  908. };
  909. let uploadGroup: any;
  910. const onSelectFiles = (item: any) => {
  911. uploadGroup = item;
  912. };
  913. const fileSuccessed = async (content: any) => {
  914. const c: any = {
  915. name: filename(content.file.name),
  916. image: content.response.url,
  917. folder: uploadGroup.name === '我的组件' ? '' : uploadGroup.name,
  918. };
  919. const ret: any = await addCollection('le5leV-components', c);
  920. if (ret && uploadGroup.name !== '我的组件') {
  921. c._id = ret._id || ret.id;
  922. if (!uploadGroup.list) {
  923. uploadGroup.list = [];
  924. }
  925. uploadGroup.list.push(c);
  926. await axios.post('/api/data/folders/update', {
  927. _id: uploadGroup._id || uploadGroup.id,
  928. list: uploadGroup.list,
  929. });
  930. }
  931. };
  932. const delFolder = async (item: any) => {
  933. if (item.list?.length) {
  934. MessagePlugin.error('文件夹不为空!');
  935. return;
  936. }
  937. const id = item._id || item.id;
  938. const ret: any = await axios.post('/api/data/folders/delete', {
  939. id,
  940. });
  941. if (ret) {
  942. const i = subGroups.value.findIndex(
  943. (elem: any) => id === elem._id || id === elem.id
  944. );
  945. i >= 0 && subGroups.value.splice(i, 1);
  946. }
  947. };
  948. onMounted(() => {
  949. groupChange('场景');
  950. document.addEventListener('dragstart', dragstart, false);
  951. document.addEventListener('dragend', dragend, false);
  952. setTimeout(() => {
  953. meta2d.on('drop', drop);
  954. }, 2000);
  955. });
  956. onUnmounted(() => {
  957. document.removeEventListener('dragstart', dragstart);
  958. document.removeEventListener('dragend', dragend);
  959. meta2d.off('drop', drop);
  960. });
  961. </script>
  962. <style lang="postcss" scoped>
  963. .graphics {
  964. display: flex;
  965. flex-direction: column;
  966. background-color: var(--color-background);
  967. z-index: 2;
  968. .input-search {
  969. flex-shrink: 0;
  970. height: 40px;
  971. }
  972. .groups-panel {
  973. display: grid;
  974. grid-template-columns: 50px 1fr;
  975. border-top: 1px solid var(--color-background-input);
  976. flex-grow: 1;
  977. overflow-y: auto;
  978. font-size: 12px;
  979. z-index: 7;
  980. .groups {
  981. & > div {
  982. display: flex;
  983. flex-direction: column;
  984. align-items: center;
  985. padding: 16px 4px;
  986. line-height: 1;
  987. cursor: pointer;
  988. .t-icon {
  989. font-size: 20px;
  990. margin-bottom: 8px;
  991. }
  992. &:hover {
  993. color: var(--color-primary);
  994. }
  995. }
  996. .active {
  997. background-color: var(--color-background-active);
  998. color: var(--color-primary);
  999. border-left: 2px solid var(--color-primary);
  1000. }
  1001. }
  1002. .list {
  1003. overflow-y: auto;
  1004. max-height: calc(100vh - 100px);
  1005. background-color: var(--color-background-active);
  1006. padding-top: 8px;
  1007. * {
  1008. background-color: var(--color-background-active);
  1009. }
  1010. :deep(.t-collapse) {
  1011. min-height: 100vh;
  1012. border: none;
  1013. }
  1014. :deep(.t-collapse-panel__header) {
  1015. border: none;
  1016. font-size: 12px;
  1017. font-weight: 400;
  1018. padding: 8px 8px 8px 16px;
  1019. & > .t-space {
  1020. display: none;
  1021. }
  1022. & > .t-space:focus {
  1023. display: inline-flex;
  1024. }
  1025. &:hover .t-collapse-panel__icon,
  1026. &:hover > .flex {
  1027. color: var(--color-primary);
  1028. }
  1029. .t-collapse-panel__icon,
  1030. .t-collapse-panel__icon * {
  1031. transition: none;
  1032. }
  1033. .t-icon {
  1034. font-size: 13px;
  1035. }
  1036. }
  1037. :deep(.t-collapse-panel__wrapper:hover) {
  1038. .t-collapse-panel__header > .t-space {
  1039. display: inline-flex;
  1040. }
  1041. }
  1042. :deep(.t-collapse-panel__body) {
  1043. border: none;
  1044. }
  1045. :deep(.t-collapse-panel__content) {
  1046. background-color: var(--color-background-active);
  1047. padding: 4px 4px 20px 4px;
  1048. display: grid;
  1049. grid-template-columns: 1fr 1fr 1fr;
  1050. grid-row-gap: 12px;
  1051. }
  1052. :deep(.t-loading--center) {
  1053. width: 100px;
  1054. .t-loading__text {
  1055. margin-left: 8px;
  1056. height: 24px;
  1057. }
  1058. }
  1059. :deep(.t-image__error) {
  1060. .t-space-item:last-child {
  1061. display: none;
  1062. }
  1063. }
  1064. :deep(.t-image__loading) {
  1065. .t-space-item:last-child {
  1066. display: none;
  1067. }
  1068. }
  1069. .graphic {
  1070. position: relative;
  1071. padding: 10px 0;
  1072. border-radius: 2px;
  1073. border: 1px solid transparent;
  1074. &:hover {
  1075. cursor: pointer;
  1076. border-color: var(--color-primary);
  1077. }
  1078. p {
  1079. margin-top: 10px;
  1080. padding: 0 8px;
  1081. text-align: center;
  1082. font-size: 12px;
  1083. height: 12px;
  1084. line-height: 1;
  1085. overflow: hidden;
  1086. text-overflow: ellipsis;
  1087. display: -webkit-box;
  1088. -webkit-line-clamp: 1;
  1089. word-break: break-all;
  1090. -webkit-box-orient: vertical;
  1091. }
  1092. .t-image__wrapper {
  1093. height: 32px;
  1094. width: 32px;
  1095. margin: auto;
  1096. :deep(.t-image) {
  1097. border-radius: 2px;
  1098. }
  1099. }
  1100. svg {
  1101. color: var(--color);
  1102. height: 32px;
  1103. width: 100%;
  1104. margin: auto;
  1105. }
  1106. .svg-box {
  1107. height: 32px;
  1108. width: 32px;
  1109. margin-left: calc(50% - 16px);
  1110. margin-top: 10px;
  1111. margin-bottom: 10px;
  1112. &:deep(svg) {
  1113. height: 100%;
  1114. width: 100%;
  1115. .cls-1 {
  1116. stroke: var(--color) !important;
  1117. stroke-width: 4px;
  1118. fill: none;
  1119. stroke-dasharray: none;
  1120. opacity: 1;
  1121. }
  1122. }
  1123. }
  1124. .price {
  1125. position: absolute;
  1126. top: 8px;
  1127. right: 8px;
  1128. display: inline-block;
  1129. z-index: 1;
  1130. border-radius: 2px;
  1131. background-color: #ff400060;
  1132. color: var(--color-bland);
  1133. font-size: 10px;
  1134. line-height: 1;
  1135. padding: 3px;
  1136. }
  1137. }
  1138. }
  1139. .two-columns {
  1140. :deep(.t-collapse-panel__content) {
  1141. grid-template-columns: 1fr 1fr;
  1142. }
  1143. .graphic {
  1144. .t-image__wrapper {
  1145. width: 100px;
  1146. height: 56.25px;
  1147. background-color: var(--color-background);
  1148. }
  1149. }
  1150. }
  1151. }
  1152. .context-menu-box {
  1153. position: fixed;
  1154. z-index: 200;
  1155. & > div {
  1156. width: 140px !important;
  1157. position: static;
  1158. }
  1159. :deep(.t-menu) {
  1160. .t-menu__item {
  1161. &.t-is-opened {
  1162. background-color: var(--color-background-popup-hover);
  1163. transition: none !important;
  1164. }
  1165. }
  1166. .t-fake-arrow {
  1167. transform: rotate(-90deg) !important;
  1168. }
  1169. .t-fake-arrow--active {
  1170. transform: rotate(90deg) !important;
  1171. }
  1172. }
  1173. }
  1174. }
  1175. </style>