Header.vue 30 KB

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