Header.vue 30 KB

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