Header.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116
  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="onScaleFull">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. <div style="width: 148px; flex-shrink: 0"></div>
  225. <input v-model="data.name" @input="onInputName" />
  226. <a :href="assets.account" target="_blank">
  227. <t-icon name="home" />
  228. 账户中心
  229. </a>
  230. <a :href="assets.account" target="_blank" class="active">
  231. <t-icon name="desktop" />
  232. 大屏可视化
  233. </a>
  234. <a :href="assets['3d']" target="_blank">
  235. <t-icon name="control-platform" />
  236. 3D可视化
  237. </a>
  238. <a :href="assets['2d']" target="_blank">
  239. <t-icon name="app" />
  240. 2D可视化
  241. </a>
  242. <t-dropdown
  243. v-if="user.id"
  244. :minColumnWidth="200"
  245. :maxHeight="500"
  246. :delay2="[10, 150]"
  247. overlayClassName="custom-dropdown header"
  248. >
  249. <a style="margin-left: 32px; margin-right: 12px">
  250. <t-avatar
  251. size="small"
  252. :image="user.avatarUrl ? user.avatarUrl : baseUrl + 'img/avatar.png'"
  253. />
  254. </a>
  255. <t-dropdown-menu>
  256. <t-dropdown-item divider="true">
  257. <a :href="assets.account">
  258. {{ user.username }}
  259. <label
  260. class="ml-16 vip-label"
  261. :style="{
  262. color: user.limit > 1 ? '#ff4000' : '#D5C781',
  263. background: user.limit > 1 ? '#ff400030' : '#D5C78133',
  264. }"
  265. >VIP</label
  266. >
  267. </a>
  268. </t-dropdown-item>
  269. <t-dropdown-item divider="true">
  270. <a :href="`${assets.account}/v`" target="_blank"> 我的大屏 </a>
  271. </t-dropdown-item>
  272. <t-dropdown-item>
  273. <a :href="`${assets.account}/account/teams`" target="_blank">
  274. 我的团队
  275. </a>
  276. </t-dropdown-item>
  277. <t-dropdown-item>
  278. <a :href="`${assets.account}/account/info`" target="_blank">
  279. 账号信息
  280. </a>
  281. </t-dropdown-item>
  282. <t-dropdown-item divider="true">
  283. <a :href="`${assets.account}/account/security`" target="_blank">
  284. 安全设置
  285. </a>
  286. </t-dropdown-item>
  287. <t-dropdown-item>
  288. <a @click="logout">退出</a>
  289. </t-dropdown-item>
  290. </t-dropdown-menu>
  291. </t-dropdown>
  292. <div class="flex middle" v-else>
  293. <a class="button primary solid" style="width: 80px" :href="login()">
  294. 登录
  295. </a>
  296. </div>
  297. </div>
  298. </template>
  299. <script lang="ts" setup>
  300. import { reactive, ref, onBeforeMount, onUnmounted, nextTick } from 'vue';
  301. import { useRouter, useRoute } from 'vue-router';
  302. import { useUser } from '@/services/user';
  303. import { MessagePlugin } from 'tdesign-vue-next';
  304. import {
  305. Meta2dBackData,
  306. dealwithFormatbeforeOpen,
  307. gotoAccount,
  308. checkData,
  309. } from '@/services/utils';
  310. import { readFile } from '@/services/file';
  311. import { compareVersion, baseVer, upgrade } from '@/services/upgrade';
  312. import { parseSvg } from '@meta2d/svg';
  313. import { Pen, getGlobalColor, isShowChild } from '@meta2d/core';
  314. import { cdn, upCdn } from '@/services/api';
  315. import JSZip from 'jszip';
  316. import axios from 'axios';
  317. import { switchTheme } from '@/services/theme';
  318. import { noLoginTip } from '@/services/utils';
  319. import {
  320. save,
  321. blank,
  322. newFile,
  323. SaveType,
  324. onScaleFull,
  325. onScaleWindow,
  326. showMagnifier,
  327. showMap,
  328. drawPen,
  329. map,
  330. magnifier,
  331. useDot,
  332. delAttrs,
  333. useAssets,
  334. } from '@/services/common';
  335. const router = useRouter();
  336. const route = useRoute();
  337. const baseUrl = import.meta.env.BASE_URL || '/';
  338. const { assets, getAssets } = useAssets();
  339. const { user, signout } = useUser();
  340. const { setDot } = useDot();
  341. const data = reactive({
  342. name: '空白文件',
  343. });
  344. onBeforeMount(async () => {
  345. getAssets();
  346. });
  347. const logout = () => {
  348. signout();
  349. meta2d.emit('logout');
  350. };
  351. const onInputName = () => {
  352. (meta2d.store.data as Meta2dBackData).name = data.name;
  353. setDot();
  354. };
  355. const initMeta2dName = () => {
  356. data.name = (meta2d.store.data as Meta2dBackData).name || '';
  357. };
  358. nextTick(() => {
  359. meta2d.on('opened', initMeta2dName);
  360. });
  361. onUnmounted(() => {
  362. meta2d.off('opened', initMeta2dName);
  363. });
  364. const login = () => {
  365. return `${assets.account}?cb=${encodeURIComponent(location.href)}`;
  366. };
  367. function load(isNew = false) {
  368. const input = document.createElement('input');
  369. input.type = 'file';
  370. input.onchange = (event) => {
  371. const elem = event.target as HTMLInputElement;
  372. if (elem.files && elem.files[0]) {
  373. blank();
  374. // 路由跳转 可能在 openFile 后执行
  375. if (elem.files[0].name.endsWith('.json')) {
  376. openJson(elem.files[0]);
  377. if (isNew) {
  378. router.push({
  379. path: '/',
  380. query: {
  381. r: Date.now() + '',
  382. },
  383. });
  384. }
  385. } else if (elem.files[0].name.endsWith('.svg')) {
  386. MessagePlugin.info(
  387. '可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能'
  388. );
  389. openSvg(elem.files[0]);
  390. } else if (elem.files[0].name.endsWith('.zip')) {
  391. router.push({
  392. path: '/',
  393. query: {
  394. r: Date.now() + '',
  395. },
  396. });
  397. setTimeout(() => {
  398. openZip(elem.files[0]);
  399. }, 500);
  400. } else {
  401. MessagePlugin.info('打开文件只支持 json,svg,zip 格式');
  402. }
  403. }
  404. };
  405. input.click();
  406. }
  407. const openJson = async (file: File) => {
  408. const text = await readFile(file);
  409. try {
  410. let data: Meta2dBackData = JSON.parse(text);
  411. if (!data.name) {
  412. data.name = file.name.replace('.json', '');
  413. }
  414. if (!data.version || compareVersion(data.version, baseVer) === -1) {
  415. // 如果版本号不存在或者版本号 version < 1.0.0
  416. data = upgrade(data, baseVer);
  417. }
  418. dealwithFormatbeforeOpen(data);
  419. for (const k of delAttrs) {
  420. delete (data as any)[k];
  421. }
  422. meta2d.open(data);
  423. } catch (e) {
  424. console.error(e);
  425. }
  426. };
  427. const openSvg = async (file: File) => {
  428. const text = await readFile(file);
  429. const pens: Pen[] = parseSvg(text);
  430. meta2d.canvas.addCaches = pens;
  431. MessagePlugin.info('svg转换成功,请点击画布决定放置位置');
  432. };
  433. const openZip = async (file: File) => {
  434. if (!(user && user.id)) {
  435. MessagePlugin.warning(noLoginTip);
  436. return;
  437. }
  438. if (!user.isVip) {
  439. gotoAccount();
  440. return;
  441. }
  442. const { default: JSZip } = await import('jszip');
  443. const zip = new JSZip();
  444. await zip.loadAsync(file);
  445. let dataStr = '';
  446. for (const key in zip.files) {
  447. if (zip.files[key].dir) {
  448. continue;
  449. }
  450. if (key.endsWith('.json')) {
  451. // 认为只有一个 json 文件
  452. // dataStr = await zip.file(key).async('string');
  453. break;
  454. }
  455. }
  456. if (!dataStr) {
  457. return false;
  458. }
  459. for (const key in zip.files) {
  460. if (zip.files[key].dir) {
  461. continue;
  462. }
  463. // let _png = key.indexOf('/png');
  464. // let _img = key.indexOf('/img');
  465. // let _image = key.indexOf('/image');
  466. // let _file = key.indexOf('/file');
  467. let _keyLower = key.toLowerCase();
  468. // if (!key.endsWith('.json') && (_png !== -1 || _img !== -1 || _image !== -1 || _file !== -1)) {
  469. if (
  470. _keyLower.endsWith('.png') ||
  471. _keyLower.endsWith('.svg') ||
  472. _keyLower.endsWith('.gif') ||
  473. _keyLower.endsWith('.jpg') ||
  474. _keyLower.endsWith('.jpeg')
  475. ) {
  476. let filename = key.substr(key.lastIndexOf('/') + 1);
  477. const extPos = filename.lastIndexOf('.');
  478. let ext = '';
  479. if (extPos > 0) {
  480. ext = filename.substr(extPos);
  481. }
  482. filename = filename.substring(0, extPos > 8 ? 8 : extPos);
  483. // 上传文件
  484. const result: any = {};
  485. // await upload(
  486. // // await zip.file(key).async('blob'),
  487. // true,
  488. // filename + ext,
  489. // "/2D/默认"
  490. // );
  491. let _key = key;
  492. // if (_png) {
  493. // _key = key.substring(_png);
  494. // } else if (_image) {
  495. // _key = key.substring(_png);
  496. // } else if (_img) {
  497. // _key = key.substring(_img);
  498. // } else if (_file) {
  499. // _key = key.substring(_file);
  500. // }
  501. if (result) {
  502. if (dataStr.replaceAll) {
  503. //'le5le.meta2d'
  504. dataStr = dataStr.replaceAll(_key.slice(12), result.url);
  505. } else {
  506. while (dataStr.includes(_key)) {
  507. dataStr = dataStr.replace(_key.slice(12), result.url);
  508. // 正则 gm 在特殊情况下报错,例如如下场景
  509. /**
  510. *    
  511. const data = '{"image":"/image/materials/IoT-Chemical(化学)/Air stripper 2(汽提塔2).svg"}';
  512. const key = '/image/materials/IoT-Chemical(化学)/Air stripper 2(汽提塔2).svg';
  513. data.replace(key, '123');
  514. data.replaceAll(key, '123')
  515. data.replace(new RegExp(key, 'gm'), '123');
  516. data.replace(new RegExp(key, 'g'), '123');
  517. */
  518. }
  519. }
  520. }
  521. }
  522. }
  523. try {
  524. let data: Meta2dBackData = JSON.parse(dataStr);
  525. if (data) {
  526. if (!data.name) {
  527. data.name = file.name.replace('.zip', '');
  528. }
  529. if (!data.version || compareVersion(data.version, baseVer) === -1) {
  530. // 如果版本号不存在或者版本号 version < 1.0.0
  531. data = upgrade(data, baseVer);
  532. }
  533. dealwithFormatbeforeOpen(data);
  534. const delAttrs = [
  535. 'userId',
  536. 'shared',
  537. 'team',
  538. 'owner',
  539. 'username',
  540. 'editor',
  541. 'editorId',
  542. 'editorName',
  543. 'createdAt',
  544. 'folder',
  545. 'image',
  546. 'id',
  547. '_id',
  548. 'view',
  549. 'updatedAt',
  550. 'star',
  551. 'recommend',
  552. ];
  553. for (const k of delAttrs) {
  554. delete (data as any)[k];
  555. }
  556. meta2d.open(data);
  557. }
  558. } catch (e) {
  559. return false;
  560. }
  561. };
  562. const downloadJson = () => {
  563. const data: Meta2dBackData = meta2d.data();
  564. if (data._id) delete data._id;
  565. checkData(data);
  566. import('file-saver').then(({ saveAs }) => {
  567. saveAs(
  568. new Blob(
  569. [JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')],
  570. {
  571. type: 'text/plain;charset=utf-8',
  572. }
  573. ),
  574. `${data.name || 'le5le.meta2d'}.json`
  575. );
  576. });
  577. };
  578. const downloadZip = async () => {
  579. if (!(user && user.id)) {
  580. MessagePlugin.warning(noLoginTip);
  581. return;
  582. }
  583. if (!user.isVip) {
  584. gotoAccount();
  585. return;
  586. }
  587. MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
  588. const [{ default: JSZip }, { saveAs }] = await Promise.all([
  589. import('jszip'),
  590. import('file-saver'),
  591. ]);
  592. const zip: any = new JSZip();
  593. const data: Meta2dBackData = meta2d.data();
  594. let _fileName =
  595. (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
  596. 'le5le.meta2d';
  597. const _zip = zip.folder(`${_fileName}`);
  598. if (data._id) delete data._id;
  599. checkData(data);
  600. _zip.file(
  601. `${_fileName}.json`,
  602. JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
  603. );
  604. await zipBkImg(_zip);
  605. await zipImages(_zip, meta2d.store.data.pens);
  606. const blob = await zip.generateAsync({ type: 'blob' });
  607. saveAs(blob, `${_fileName}.zip`);
  608. };
  609. const downloadHtml = async () => {
  610. if (!(user && user.id)) {
  611. MessagePlugin.warning(noLoginTip);
  612. return;
  613. }
  614. if (!user.isVip) {
  615. gotoAccount();
  616. return;
  617. }
  618. MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
  619. const data: Meta2dBackData = meta2d.data();
  620. if (data._id) delete data._id;
  621. checkData(data);
  622. const [{ default: JSZip }, { saveAs }] = await Promise.all([
  623. import('jszip'),
  624. import('file-saver'),
  625. ]);
  626. const zip = new JSZip();
  627. let _fileName =
  628. (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
  629. 'le5le.meta2d';
  630. //处理cdn图片地址
  631. const _zip: any = zip.folder(`${_fileName}`);
  632. _zip.file(
  633. 'data.json',
  634. JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
  635. );
  636. await Promise.all([
  637. zipBkImg(_zip),
  638. zipImages(_zip, meta2d.store.data.pens),
  639. zipFiles(_zip),
  640. ]);
  641. const blob = await zip.generateAsync({ type: 'blob' });
  642. saveAs(blob, `${_fileName}.zip`);
  643. };
  644. async function zipBkImg(zip: JSZip) {
  645. let img = meta2d.store.data.bkImage;
  646. if (img) {
  647. if (img.startsWith('/') || img.startsWith(cdn) || img.startsWith(upCdn)) {
  648. zipImage(zip, img);
  649. }
  650. }
  651. }
  652. enum Frame {
  653. vue2,
  654. vue3,
  655. react,
  656. }
  657. const downloadVue3 = async () => {
  658. downloadAsFrame(Frame.vue3);
  659. };
  660. const downloadVue2 = async () => {
  661. downloadAsFrame(Frame.vue2);
  662. };
  663. const downloadReact = async () => {
  664. downloadAsFrame(Frame.react);
  665. };
  666. async function downloadAsFrame(type: Frame) {
  667. if (!(user && user.id)) {
  668. MessagePlugin.warning(noLoginTip);
  669. return;
  670. }
  671. if (!user.isVip) {
  672. gotoAccount();
  673. return;
  674. }
  675. MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
  676. const data: Meta2dBackData = meta2d.data();
  677. if (data._id) delete data._id;
  678. checkData(data);
  679. const [{ default: JSZip }, { saveAs }] = await Promise.all([
  680. import('jszip'),
  681. import('file-saver'),
  682. ]);
  683. const zip = new JSZip();
  684. let _fileName =
  685. (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
  686. 'le5le.meta2d';
  687. const _zip: any = zip.folder(`${_fileName}`);
  688. _zip.file(
  689. 'data.json',
  690. JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
  691. );
  692. await Promise.all([
  693. zipBkImg(_zip),
  694. zipImages(_zip, meta2d.store.data.pens),
  695. type === Frame.vue3
  696. ? zipVue3Files(_zip)
  697. : type === Frame.vue2
  698. ? zipVue2Files(_zip)
  699. : zipReactFiles(_zip),
  700. ]);
  701. const blob = await zip.generateAsync({ type: 'blob' });
  702. saveAs(blob, `${_fileName}.zip`);
  703. }
  704. async function zipVue3Files(zip: JSZip) {
  705. const files = [
  706. '/view/js/marked.min.js',
  707. '/view/js/lcjs.iife.js',
  708. '/view/vue3/Meta2d.vue',
  709. '/view/index.html',
  710. '/view/js/meta2d.js',
  711. '/view/使用说明.md',
  712. ] as const;
  713. // 文件同时加载
  714. await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
  715. }
  716. async function zipVue2Files(zip: JSZip) {
  717. const files = [
  718. '/view/js/marked.min.js',
  719. '/view/js/lcjs.iife.js',
  720. '/view/vue2/Meta2d.vue',
  721. '/view/index.html',
  722. '/view/js/meta2d.js',
  723. '/view/使用说明.md',
  724. ] as const;
  725. // 文件同时加载
  726. await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
  727. }
  728. async function zipReactFiles(zip: JSZip) {
  729. const files = [
  730. '/view/js/marked.min.js',
  731. '/view/js/lcjs.iife.js',
  732. '/view/react/Meta2d.jsx',
  733. '/view/react/Meta2d.css',
  734. '/view/index.html',
  735. '/view/js/meta2d.js',
  736. '/view/使用说明.md',
  737. ] as const;
  738. // 文件同时加载
  739. await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
  740. }
  741. async function zipFiles(zip: JSZip) {
  742. const files = [
  743. '/view/js/marked.min.js',
  744. '/view/js/lcjs.iife.js',
  745. '/view/js/index.js',
  746. '/view/js/meta2d.js',
  747. '/view/index.html',
  748. '/view/index.css',
  749. '/view/favicon.ico',
  750. '/view/使用说明.pdf',
  751. ] as const;
  752. // 文件同时加载
  753. await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
  754. }
  755. async function zipFile(zip: JSZip, filePath: string) {
  756. const res: Blob = await axios.get((cdn ? cdn + '/2d' : '') + filePath, {
  757. responseType: 'blob',
  758. });
  759. zip.file(filePath.replace('/view', ''), res, { createFolders: true });
  760. }
  761. /**
  762. * 图片放到 zip 里
  763. * @param pens 可以是非具有 calculative 的 pen
  764. */
  765. async function zipImages(zip: JSZip, pens: Pen[]) {
  766. if (!pens) {
  767. return;
  768. }
  769. // 不止 image 上有图片, strokeImage ,backgroundImage 也有图片
  770. const imageKeys = [
  771. {
  772. string: 'image',
  773. },
  774. { string: 'strokeImage' },
  775. { string: 'backgroundImage' },
  776. ] as const;
  777. const images: string[] = [];
  778. for (const pen of pens) {
  779. for (const i of imageKeys) {
  780. const image = pen[i.string];
  781. if (image) {
  782. // HTMLImageElement 无法精确控制图片格式
  783. if (
  784. image.startsWith('/') ||
  785. image.startsWith(cdn) ||
  786. image.startsWith(upCdn)
  787. ) {
  788. // 只考虑相对路径下的 image ,绝对路径图片无需下载
  789. if (!images.includes(image)) {
  790. images.push(image);
  791. }
  792. }
  793. }
  794. }
  795. // 无需递归遍历子节点,现在所有的节点都在外层
  796. }
  797. await Promise.all(images.map((image) => zipImage(zip, image)));
  798. }
  799. async function zipImage(zip: JSZip, image: string) {
  800. const res: Blob = await axios.get(image, {
  801. responseType: 'blob',
  802. params: {
  803. isZip: true,
  804. },
  805. });
  806. zip.file(cdn ? image.replace(cdn, '').replace(upCdn, '') : image, res, {
  807. createFolders: true,
  808. });
  809. }
  810. const downloadImageTips =
  811. '无法下载,宽度不合法,画布可能没有画笔/画布大小超出浏览器最大限制';
  812. const downloadPng = () => {
  813. if (!meta2d.store.data.pens.length) {
  814. MessagePlugin.warning(downloadImageTips);
  815. return;
  816. }
  817. try {
  818. meta2d.downloadPng();
  819. } catch (e) {
  820. MessagePlugin.warning(downloadImageTips);
  821. }
  822. };
  823. async function getIconDefs(url: string) {
  824. let res: any = await axios.get(url);
  825. let str = res.match(/@font-face([\s\S]*?)\}/)[1];
  826. str = `@font-face ${str} }`;
  827. return str;
  828. }
  829. const downloadSvg = async () => {
  830. // await import('@/assets/canvas2svg');
  831. for (const pen of meta2d.store.data.pens) {
  832. if (pen.calculative.img) {
  833. //重新生成绘制图片
  834. pen.onRenderPenRaw?.(pen);
  835. }
  836. }
  837. if (!C2S) {
  838. MessagePlugin.error('请先加载乐吾乐官网下的canvas2svg.js');
  839. return;
  840. }
  841. const rect: any = meta2d.getRect();
  842. if (!isFinite(rect.width)) {
  843. MessagePlugin.error(downloadImageTips);
  844. return;
  845. }
  846. rect.x -= 10;
  847. rect.y -= 10;
  848. const ctx = new C2S(rect.width + 20, rect.height + 20);
  849. ctx.textBaseline = 'middle';
  850. ctx.strokeStyle = getGlobalColor(meta2d.store);
  851. for (const pen of meta2d.store.data.pens) {
  852. // 不使用 calculative.inView 的原因是,如果 pen 在 view 之外,那么它的 calculative.inView 为 false,但是它的绘制还是需要的
  853. if (!isShowChild(pen, meta2d.store) || pen.visible == false) {
  854. continue;
  855. }
  856. meta2d.renderPenRaw(ctx, pen, rect);
  857. }
  858. let mySerializedSVG = ctx.getSerializedSvg();
  859. let icon_pens = meta2d.store.data.pens.filter(
  860. (item) => item.iconFamily && item.icon
  861. );
  862. if (icon_pens && icon_pens.length > 0) {
  863. let iconList = [
  864. '/icon/国家电网/iconfont.css',
  865. '/icon/电气工程/iconfont.css',
  866. '/icon/通用图标/iconfont.css',
  867. ];
  868. let defsList: any = await Promise.all(
  869. iconList.map((item) => getIconDefs(item))
  870. );
  871. mySerializedSVG = mySerializedSVG.replace(
  872. '<defs/>',
  873. `<defs>
  874. <style type="text/css">
  875. ${defsList.join('\n')}
  876. </style>
  877. {{bk}}
  878. </defs>
  879. {{bkRect}}`
  880. );
  881. }
  882. /* mySerializedSVG = mySerializedSVG.replace(
  883. '<defs/>',
  884. `<defs>
  885. <style type="text/css">
  886. @font-face {
  887. font-family: 'ticon';
  888. src: url('icon/通用图标/iconfont.ttf') format('truetype');
  889. }
  890. </style>
  891. {{bk}}
  892. </defs>
  893. {{bkRect}}`
  894. );
  895. */
  896. if (meta2d.store.data.background) {
  897. mySerializedSVG = mySerializedSVG.replace('{{bk}}', '');
  898. mySerializedSVG = mySerializedSVG.replace(
  899. '{{bkRect}}',
  900. `<rect x="0" y="0" width="100%" height="100%" fill="${meta2d.store.data.background}"></rect>`
  901. );
  902. } else {
  903. mySerializedSVG = mySerializedSVG.replace('{{bk}}', '');
  904. mySerializedSVG = mySerializedSVG.replace('{{bkRect}}', '');
  905. }
  906. mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, '&#x');
  907. const urlObject: any = (window as any).URL || window;
  908. const export_blob = new Blob([mySerializedSVG]);
  909. const url = urlObject.createObjectURL(export_blob);
  910. const a = document.createElement('a');
  911. a.setAttribute(
  912. 'download',
  913. `${(meta2d.store.data as Meta2dBackData).name || 'le5le.meta2d'}.svg`
  914. );
  915. a.setAttribute('href', url);
  916. const evt = document.createEvent('MouseEvents');
  917. evt.initEvent('click', true, true);
  918. a.dispatchEvent(evt);
  919. };
  920. const onUndo = () => {
  921. meta2d.undo();
  922. };
  923. const onRedo = () => {
  924. meta2d.redo();
  925. };
  926. const onCut = () => {
  927. meta2d.cut();
  928. };
  929. const onCopy = () => {
  930. meta2d.copy();
  931. };
  932. const onPaste = () => {
  933. meta2d.paste();
  934. };
  935. const onAll = () => {
  936. meta2d.activeAll();
  937. };
  938. const onDelete = () => {
  939. meta2d.delete();
  940. };
  941. const onToggleAnchor = () => {
  942. //取消连线状态
  943. // meta2d.store.options.disableAnchor = false;
  944. if (!meta2d.store.options.disableAnchor) {
  945. meta2d.canvas.drawingLineName && drawPen();
  946. meta2d.toggleAnchorMode();
  947. }
  948. };
  949. const onAddAnchorHand = () => {
  950. meta2d.addAnchorHand();
  951. };
  952. const onRemoveAnchorHand = () => {
  953. meta2d.removeAnchorHand();
  954. };
  955. const onToggleAnchorHand = () => {
  956. meta2d.toggleAnchorHand();
  957. };
  958. const onScaleUp = () => {
  959. const _scale = meta2d.store.data.scale + 0.1;
  960. meta2d.scale(_scale);
  961. };
  962. const onScaleDown = () => {
  963. const _scale = meta2d.store.data.scale - 0.1;
  964. meta2d.scale(_scale);
  965. };
  966. const autoAnchor = ref(true);
  967. const onAutoAnchor = () => {
  968. meta2d.store.options.autoAnchor = !meta2d.store.options.autoAnchor;
  969. autoAnchor.value = meta2d.store.options.autoAnchor;
  970. };
  971. const showAnchor = ref(false);
  972. const onDisableAnchor = () => {
  973. meta2d.store.options.disableAnchor = !meta2d.store.options.disableAnchor;
  974. changeDisableAnchor();
  975. };
  976. const changeDisableAnchor = () => {
  977. const { disableAnchor, autoAnchor } = meta2d.store.options;
  978. showAnchor.value = !disableAnchor || false;
  979. if (disableAnchor && autoAnchor) {
  980. // 禁用瞄点开了,需要关闭自动瞄点
  981. onAutoAnchor();
  982. }
  983. };
  984. </script>
  985. <style lang="postcss" scoped>
  986. .app-header {
  987. display: flex;
  988. height: 40px;
  989. background-color: var(--color-background);
  990. position: relative;
  991. z-index: 2;
  992. .logo {
  993. display: flex;
  994. padding: 0 16px;
  995. align-items: center;
  996. font-size: 14px;
  997. font-weight: 500;
  998. img {
  999. height: 20px;
  1000. margin-right: 6px;
  1001. }
  1002. }
  1003. a {
  1004. display: flex;
  1005. padding: 0 8px;
  1006. margin: 0 8px;
  1007. align-items: center;
  1008. color: var(--color);
  1009. text-decoration: none;
  1010. white-space: nowrap;
  1011. &:hover {
  1012. color: var(--color-primary);
  1013. }
  1014. svg {
  1015. font-size: 15px;
  1016. margin: 2px 4px 0 0;
  1017. }
  1018. &.active {
  1019. background-color: var(--color-primary);
  1020. color: #ffffff;
  1021. }
  1022. }
  1023. input {
  1024. font-size: var(--font-size);
  1025. flex-grow: 1;
  1026. background: none;
  1027. outline: none;
  1028. border: none;
  1029. text-align: center;
  1030. color: var(--color-title);
  1031. }
  1032. }
  1033. </style>