123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089 |
- <template>
- <div class="app-header">
- <a class="logo" :href="assets.home" target="_blank">
- <img src="/favicon.ico" />
- <span>乐吾乐</span>
- </a>
- <t-dropdown
- :minColumnWidth="200"
- :maxHeight="560"
- :delay2="[10, 150]"
- overlayClassName="header-dropdown"
- >
- <a> 文件 </a>
- <t-dropdown-menu>
- <t-dropdown-item @click="newFile">
- <a>新建文件</a>
- </t-dropdown-item>
- <t-dropdown-item @click="openFile">
- <a>打开文件</a>
- </t-dropdown-item>
- <t-dropdown-item divider="true" @click="loadFile">
- <a>导入文件</a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="save()">保存</a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="save(SaveType.SaveAs)">另保存</a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a @click="downloadJson">下载JSON文件</a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="downloadZip">
- <div class="flex">
- 导出为ZIP文件 <span class="flex-grow"></span>
- <span><label>VIP</label></span>
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="downloadHtml">
- <div class="flex">
- 导出为HTML <span class="flex-grow"></span>
- <span><label>VIP</label></span>
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="downloadVue3">
- <div class="flex">
- 导出为Vue3组件 <span class="flex-grow"></span>
- <span><label>VIP</label></span>
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="downloadVue2">
- <div class="flex">
- 导出为Vue2组件 <span class="flex-grow"></span>
- <span><label>VIP</label></span>
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a @click="downloadReact">
- <div class="flex">
- 导出为React组件 <span class="flex-grow"></span>
- <span><label>VIP</label></span>
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="downloadPng">下载为PNG</a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="downloadSvg">下载为SVG</a>
- </t-dropdown-item>
- </t-dropdown-menu>
- </t-dropdown>
- <t-dropdown
- :minColumnWidth="180"
- :maxHeight="500"
- :delay2="[10, 150]"
- overlayClassName="header-dropdown"
- >
- <a> 编辑 </a>
- <t-dropdown-menu>
- <t-dropdown-item>
- <a @click="onUndo">
- <div class="flex">
- 撤销 <span class="flex-grow"></span> Ctrl + Z
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a @click="onRedo">
- <div class="flex">
- 恢复 <span class="flex-grow"></span> Ctrl + Y
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="onCut">
- <div class="flex">
- 剪切 <span class="flex-grow"></span> Ctrl + X
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="onCopy">
- <div class="flex">
- 复制 <span class="flex-grow"></span> Ctrl + C
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a @click="onPaste">
- <div class="flex">
- 粘贴 <span class="flex-grow"></span> Ctrl + V
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="onAll">
- <div class="flex">
- 全选 <span class="flex-grow"></span> Ctrl + A
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="onDelete">
- <div class="flex">删除 <span class="flex-grow"></span> DELETE</div>
- </a>
- </t-dropdown-item>
- </t-dropdown-menu>
- </t-dropdown>
- <t-dropdown
- :minColumnWidth="180"
- :maxHeight="500"
- :delay2="[10, 150]"
- overlayClassName="header-dropdown"
- >
- <a> 工具 </a>
- <t-dropdown-menu>
- <t-dropdown-item>
- <a @click="onScaleWindow">窗口大小</a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="onScaleUp">放大</a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="onScaleDown">缩小</a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a @click="onScaleView">100%视图</a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="showMap">
- <div class="flex middle">
- 鹰眼地图 <span class="flex-grow"></span>
- <t-icon v-show="map" name="check" />
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a @click="showMagnifier">
- <div class="flex middle">
- 放大镜 <span class="flex-grow"></span>
- <t-icon v-show="magnifier" name="check" />
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="onAutoAnchor">
- <div class="flex middle">
- 自动锚点 <span class="flex-grow"></span>
- <t-icon v-show="autoAnchor" name="check" />
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a @click="onDisableAnchor">
- <div class="flex middle">
- 显示锚点 <span class="flex-grow"></span>
- <t-icon v-show="showAnchor" name="check" />
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a @click="onToggleAnchor">
- <div
- class="flex"
- :style="{
- color: showAnchor ? '' : '#4f5b75',
- }"
- >
- 添加/删除锚点 <span class="flex-grow"></span> A
- </div>
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="switchTheme('light')">明亮主题</a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="switchTheme('dark')">暗黑主题</a>
- </t-dropdown-item>
- </t-dropdown-menu>
- </t-dropdown>
- <t-dropdown
- :minColumnWidth="180"
- :maxHeight="500"
- :delay2="[10, 150]"
- overlayClassName="header-dropdown"
- >
- <a> 帮助 </a>
- <t-dropdown-menu>
- <t-dropdown-item v-for="item in assets.helps" :divider="item.divider">
- <a :href="item.url" target="_blank">{{ item.name }}</a>
- </t-dropdown-item>
- </t-dropdown-menu>
- </t-dropdown>
- <input v-model="data.name" @input="onInputName" />
- <div style="width: 290px; flex-shrink: 0"></div>
- <t-dropdown
- v-if="user.id"
- :minColumnWidth="200"
- :maxHeight="500"
- :delay2="[10, 150]"
- overlayClassName="custom-dropdown header"
- >
- <a style="margin-left: 32px; margin-right: 12px">
- <t-avatar
- size="small"
- :image="user.avatarUrl ? user.avatarUrl : baseUrl + 'img/avatar.png'"
- />
- </a>
- <t-dropdown-menu>
- <t-dropdown-item divider="true">
- <a :href="assets.account">
- {{ user.username }}
- <label class="ml-16 vip-label">VIP</label>
- </a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a :href="`${assets.account}/v`" target="_blank"> 我的大屏 </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a :href="`${assets.account}/account/teams`" target="_blank">
- 我的团队
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a :href="`${assets.account}/account/info`" target="_blank">
- 账号信息
- </a>
- </t-dropdown-item>
- <t-dropdown-item divider="true">
- <a :href="`${assets.account}/account/security`" target="_blank">
- 安全设置
- </a>
- </t-dropdown-item>
- <t-dropdown-item>
- <a @click="signout">退出</a>
- </t-dropdown-item>
- </t-dropdown-menu>
- </t-dropdown>
- <div class="flex middle" v-else>
- <a class="button primary solid" style="width: 80px" :href="login()">
- 登录
- </a>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { reactive, ref, onBeforeMount, onUnmounted, nextTick } from 'vue';
- import { useRouter, useRoute } from 'vue-router';
- import { useUser } from '@/services/user';
- import { MessagePlugin } from 'tdesign-vue-next';
- import {
- Meta2dBackData,
- dealwithFormatbeforeOpen,
- gotoAccount,
- checkData,
- } from '@/services/utils';
- import { readFile } from '@/services/file';
- import { compareVersion, baseVer, upgrade } from '@/services/upgrade';
- import { parseSvg } from '@meta2d/svg';
- import { Pen, getGlobalColor, isShowChild } from '@meta2d/core';
- import { cdn, upCdn } from '@/services/api';
- import JSZip from 'jszip';
- import axios from 'axios';
- import { switchTheme } from '@/services/theme';
- import { noLoginTip } from '@/services/utils';
- import { useDot, notificFn } from '@/services/common';
- import {
- save,
- newFile,
- SaveType,
- onScaleView,
- onScaleWindow,
- showMagnifier,
- showMap,
- newfile,
- title,
- drawPen,
- map,
- magnifier,
- } from '@/services/common';
- const router = useRouter();
- const route = useRoute();
- const baseUrl = import.meta.env.BASE_URL || '/';
- const assets = reactive({
- home: 'https://le5le.com',
- account: 'https://account.le5le.com',
- helps: [
- {
- name: '产品介绍',
- url: 'https://doc.le5le.com/document/118756411',
- },
- {
- name: '快速上手',
- url: 'https://doc.le5le.com/document/119363000',
- },
- {
- name: '使用手册',
- url: 'https://doc.le5le.com/document/118764244',
- },
- {
- name: '快捷键',
- url: 'https://doc.le5le.com/document/119620214',
- divider: true,
- },
- {
- name: '企业服务与支持',
- url: 'https://doc.le5le.com/document/119296274',
- divider: true,
- },
- {
- name: '关于我们',
- url: 'https://le5le.com/about.html',
- },
- ],
- });
- const { user, signout } = useUser();
- const { setDot } = useDot();
- const data = reactive({
- name: '空白文件',
- });
- onBeforeMount(async () => {
- // 官网或安装包版本
- if (
- import.meta.env.VITE_TRIAL == undefined ||
- import.meta.env.VITE_TRIAL == 1
- ) {
- return;
- }
- // 企业版
- const ret = await axios.get('/api/assets');
- if (ret) {
- Object.assign(assets, ret);
- }
- });
- const onInputName = () => {
- (meta2d.store.data as Meta2dBackData).name = data.name;
- setDot(true);
- };
- const initMeta2dName = () => {
- data.name = (meta2d.store.data as Meta2dBackData).name || '';
- };
- nextTick(() => {
- meta2d.on('opened', initMeta2dName);
- });
- onUnmounted(() => {
- meta2d.off('opened', initMeta2dName);
- });
- function login() {
- return `${assets.account}?cb=${encodeURIComponent(location.href)}`;
- }
- function load(newT: boolean = false) {
- const input = document.createElement('input');
- input.type = 'file';
- input.onchange = (event) => {
- const elem = event.target as HTMLInputElement;
- if (elem.files && elem.files[0]) {
- newT && newfile(true);
- // 路由跳转 可能在 openFile 后执行
- if (elem.files[0].name.endsWith('.json')) {
- openJson(elem.files[0], newT);
- } else if (elem.files[0].name.endsWith('.svg')) {
- MessagePlugin.info(
- '可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能'
- );
- openSvg(elem.files[0]);
- } else if (elem.files[0].name.endsWith('.zip')) {
- openZip(elem.files[0], newT);
- } else {
- MessagePlugin.info('打开文件只支持 json,svg,zip 格式');
- }
- }
- };
- input.click();
- }
- const openJson = async (file: File, isNew: boolean = false) => {
- const text = await readFile(file);
- try {
- let data: Meta2dBackData = JSON.parse(text);
- if (!data.name) {
- data.name = file.name.replace('.json', '');
- }
- if (!data.version || compareVersion(data.version, baseVer) === -1) {
- // 如果版本号不存在或者版本号 version < 1.0.0
- data = upgrade(data, baseVer);
- }
- dealwithFormatbeforeOpen(data);
- data._id = undefined;
- if (!isNew) {
- delete data.owner;
- delete data.editor;
- delete data.username;
- delete data.editorId;
- delete data.editorName;
- }
- // if (!(window as any).meta2dFolder?.includes(data.folder)) {
- // delete data.folder;
- // }
- meta2d.open(data);
- } catch (e) {
- console.log(e);
- }
- };
- const openSvg = async (file: File) => {
- const text = await readFile(file);
- const pens: Pen[] = parseSvg(text);
- meta2d.canvas.addCaches = pens;
- MessagePlugin.info('svg转换成功,请点击画布决定放置位置');
- };
- const openZip = async (file: File, isNew: boolean = false) => {
- if (!(user && user.id)) {
- MessagePlugin.warning(noLoginTip);
- return;
- }
- if (!user.isVip) {
- gotoAccount();
- return;
- }
- const { default: JSZip } = await import('jszip');
- const zip = new JSZip();
- await zip.loadAsync(file);
- let dataStr = '';
- for (const key in zip.files) {
- if (zip.files[key].dir) {
- continue;
- }
- if (key.endsWith('.json')) {
- // 认为只有一个 json 文件
- // dataStr = await zip.file(key).async('string');
- break;
- }
- }
- if (!dataStr) {
- return false;
- }
- for (const key in zip.files) {
- if (zip.files[key].dir) {
- continue;
- }
- // let _png = key.indexOf('/png');
- // let _img = key.indexOf('/img');
- // let _image = key.indexOf('/image');
- // let _file = key.indexOf('/file');
- let _keyLower = key.toLowerCase();
- // if (!key.endsWith('.json') && (_png !== -1 || _img !== -1 || _image !== -1 || _file !== -1)) {
- if (
- _keyLower.endsWith('.png') ||
- _keyLower.endsWith('.svg') ||
- _keyLower.endsWith('.gif') ||
- _keyLower.endsWith('.jpg') ||
- _keyLower.endsWith('.jpeg')
- ) {
- let filename = key.substr(key.lastIndexOf('/') + 1);
- const extPos = filename.lastIndexOf('.');
- let ext = '';
- if (extPos > 0) {
- ext = filename.substr(extPos);
- }
- filename = filename.substring(0, extPos > 8 ? 8 : extPos);
- // 上传文件
- const result: any = {};
- // await upload(
- // // await zip.file(key).async('blob'),
- // true,
- // filename + ext,
- // "/2D/未分类"
- // );
- let _key = key;
- // if (_png) {
- // _key = key.substring(_png);
- // } else if (_image) {
- // _key = key.substring(_png);
- // } else if (_img) {
- // _key = key.substring(_img);
- // } else if (_file) {
- // _key = key.substring(_file);
- // }
- if (result) {
- if (dataStr.replaceAll) {
- //'le5le.meta2d'
- dataStr = dataStr.replaceAll(_key.slice(12), result.url);
- } else {
- while (dataStr.includes(_key)) {
- dataStr = dataStr.replace(_key.slice(12), result.url);
- // 正则 gm 在特殊情况下报错,例如如下场景
- /**
- *
- const data = '{"image":"/image/materials/IoT-Chemical(化学)/Air stripper 2(汽提塔2).svg"}';
- const key = '/image/materials/IoT-Chemical(化学)/Air stripper 2(汽提塔2).svg';
- data.replace(key, '123');
- data.replaceAll(key, '123')
- data.replace(new RegExp(key, 'gm'), '123');
- data.replace(new RegExp(key, 'g'), '123');
- */
- }
- }
- }
- }
- }
- try {
- let data: Meta2dBackData = JSON.parse(dataStr);
- if (data) {
- if (!data.name) {
- data.name = file.name.replace('.zip', '');
- }
- if (!data.version || compareVersion(data.version, baseVer) === -1) {
- // 如果版本号不存在或者版本号 version < 1.0.0
- data = upgrade(data, baseVer);
- }
- dealwithFormatbeforeOpen(data);
- data._id = undefined;
- if (!isNew) {
- delete data.owner;
- delete data.editor;
- delete data.username;
- delete data.editorId;
- delete data.editorName;
- }
- if (!(window as any).meta2dFolder?.includes(data.folder)) {
- delete data.folder;
- }
- meta2d.open(data);
- }
- } catch (e) {
- return false;
- }
- };
- async function loadFile(newT: boolean = false) {
- // if (dot.value) {
- // if (await showNotification(title)) {
- // load(newT);
- // }
- // } else {
- // load(newT);
- // }
- //默认 导入新建项目
- notificFn(load, newT);
- }
- async function openFile() {
- loadFile(true);
- }
- const downloadJson = () => {
- const data: Meta2dBackData = meta2d.data();
- if (data._id) delete data._id;
- checkData(data);
- import('file-saver').then(({ saveAs }) => {
- saveAs(
- new Blob(
- [JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')],
- {
- type: 'text/plain;charset=utf-8',
- }
- ),
- `${data.name || 'le5le.meta2d'}.json`
- );
- });
- };
- const downloadZip = async () => {
- if (!(user && user.id)) {
- MessagePlugin.warning(noLoginTip);
- return;
- }
- if (!user.isVip) {
- gotoAccount();
- return;
- }
- MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
- const [{ default: JSZip }, { saveAs }] = await Promise.all([
- import('jszip'),
- import('file-saver'),
- ]);
- const zip: any = new JSZip();
- const data: Meta2dBackData = meta2d.data();
- let _fileName =
- (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
- 'le5le.meta2d';
- const _zip = zip.folder(`${_fileName}`);
- if (data._id) delete data._id;
- checkData(data);
- _zip.file(
- `${_fileName}.json`,
- JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
- );
- await zipImages(_zip, meta2d.store.data.pens);
- const blob = await zip.generateAsync({ type: 'blob' });
- saveAs(blob, `${_fileName}.zip`);
- };
- const downloadHtml = async () => {
- if (!(user && user.id)) {
- MessagePlugin.warning(noLoginTip);
- return;
- }
- if (!user.isVip) {
- gotoAccount();
- return;
- }
- MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
- const data: Meta2dBackData = meta2d.data();
- if (data._id) delete data._id;
- checkData(data);
- const [{ default: JSZip }, { saveAs }] = await Promise.all([
- import('jszip'),
- import('file-saver'),
- ]);
- const zip = new JSZip();
- let _fileName =
- (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
- 'le5le.meta2d';
- //处理cdn图片地址
- const _zip: any = zip.folder(`${_fileName}`);
- _zip.file(
- 'data.json',
- JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
- );
- await Promise.all([zipImages(_zip, meta2d.store.data.pens), zipFiles(_zip)]);
- const blob = await zip.generateAsync({ type: 'blob' });
- saveAs(blob, `${_fileName}.zip`);
- };
- enum Frame {
- vue2,
- vue3,
- react,
- }
- const downloadVue3 = async () => {
- downloadAsFrame(Frame.vue3);
- };
- const downloadVue2 = async () => {
- downloadAsFrame(Frame.vue2);
- };
- const downloadReact = async () => {
- downloadAsFrame(Frame.react);
- };
- async function downloadAsFrame(type: Frame) {
- if (!(user && user.id)) {
- MessagePlugin.warning(noLoginTip);
- return;
- }
- if (!user.isVip) {
- gotoAccount();
- return;
- }
- MessagePlugin.info('正在下载打包中,可能需要几分钟,请耐心等待...');
- const data: Meta2dBackData = meta2d.data();
- if (data._id) delete data._id;
- checkData(data);
- const [{ default: JSZip }, { saveAs }] = await Promise.all([
- import('jszip'),
- import('file-saver'),
- ]);
- const zip = new JSZip();
- let _fileName =
- (data.name && data.name.replace(/\//g, '_').replace(/:/g, '_')) ||
- 'le5le.meta2d';
- const _zip: any = zip.folder(`${_fileName}`);
- _zip.file(
- 'data.json',
- JSON.stringify(data).replaceAll(cdn, '').replaceAll(upCdn, '')
- );
- await Promise.all([
- zipImages(_zip, meta2d.store.data.pens),
- type === Frame.vue3
- ? zipVue3Files(_zip)
- : type === Frame.vue2
- ? zipVue2Files(_zip)
- : zipReactFiles(_zip),
- ]);
- const blob = await zip.generateAsync({ type: 'blob' });
- saveAs(blob, `${_fileName}.zip`);
- }
- async function zipVue3Files(zip: JSZip) {
- const files = [
- '/view/js/marked.min.js',
- '/view/js/lcjs.iife.js',
- '/view/vue3/Meta2d.vue',
- '/view/index.html',
- '/view/js/meta2d.js',
- '/view/使用说明.md',
- ] as const;
- // 文件同时加载
- await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
- }
- async function zipVue2Files(zip: JSZip) {
- const files = [
- '/view/js/marked.min.js',
- '/view/js/lcjs.iife.js',
- '/view/vue2/Meta2d.vue',
- '/view/index.html',
- '/view/js/meta2d.js',
- '/view/使用说明.md',
- ] as const;
- // 文件同时加载
- await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
- }
- async function zipReactFiles(zip: JSZip) {
- const files = [
- '/view/js/marked.min.js',
- '/view/js/lcjs.iife.js',
- '/view/react/Meta2d.jsx',
- '/view/react/Meta2d.css',
- '/view/index.html',
- '/view/js/meta2d.js',
- '/view/使用说明.md',
- ] as const;
- // 文件同时加载
- await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
- }
- async function zipFiles(zip: JSZip) {
- const files = [
- '/view/js/marked.min.js',
- '/view/js/lcjs.iife.js',
- '/view/js/index.js',
- '/view/js/meta2d.js',
- '/view/index.html',
- '/view/index.css',
- '/view/favicon.ico',
- '/view/使用说明.pdf',
- ] as const;
- // 文件同时加载
- await Promise.all(files.map((filePath) => zipFile(zip, filePath)));
- }
- async function zipFile(zip: JSZip, filePath: string) {
- const res: Blob = await axios.get((cdn ? cdn + '/2d' : '') + filePath, {
- responseType: 'blob',
- });
- zip.file(filePath.replace('/view', ''), res, { createFolders: true });
- }
- /**
- * 图片放到 zip 里
- * @param pens 可以是非具有 calculative 的 pen
- */
- async function zipImages(zip: JSZip, pens: Pen[]) {
- if (!pens) {
- return;
- }
- // 不止 image 上有图片, strokeImage ,backgroundImage 也有图片
- const imageKeys = [
- {
- string: 'image',
- },
- { string: 'strokeImage' },
- { string: 'backgroundImage' },
- ] as const;
- const images: string[] = [];
- for (const pen of pens) {
- for (const i of imageKeys) {
- const image = pen[i.string];
- if (image) {
- // HTMLImageElement 无法精确控制图片格式
- if (
- image.startsWith('/') ||
- image.startsWith(cdn) ||
- image.startsWith(upCdn)
- ) {
- // 只考虑相对路径下的 image ,绝对路径图片无需下载
- if (!images.includes(image)) {
- images.push(image);
- }
- }
- }
- }
- // 无需递归遍历子节点,现在所有的节点都在外层
- }
- await Promise.all(images.map((image) => zipImage(zip, image)));
- }
- async function zipImage(zip: JSZip, image: string) {
- const res: Blob = await axios.get(image, {
- responseType: 'blob',
- params: {
- isZip: true,
- },
- });
- zip.file(cdn ? image.replace(cdn, '').replace(upCdn, '') : image, res, {
- createFolders: true,
- });
- }
- const downloadImageTips =
- '无法下载,宽度不合法,画布可能没有画笔/画布大小超出浏览器最大限制';
- const downloadPng = () => {
- const name = (meta2d.store.data as Meta2dBackData).name;
- try {
- meta2d.downloadPng(name ? name + '.png' : undefined);
- } catch (e) {
- MessagePlugin.warning(downloadImageTips);
- }
- };
- async function getIconDefs(url: string) {
- let res: any = await axios.get(url);
- let str = res.match(/@font-face([\s\S]*?)\}/)[1];
- str = `@font-face ${str} }`;
- return str;
- }
- const downloadSvg = async () => {
- await import('@/assets/canvas2svg');
- if (!C2S) {
- MessagePlugin.error('请先加载乐吾乐官网下的canvas2svg.js');
- return;
- }
- const rect: any = meta2d.getRect();
- if (!isFinite(rect.width)) {
- MessagePlugin.error(downloadImageTips);
- return;
- }
- rect.x -= 10;
- rect.y -= 10;
- const ctx = new C2S(rect.width + 20, rect.height + 20);
- ctx.textBaseline = 'middle';
- ctx.strokeStyle = getGlobalColor(meta2d.store);
- for (const pen of meta2d.store.data.pens) {
- // 不使用 calculative.inView 的原因是,如果 pen 在 view 之外,那么它的 calculative.inView 为 false,但是它的绘制还是需要的
- if (!isShowChild(pen, meta2d.store) || pen.visible == false) {
- continue;
- }
- meta2d.renderPenRaw(ctx, pen, rect);
- }
- let mySerializedSVG = ctx.getSerializedSvg();
- let icon_pens = meta2d.store.data.pens.filter(
- (item) => item.iconFamily && item.icon
- );
- if (icon_pens && icon_pens.length > 0) {
- let iconList = [
- '/icon/国家电网/iconfont.css',
- '/icon/电气工程/iconfont.css',
- '/icon/通用图标/iconfont.css',
- ];
- let defsList: any = await Promise.all(
- iconList.map((item) => getIconDefs(item))
- );
- mySerializedSVG = mySerializedSVG.replace(
- '<defs/>',
- `<defs>
- <style type="text/css">
- ${defsList.join('\n')}
- </style>
- {{bk}}
- </defs>
- {{bkRect}}`
- );
- }
- /* mySerializedSVG = mySerializedSVG.replace(
- '<defs/>',
- `<defs>
- <style type="text/css">
- @font-face {
- font-family: 'ticon';
- src: url('icon/通用图标/iconfont.ttf') format('truetype');
- }
- </style>
- {{bk}}
- </defs>
- {{bkRect}}`
- );
- */
- if (meta2d.store.data.background) {
- mySerializedSVG = mySerializedSVG.replace('{{bk}}', '');
- mySerializedSVG = mySerializedSVG.replace(
- '{{bkRect}}',
- `<rect x="0" y="0" width="100%" height="100%" fill="${meta2d.store.data.background}"></rect>`
- );
- } else {
- mySerializedSVG = mySerializedSVG.replace('{{bk}}', '');
- mySerializedSVG = mySerializedSVG.replace('{{bkRect}}', '');
- }
- mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, '&#x');
- const urlObject: any = (window as any).URL || window;
- const export_blob = new Blob([mySerializedSVG]);
- const url = urlObject.createObjectURL(export_blob);
- const a = document.createElement('a');
- a.setAttribute(
- 'download',
- `${(meta2d.store.data as Meta2dBackData).name || 'le5le.meta2d'}.svg`
- );
- a.setAttribute('href', url);
- const evt = document.createEvent('MouseEvents');
- evt.initEvent('click', true, true);
- a.dispatchEvent(evt);
- };
- const onUndo = () => {
- meta2d.undo();
- };
- const onRedo = () => {
- meta2d.redo();
- };
- const onCut = () => {
- meta2d.cut();
- };
- const onCopy = () => {
- meta2d.copy();
- };
- const onPaste = () => {
- meta2d.paste();
- };
- const onAll = () => {
- // todo
- };
- const onDelete = () => {
- meta2d.delete();
- };
- const onToggleAnchor = () => {
- //取消连线状态
- // meta2d.store.options.disableAnchor = false;
- if (!meta2d.store.options.disableAnchor) {
- meta2d.canvas.drawingLineName && drawPen();
- meta2d.toggleAnchorMode();
- }
- };
- const onAddAnchorHand = () => {
- meta2d.addAnchorHand();
- };
- const onRemoveAnchorHand = () => {
- meta2d.removeAnchorHand();
- };
- const onToggleAnchorHand = () => {
- meta2d.toggleAnchorHand();
- };
- const onScaleUp = () => {
- const _scale = meta2d.store.data.scale + 0.1;
- meta2d.scale(_scale);
- };
- const onScaleDown = () => {
- const _scale = meta2d.store.data.scale - 0.1;
- meta2d.scale(_scale);
- };
- const autoAnchor = ref(true);
- const onAutoAnchor = () => {
- meta2d.store.options.autoAnchor = !meta2d.store.options.autoAnchor;
- autoAnchor.value = meta2d.store.options.autoAnchor;
- };
- const showAnchor = ref(false);
- const onDisableAnchor = () => {
- meta2d.store.options.disableAnchor = !meta2d.store.options.disableAnchor;
- changeDisableAnchor();
- };
- const changeDisableAnchor = () => {
- const { disableAnchor, autoAnchor } = meta2d.store.options;
- showAnchor.value = !disableAnchor || false;
- if (disableAnchor && autoAnchor) {
- // 禁用瞄点开了,需要关闭自动瞄点
- onAutoAnchor();
- }
- };
- </script>
- <style lang="postcss" scoped>
- .app-header {
- display: flex;
- height: 40px;
- .logo {
- display: flex;
- padding: 0 16px;
- align-items: center;
- font-size: 14px;
- font-weight: 500;
- img {
- height: 20px;
- margin-right: 6px;
- }
- }
- a {
- display: flex;
- padding: 0 8px;
- margin: 0 8px;
- align-items: center;
- color: var(--color);
- text-decoration: none;
- &:hover {
- color: var(--color-primary);
- }
- }
- input {
- font-size: var(--font-size);
- flex-grow: 1;
- background: none;
- outline: none;
- border: none;
- text-align: center;
- color: var(--color-title);
- }
- }
- </style>
|