Header.vue 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  1. <template>
  2. <div class="app-header">
  3. <a class="logo" :href="assets.home" target="_blank">
  4. <img src="/favicon.ico" />
  5. <span>乐吾乐</span>
  6. </a>
  7. <t-dropdown
  8. :minColumnWidth="200"
  9. :maxHeight="560"
  10. :delay2="[10, 150]"
  11. overlayClassName="header-dropdown"
  12. trigger1="click"
  13. >
  14. <a> 文件 </a>
  15. <t-dropdown-menu>
  16. <t-dropdown-item @click="newFile">
  17. <a>新建文件</a>
  18. </t-dropdown-item>
  19. <t-dropdown-item @click="load(true)">
  20. <a>打开文件</a>
  21. </t-dropdown-item>
  22. <t-dropdown-item divider="true" @click="load">
  23. <a>导入文件</a>
  24. </t-dropdown-item>
  25. <t-dropdown-item>
  26. <a @click="save()">保存</a>
  27. </t-dropdown-item>
  28. <t-dropdown-item>
  29. <a @click="save(SaveType.SaveAs)">另保存</a>
  30. </t-dropdown-item>
  31. <t-dropdown-item divider="true">
  32. <a @click="downloadJson">下载JSON文件</a>
  33. </t-dropdown-item>
  34. <t-dropdown-item>
  35. <a @click="downloadZip">
  36. <div class="flex">
  37. 导出为ZIP文件 <span class="flex-grow"></span>
  38. <span><label>VIP</label></span>
  39. </div>
  40. </a>
  41. </t-dropdown-item>
  42. <t-dropdown-item>
  43. <a @click="downloadHtml">
  44. <div class="flex">
  45. 导出为HTML <span class="flex-grow"></span>
  46. <span><label>VIP</label></span>
  47. </div>
  48. </a>
  49. </t-dropdown-item>
  50. <t-dropdown-item>
  51. <a @click="downloadVue3">
  52. <div class="flex">
  53. 导出为Vue3组件 <span class="flex-grow"></span>
  54. <span><label>VIP</label></span>
  55. </div>
  56. </a>
  57. </t-dropdown-item>
  58. <t-dropdown-item>
  59. <a @click="downloadVue2">
  60. <div class="flex">
  61. 导出为Vue2组件 <span class="flex-grow"></span>
  62. <span><label>VIP</label></span>
  63. </div>
  64. </a>
  65. </t-dropdown-item>
  66. <t-dropdown-item divider="true">
  67. <a @click="downloadReact">
  68. <div class="flex">
  69. 导出为React组件 <span class="flex-grow"></span>
  70. <span><label>VIP</label></span>
  71. </div>
  72. </a>
  73. </t-dropdown-item>
  74. <t-dropdown-item>
  75. <a @click="downloadPng">下载为PNG</a>
  76. </t-dropdown-item>
  77. <t-dropdown-item>
  78. <a @click="downloadSvg">下载为SVG</a>
  79. </t-dropdown-item>
  80. </t-dropdown-menu>
  81. </t-dropdown>
  82. <t-dropdown
  83. :minColumnWidth="180"
  84. :maxHeight="500"
  85. :delay2="[10, 150]"
  86. overlayClassName="header-dropdown"
  87. >
  88. <a> 编辑 </a>
  89. <t-dropdown-menu>
  90. <t-dropdown-item>
  91. <a @click="onUndo">
  92. <div class="flex">
  93. 撤销 <span class="flex-grow"></span> Ctrl + Z
  94. </div>
  95. </a>
  96. </t-dropdown-item>
  97. <t-dropdown-item divider="true">
  98. <a @click="onRedo">
  99. <div class="flex">
  100. 恢复 <span class="flex-grow"></span> Ctrl + Y
  101. </div>
  102. </a>
  103. </t-dropdown-item>
  104. <t-dropdown-item>
  105. <a @click="onCut">
  106. <div class="flex">
  107. 剪切 <span class="flex-grow"></span> Ctrl + X
  108. </div>
  109. </a>
  110. </t-dropdown-item>
  111. <t-dropdown-item>
  112. <a @click="onCopy">
  113. <div class="flex">
  114. 复制 <span class="flex-grow"></span> Ctrl + C
  115. </div>
  116. </a>
  117. </t-dropdown-item>
  118. <t-dropdown-item divider="true">
  119. <a @click="onPaste">
  120. <div class="flex">
  121. 粘贴 <span class="flex-grow"></span> Ctrl + V
  122. </div>
  123. </a>
  124. </t-dropdown-item>
  125. <t-dropdown-item>
  126. <a @click="onAll">
  127. <div class="flex">
  128. 全选 <span class="flex-grow"></span> Ctrl + A
  129. </div>
  130. </a>
  131. </t-dropdown-item>
  132. <t-dropdown-item>
  133. <a @click="onDelete">
  134. <div class="flex">删除 <span class="flex-grow"></span> DELETE</div>
  135. </a>
  136. </t-dropdown-item>
  137. </t-dropdown-menu>
  138. </t-dropdown>
  139. <t-dropdown
  140. :minColumnWidth="180"
  141. :maxHeight="500"
  142. :delay2="[10, 150]"
  143. overlayClassName="header-dropdown"
  144. >
  145. <a> 工具 </a>
  146. <t-dropdown-menu>
  147. <t-dropdown-item>
  148. <a @click="onScaleWindow">窗口大小</a>
  149. </t-dropdown-item>
  150. <t-dropdown-item>
  151. <a @click="onScaleUp">放大</a>
  152. </t-dropdown-item>
  153. <t-dropdown-item>
  154. <a @click="onScaleDown">缩小</a>
  155. </t-dropdown-item>
  156. <t-dropdown-item divider="true">
  157. <a @click="onScaleView">100%视图</a>
  158. </t-dropdown-item>
  159. <t-dropdown-item>
  160. <a @click="showMap">
  161. <div class="flex middle">
  162. 鹰眼地图 <span class="flex-grow"></span>
  163. <t-icon v-show="map" name="check" />
  164. </div>
  165. </a>
  166. </t-dropdown-item>
  167. <t-dropdown-item divider="true">
  168. <a @click="showMagnifier">
  169. <div class="flex middle">
  170. 放大镜 <span class="flex-grow"></span>
  171. <t-icon v-show="magnifier" name="check" />
  172. </div>
  173. </a>
  174. </t-dropdown-item>
  175. <t-dropdown-item>
  176. <a @click="onAutoAnchor">
  177. <div class="flex middle">
  178. 自动锚点 <span class="flex-grow"></span>
  179. <t-icon v-show="autoAnchor" name="check" />
  180. </div>
  181. </a>
  182. </t-dropdown-item>
  183. <t-dropdown-item divider="true">
  184. <a @click="onDisableAnchor">
  185. <div class="flex middle">
  186. 显示锚点 <span class="flex-grow"></span>
  187. <t-icon v-show="showAnchor" name="check" />
  188. </div>
  189. </a>
  190. </t-dropdown-item>
  191. <t-dropdown-item divider="true">
  192. <a @click="onToggleAnchor">
  193. <div
  194. class="flex"
  195. :style="{
  196. color: showAnchor ? '' : '#4f5b75',
  197. }"
  198. >
  199. 添加/删除锚点 <span class="flex-grow"></span> A
  200. </div>
  201. </a>
  202. </t-dropdown-item>
  203. <t-dropdown-item>
  204. <a @click="switchTheme('light')">明亮主题</a>
  205. </t-dropdown-item>
  206. <t-dropdown-item>
  207. <a @click="switchTheme('dark')">暗黑主题</a>
  208. </t-dropdown-item>
  209. </t-dropdown-menu>
  210. </t-dropdown>
  211. <t-dropdown
  212. :minColumnWidth="180"
  213. :maxHeight="500"
  214. :delay2="[10, 150]"
  215. overlayClassName="header-dropdown"
  216. >
  217. <a> 帮助 </a>
  218. <t-dropdown-menu>
  219. <t-dropdown-item v-for="item in assets.helps" :divider="item.divider">
  220. <a :href="item.url" target="_blank">{{ item.name }}</a>
  221. </t-dropdown-item>
  222. </t-dropdown-menu>
  223. </t-dropdown>
  224. <input v-model="data.name" @input="onInputName" />
  225. <div style="width: 290px; flex-shrink: 0"></div>
  226. <t-dropdown
  227. v-if="user.id"
  228. :minColumnWidth="200"
  229. :maxHeight="500"
  230. :delay2="[10, 150]"
  231. overlayClassName="custom-dropdown header"
  232. >
  233. <a style="margin-left: 32px; margin-right: 12px">
  234. <t-avatar
  235. size="small"
  236. :image="user.avatarUrl ? user.avatarUrl : baseUrl + 'img/avatar.png'"
  237. />
  238. </a>
  239. <t-dropdown-menu>
  240. <t-dropdown-item divider="true">
  241. <a :href="assets.account">
  242. {{ user.username }}
  243. <label class="ml-16 vip-label">VIP</label>
  244. </a>
  245. </t-dropdown-item>
  246. <t-dropdown-item divider="true">
  247. <a :href="`${assets.account}/v`" target="_blank"> 我的大屏 </a>
  248. </t-dropdown-item>
  249. <t-dropdown-item>
  250. <a :href="`${assets.account}/account/teams`" target="_blank">
  251. 我的团队
  252. </a>
  253. </t-dropdown-item>
  254. <t-dropdown-item>
  255. <a :href="`${assets.account}/account/info`" target="_blank">
  256. 账号信息
  257. </a>
  258. </t-dropdown-item>
  259. <t-dropdown-item divider="true">
  260. <a :href="`${assets.account}/account/security`" target="_blank">
  261. 安全设置
  262. </a>
  263. </t-dropdown-item>
  264. <t-dropdown-item>
  265. <a @click="signout">退出</a>
  266. </t-dropdown-item>
  267. </t-dropdown-menu>
  268. </t-dropdown>
  269. <div class="flex middle" v-else>
  270. <a class="button primary solid" style="width: 80px" :href="login()">
  271. 登录
  272. </a>
  273. </div>
  274. </div>
  275. </template>
  276. <script lang="ts" setup>
  277. import { reactive, ref, onBeforeMount, onUnmounted, nextTick } from 'vue';
  278. import { useRouter, useRoute } from 'vue-router';
  279. import { useUser } from '@/services/user';
  280. import { MessagePlugin } from 'tdesign-vue-next';
  281. import {
  282. Meta2dBackData,
  283. dealwithFormatbeforeOpen,
  284. gotoAccount,
  285. checkData,
  286. } from '@/services/utils';
  287. import { readFile } from '@/services/file';
  288. import { compareVersion, baseVer, upgrade } from '@/services/upgrade';
  289. import { parseSvg } from '@meta2d/svg';
  290. import { Pen, getGlobalColor, isShowChild } from '@meta2d/core';
  291. import { cdn, upCdn } from '@/services/api';
  292. import JSZip from 'jszip';
  293. import axios from 'axios';
  294. import { switchTheme } from '@/services/theme';
  295. import { noLoginTip } from '@/services/utils';
  296. import {
  297. save,
  298. blank,
  299. newFile,
  300. SaveType,
  301. onScaleView,
  302. onScaleWindow,
  303. showMagnifier,
  304. showMap,
  305. drawPen,
  306. map,
  307. magnifier,
  308. useDot,
  309. delAttrs,
  310. useAssets,
  311. } from '@/services/common';
  312. const router = useRouter();
  313. const route = useRoute();
  314. const baseUrl = import.meta.env.BASE_URL || '/';
  315. const { assets, getAssets } = useAssets();
  316. const { user, signout } = useUser();
  317. const { setDot } = useDot();
  318. const data = reactive({
  319. name: '空白文件',
  320. });
  321. onBeforeMount(async () => {
  322. getAssets();
  323. });
  324. const onInputName = () => {
  325. (meta2d.store.data as Meta2dBackData).name = data.name;
  326. setDot();
  327. };
  328. const initMeta2dName = () => {
  329. data.name = (meta2d.store.data as Meta2dBackData).name || '';
  330. };
  331. nextTick(() => {
  332. meta2d.on('opened', initMeta2dName);
  333. });
  334. onUnmounted(() => {
  335. meta2d.off('opened', initMeta2dName);
  336. });
  337. const login = () => {
  338. return `${assets.account}?cb=${encodeURIComponent(location.href)}`;
  339. };
  340. function load(isNew = false) {
  341. const input = document.createElement('input');
  342. input.type = 'file';
  343. input.onchange = (event) => {
  344. const elem = event.target as HTMLInputElement;
  345. if (elem.files && elem.files[0]) {
  346. blank();
  347. // 路由跳转 可能在 openFile 后执行
  348. if (elem.files[0].name.endsWith('.json')) {
  349. openJson(elem.files[0]);
  350. if (isNew) {
  351. router.push({
  352. path: '/',
  353. query: {
  354. r: Date.now() + '',
  355. },
  356. });
  357. }
  358. } else if (elem.files[0].name.endsWith('.svg')) {
  359. MessagePlugin.info(
  360. '可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能'
  361. );
  362. openSvg(elem.files[0]);
  363. } else if (elem.files[0].name.endsWith('.zip')) {
  364. router.push({
  365. path: '/',
  366. query: {
  367. r: Date.now() + '',
  368. },
  369. });
  370. setTimeout(() => {
  371. openZip(elem.files[0]);
  372. }, 500);
  373. } else {
  374. MessagePlugin.info('打开文件只支持 json,svg,zip 格式');
  375. }
  376. }
  377. };
  378. input.click();
  379. }
  380. const openJson = async (file: File) => {
  381. const text = await readFile(file);
  382. try {
  383. let data: Meta2dBackData = JSON.parse(text);
  384. if (!data.name) {
  385. data.name = file.name.replace('.json', '');
  386. }
  387. if (!data.version || compareVersion(data.version, baseVer) === -1) {
  388. // 如果版本号不存在或者版本号 version < 1.0.0
  389. data = upgrade(data, baseVer);
  390. }
  391. dealwithFormatbeforeOpen(data);
  392. for (const k of delAttrs) {
  393. delete (data as any)[k];
  394. }
  395. meta2d.open(data);
  396. } catch (e) {
  397. console.log(e);
  398. }
  399. };
  400. const openSvg = async (file: File) => {
  401. const text = await readFile(file);
  402. const pens: Pen[] = parseSvg(text);
  403. meta2d.canvas.addCaches = pens;
  404. MessagePlugin.info('svg转换成功,请点击画布决定放置位置');
  405. };
  406. const openZip = async (file: File) => {
  407. if (!(user && user.id)) {
  408. MessagePlugin.warning(noLoginTip);
  409. return;
  410. }
  411. if (!user.isVip) {
  412. gotoAccount();
  413. return;
  414. }
  415. const { default: JSZip } = await import('jszip');
  416. const zip = new JSZip();
  417. await zip.loadAsync(file);
  418. let dataStr = '';
  419. for (const key in zip.files) {
  420. if (zip.files[key].dir) {
  421. continue;
  422. }
  423. if (key.endsWith('.json')) {
  424. // 认为只有一个 json 文件
  425. // dataStr = await zip.file(key).async('string');
  426. break;
  427. }
  428. }
  429. if (!dataStr) {
  430. return false;
  431. }
  432. for (const key in zip.files) {
  433. if (zip.files[key].dir) {
  434. continue;
  435. }
  436. // let _png = key.indexOf('/png');
  437. // let _img = key.indexOf('/img');
  438. // let _image = key.indexOf('/image');
  439. // let _file = key.indexOf('/file');
  440. let _keyLower = key.toLowerCase();
  441. // if (!key.endsWith('.json') && (_png !== -1 || _img !== -1 || _image !== -1 || _file !== -1)) {
  442. if (
  443. _keyLower.endsWith('.png') ||
  444. _keyLower.endsWith('.svg') ||
  445. _keyLower.endsWith('.gif') ||
  446. _keyLower.endsWith('.jpg') ||
  447. _keyLower.endsWith('.jpeg')
  448. ) {
  449. let filename = key.substr(key.lastIndexOf('/') + 1);
  450. const extPos = filename.lastIndexOf('.');
  451. let ext = '';
  452. if (extPos > 0) {
  453. ext = filename.substr(extPos);
  454. }
  455. filename = filename.substring(0, extPos > 8 ? 8 : extPos);
  456. // 上传文件
  457. const result: any = {};
  458. // await upload(
  459. // // await zip.file(key).async('blob'),
  460. // true,
  461. // filename + ext,
  462. // "/2D/未分类"
  463. // );
  464. let _key = key;
  465. // if (_png) {
  466. // _key = key.substring(_png);
  467. // } else if (_image) {
  468. // _key = key.substring(_png);
  469. // } else if (_img) {
  470. // _key = key.substring(_img);
  471. // } else if (_file) {
  472. // _key = key.substring(_file);
  473. // }
  474. if (result) {
  475. if (dataStr.replaceAll) {
  476. //'le5le.meta2d'
  477. dataStr = dataStr.replaceAll(_key.slice(12), result.url);
  478. } else {
  479. while (dataStr.includes(_key)) {
  480. dataStr = dataStr.replace(_key.slice(12), result.url);
  481. // 正则 gm 在特殊情况下报错,例如如下场景
  482. /**
  483. *    
  484. const data = '{"image":"/image/materials/IoT-Chemical(化学)/Air stripper 2(汽提塔2).svg"}';
  485. const key = '/image/materials/IoT-Chemical(化学)/Air stripper 2(汽提塔2).svg';
  486. data.replace(key, '123');
  487. data.replaceAll(key, '123')
  488. data.replace(new RegExp(key, 'gm'), '123');
  489. data.replace(new RegExp(key, 'g'), '123');
  490. */
  491. }
  492. }
  493. }
  494. }
  495. }
  496. try {
  497. let data: Meta2dBackData = JSON.parse(dataStr);
  498. if (data) {
  499. if (!data.name) {
  500. data.name = file.name.replace('.zip', '');
  501. }
  502. if (!data.version || compareVersion(data.version, baseVer) === -1) {
  503. // 如果版本号不存在或者版本号 version < 1.0.0
  504. data = upgrade(data, baseVer);
  505. }
  506. dealwithFormatbeforeOpen(data);
  507. const delAttrs = [
  508. 'userId',
  509. 'shared',
  510. 'team',
  511. 'owner',
  512. 'username',
  513. 'editor',
  514. 'editorId',
  515. 'editorName',
  516. 'createdAt',
  517. 'folder',
  518. 'image',
  519. 'id',
  520. '_id',
  521. 'view',
  522. 'updatedAt',
  523. 'star',
  524. 'recommend',
  525. ];
  526. for (const k of delAttrs) {
  527. delete (data as any)[k];
  528. }
  529. meta2d.open(data);
  530. }
  531. } catch (e) {
  532. return false;
  533. }
  534. };
  535. const downloadJson = () => {
  536. const data: Meta2dBackData = meta2d.data();
  537. if (data._id) delete data._id;
  538. checkData(data);
  539. import('file-saver').then(({ saveAs }) => {
  540. saveAs(
  541. new Blob(
  542. [JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')],
  543. {
  544. type: 'text/plain;charset=utf-8',
  545. }
  546. ),
  547. `${data.name || 'le5le.meta2d'}.json`
  548. );
  549. });
  550. };
  551. const downloadZip = async () => {
  552. if (!(user && user.id)) {
  553. MessagePlugin.warning(noLoginTip);
  554. return;
  555. }
  556. if (!user.isVip) {
  557. gotoAccount();
  558. return;
  559. }
  560. MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
  561. const [{ default: JSZip }, { saveAs }] = await Promise.all([
  562. import('jszip'),
  563. import('file-saver'),
  564. ]);
  565. const zip: any = new JSZip();
  566. const data: Meta2dBackData = meta2d.data();
  567. let _fileName =
  568. (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
  569. 'le5le.meta2d';
  570. const _zip = zip.folder(`${_fileName}`);
  571. if (data._id) delete data._id;
  572. checkData(data);
  573. _zip.file(
  574. `${_fileName}.json`,
  575. JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
  576. );
  577. await zipImages(_zip, meta2d.store.data.pens);
  578. const blob = await zip.generateAsync({ type: 'blob' });
  579. saveAs(blob, `${_fileName}.zip`);
  580. };
  581. const downloadHtml = async () => {
  582. if (!(user && user.id)) {
  583. MessagePlugin.warning(noLoginTip);
  584. return;
  585. }
  586. if (!user.isVip) {
  587. gotoAccount();
  588. return;
  589. }
  590. MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
  591. const data: Meta2dBackData = meta2d.data();
  592. if (data._id) delete data._id;
  593. checkData(data);
  594. const [{ default: JSZip }, { saveAs }] = await Promise.all([
  595. import('jszip'),
  596. import('file-saver'),
  597. ]);
  598. const zip = new JSZip();
  599. let _fileName =
  600. (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
  601. 'le5le.meta2d';
  602. //处理cdn图片地址
  603. const _zip: any = zip.folder(`${_fileName}`);
  604. _zip.file(
  605. 'data.json',
  606. JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
  607. );
  608. await Promise.all([zipImages(_zip, meta2d.store.data.pens), zipFiles(_zip)]);
  609. const blob = await zip.generateAsync({ type: 'blob' });
  610. saveAs(blob, `${_fileName}.zip`);
  611. };
  612. enum Frame {
  613. vue2,
  614. vue3,
  615. react,
  616. }
  617. const downloadVue3 = async () => {
  618. downloadAsFrame(Frame.vue3);
  619. };
  620. const downloadVue2 = async () => {
  621. downloadAsFrame(Frame.vue2);
  622. };
  623. const downloadReact = async () => {
  624. downloadAsFrame(Frame.react);
  625. };
  626. async function downloadAsFrame(type: Frame) {
  627. if (!(user && user.id)) {
  628. MessagePlugin.warning(noLoginTip);
  629. return;
  630. }
  631. if (!user.isVip) {
  632. gotoAccount();
  633. return;
  634. }
  635. MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
  636. const data: Meta2dBackData = meta2d.data();
  637. if (data._id) delete data._id;
  638. checkData(data);
  639. const [{ default: JSZip }, { saveAs }] = await Promise.all([
  640. import('jszip'),
  641. import('file-saver'),
  642. ]);
  643. const zip = new JSZip();
  644. let _fileName =
  645. (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
  646. 'le5le.meta2d';
  647. const _zip: any = zip.folder(`${_fileName}`);
  648. _zip.file(
  649. 'data.json',
  650. JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
  651. );
  652. await Promise.all([
  653. zipImages(_zip, meta2d.store.data.pens),
  654. type === Frame.vue3
  655. ? zipVue3Files(_zip)
  656. : type === Frame.vue2
  657. ? zipVue2Files(_zip)
  658. : zipReactFiles(_zip),
  659. ]);
  660. const blob = await zip.generateAsync({ type: 'blob' });
  661. saveAs(blob, `${_fileName}.zip`);
  662. }
  663. async function zipVue3Files(zip: JSZip) {
  664. const files = [
  665. '/view/js/marked.min.js',
  666. '/view/js/lcjs.iife.js',
  667. '/view/vue3/Meta2d.vue',
  668. '/view/index.html',
  669. '/view/js/meta2d.js',
  670. '/view/使用说明.md',
  671. ] as const;
  672. // 文件同时加载
  673. await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
  674. }
  675. async function zipVue2Files(zip: JSZip) {
  676. const files = [
  677. '/view/js/marked.min.js',
  678. '/view/js/lcjs.iife.js',
  679. '/view/vue2/Meta2d.vue',
  680. '/view/index.html',
  681. '/view/js/meta2d.js',
  682. '/view/使用说明.md',
  683. ] as const;
  684. // 文件同时加载
  685. await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
  686. }
  687. async function zipReactFiles(zip: JSZip) {
  688. const files = [
  689. '/view/js/marked.min.js',
  690. '/view/js/lcjs.iife.js',
  691. '/view/react/Meta2d.jsx',
  692. '/view/react/Meta2d.css',
  693. '/view/index.html',
  694. '/view/js/meta2d.js',
  695. '/view/使用说明.md',
  696. ] as const;
  697. // 文件同时加载
  698. await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
  699. }
  700. async function zipFiles(zip: JSZip) {
  701. const files = [
  702. '/view/js/marked.min.js',
  703. '/view/js/lcjs.iife.js',
  704. '/view/js/index.js',
  705. '/view/js/meta2d.js',
  706. '/view/index.html',
  707. '/view/index.css',
  708. '/view/favicon.ico',
  709. '/view/使用说明.pdf',
  710. ] as const;
  711. // 文件同时加载
  712. await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
  713. }
  714. async function zipFile(zip: JSZip, filePath: string) {
  715. const res: Blob = await axios.get((cdn ? cdn + '/2d' : '') + filePath, {
  716. responseType: 'blob',
  717. });
  718. zip.file(filePath.replace('/view', ''), res, { createFolders: true });
  719. }
  720. /**
  721. * 图片放到 zip 里
  722. * @param pens 可以是非具有 calculative 的 pen
  723. */
  724. async function zipImages(zip: JSZip, pens: Pen[]) {
  725. if (!pens) {
  726. return;
  727. }
  728. // 不止 image 上有图片, strokeImage ,backgroundImage 也有图片
  729. const imageKeys = [
  730. {
  731. string: 'image',
  732. },
  733. { string: 'strokeImage' },
  734. { string: 'backgroundImage' },
  735. ] as const;
  736. const images: string[] = [];
  737. for (const pen of pens) {
  738. for (const i of imageKeys) {
  739. const image = pen[i.string];
  740. if (image) {
  741. // HTMLImageElement 无法精确控制图片格式
  742. if (
  743. image.startsWith('/') ||
  744. image.startsWith(cdn) ||
  745. image.startsWith(upCdn)
  746. ) {
  747. // 只考虑相对路径下的 image ,绝对路径图片无需下载
  748. if (!images.includes(image)) {
  749. images.push(image);
  750. }
  751. }
  752. }
  753. }
  754. // 无需递归遍历子节点,现在所有的节点都在外层
  755. }
  756. await Promise.all(images.map((image) => zipImage(zip, image)));
  757. }
  758. async function zipImage(zip: JSZip, image: string) {
  759. const res: Blob = await axios.get(image, {
  760. responseType: 'blob',
  761. params: {
  762. isZip: true,
  763. },
  764. });
  765. zip.file(cdn ? image.replace(cdn, '').replace(upCdn, '') : image, res, {
  766. createFolders: true,
  767. });
  768. }
  769. const downloadImageTips =
  770. '无法下载,宽度不合法,画布可能没有画笔/画布大小超出浏览器最大限制';
  771. const downloadPng = () => {
  772. const name = (meta2d.store.data as Meta2dBackData).name;
  773. try {
  774. meta2d.downloadPng(name ? name + '.png' : undefined);
  775. } catch (e) {
  776. MessagePlugin.warning(downloadImageTips);
  777. }
  778. };
  779. async function getIconDefs(url: string) {
  780. let res: any = await axios.get(url);
  781. let str = res.match(/@font-face([\s\S]*?)\}/)[1];
  782. str = `@font-face ${str} }`;
  783. return str;
  784. }
  785. const downloadSvg = async () => {
  786. await import('@/assets/canvas2svg');
  787. if (!C2S) {
  788. MessagePlugin.error('请先加载乐吾乐官网下的canvas2svg.js');
  789. return;
  790. }
  791. const rect: any = meta2d.getRect();
  792. if (!isFinite(rect.width)) {
  793. MessagePlugin.error(downloadImageTips);
  794. return;
  795. }
  796. rect.x -= 10;
  797. rect.y -= 10;
  798. const ctx = new C2S(rect.width + 20, rect.height + 20);
  799. ctx.textBaseline = 'middle';
  800. ctx.strokeStyle = getGlobalColor(meta2d.store);
  801. for (const pen of meta2d.store.data.pens) {
  802. // 不使用 calculative.inView 的原因是,如果 pen 在 view 之外,那么它的 calculative.inView 为 false,但是它的绘制还是需要的
  803. if (!isShowChild(pen, meta2d.store) || pen.visible == false) {
  804. continue;
  805. }
  806. meta2d.renderPenRaw(ctx, pen, rect);
  807. }
  808. let mySerializedSVG = ctx.getSerializedSvg();
  809. let icon_pens = meta2d.store.data.pens.filter(
  810. (item) => item.iconFamily && item.icon
  811. );
  812. if (icon_pens && icon_pens.length > 0) {
  813. let iconList = [
  814. '/icon/国家电网/iconfont.css',
  815. '/icon/电气工程/iconfont.css',
  816. '/icon/通用图标/iconfont.css',
  817. ];
  818. let defsList: any = await Promise.all(
  819. iconList.map((item) => getIconDefs(item))
  820. );
  821. mySerializedSVG = mySerializedSVG.replace(
  822. '<defs/>',
  823. `<defs>
  824. <style type="text/css">
  825. ${defsList.join('\n')}
  826. </style>
  827. {{bk}}
  828. </defs>
  829. {{bkRect}}`
  830. );
  831. }
  832. /* mySerializedSVG = mySerializedSVG.replace(
  833. '<defs/>',
  834. `<defs>
  835. <style type="text/css">
  836. @font-face {
  837. font-family: 'ticon';
  838. src: url('icon/通用图标/iconfont.ttf') format('truetype');
  839. }
  840. </style>
  841. {{bk}}
  842. </defs>
  843. {{bkRect}}`
  844. );
  845. */
  846. if (meta2d.store.data.background) {
  847. mySerializedSVG = mySerializedSVG.replace('{{bk}}', '');
  848. mySerializedSVG = mySerializedSVG.replace(
  849. '{{bkRect}}',
  850. `<rect x="0" y="0" width="100%" height="100%" fill="${meta2d.store.data.background}"></rect>`
  851. );
  852. } else {
  853. mySerializedSVG = mySerializedSVG.replace('{{bk}}', '');
  854. mySerializedSVG = mySerializedSVG.replace('{{bkRect}}', '');
  855. }
  856. mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, '&#x');
  857. const urlObject: any = (window as any).URL || window;
  858. const export_blob = new Blob([mySerializedSVG]);
  859. const url = urlObject.createObjectURL(export_blob);
  860. const a = document.createElement('a');
  861. a.setAttribute(
  862. 'download',
  863. `${(meta2d.store.data as Meta2dBackData).name || 'le5le.meta2d'}.svg`
  864. );
  865. a.setAttribute('href', url);
  866. const evt = document.createEvent('MouseEvents');
  867. evt.initEvent('click', true, true);
  868. a.dispatchEvent(evt);
  869. };
  870. const onUndo = () => {
  871. meta2d.undo();
  872. };
  873. const onRedo = () => {
  874. meta2d.redo();
  875. };
  876. const onCut = () => {
  877. meta2d.cut();
  878. };
  879. const onCopy = () => {
  880. meta2d.copy();
  881. };
  882. const onPaste = () => {
  883. meta2d.paste();
  884. };
  885. const onAll = () => {
  886. meta2d.activeAll();
  887. };
  888. const onDelete = () => {
  889. meta2d.delete();
  890. };
  891. const onToggleAnchor = () => {
  892. //取消连线状态
  893. // meta2d.store.options.disableAnchor = false;
  894. if (!meta2d.store.options.disableAnchor) {
  895. meta2d.canvas.drawingLineName && drawPen();
  896. meta2d.toggleAnchorMode();
  897. }
  898. };
  899. const onAddAnchorHand = () => {
  900. meta2d.addAnchorHand();
  901. };
  902. const onRemoveAnchorHand = () => {
  903. meta2d.removeAnchorHand();
  904. };
  905. const onToggleAnchorHand = () => {
  906. meta2d.toggleAnchorHand();
  907. };
  908. const onScaleUp = () => {
  909. const _scale = meta2d.store.data.scale + 0.1;
  910. meta2d.scale(_scale);
  911. };
  912. const onScaleDown = () => {
  913. const _scale = meta2d.store.data.scale - 0.1;
  914. meta2d.scale(_scale);
  915. };
  916. const autoAnchor = ref(true);
  917. const onAutoAnchor = () => {
  918. meta2d.store.options.autoAnchor = !meta2d.store.options.autoAnchor;
  919. autoAnchor.value = meta2d.store.options.autoAnchor;
  920. };
  921. const showAnchor = ref(false);
  922. const onDisableAnchor = () => {
  923. meta2d.store.options.disableAnchor = !meta2d.store.options.disableAnchor;
  924. changeDisableAnchor();
  925. };
  926. const changeDisableAnchor = () => {
  927. const { disableAnchor, autoAnchor } = meta2d.store.options;
  928. showAnchor.value = !disableAnchor || false;
  929. if (disableAnchor && autoAnchor) {
  930. // 禁用瞄点开了,需要关闭自动瞄点
  931. onAutoAnchor();
  932. }
  933. };
  934. </script>
  935. <style lang="postcss" scoped>
  936. .app-header {
  937. display: flex;
  938. height: 40px;
  939. .logo {
  940. display: flex;
  941. padding: 0 16px;
  942. align-items: center;
  943. font-size: 14px;
  944. font-weight: 500;
  945. img {
  946. height: 20px;
  947. margin-right: 6px;
  948. }
  949. }
  950. a {
  951. display: flex;
  952. padding: 0 8px;
  953. margin: 0 8px;
  954. align-items: center;
  955. color: var(--color);
  956. text-decoration: none;
  957. &:hover {
  958. color: var(--color-primary);
  959. }
  960. }
  961. input {
  962. font-size: var(--font-size);
  963. flex-grow: 1;
  964. background: none;
  965. outline: none;
  966. border: none;
  967. text-align: center;
  968. color: var(--color-title);
  969. }
  970. }
  971. </style>