1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051 |
- <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"
- trigger1="click"
- >
- <a> 文件 </a>
- <t-dropdown-menu>
- <t-dropdown-item @click="newFile">
- <a>新建文件</a>
- </t-dropdown-item>
- <t-dropdown-item @click="load(true)">
- <a>打开文件</a>
- </t-dropdown-item>
- <t-dropdown-item divider="true" @click="load">
- <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 {
- save,
- blank,
- newFile,
- SaveType,
- onScaleView,
- onScaleWindow,
- showMagnifier,
- showMap,
- drawPen,
- map,
- magnifier,
- useDot,
- delAttrs,
- useAssets,
- } from '@/services/common';
- const router = useRouter();
- const route = useRoute();
- const baseUrl = import.meta.env.BASE_URL || '/';
- const { assets, getAssets } = useAssets();
- const { user, signout } = useUser();
- const { setDot } = useDot();
- const data = reactive({
- name: '空白文件',
- });
- onBeforeMount(async () => {
- getAssets();
- });
- const onInputName = () => {
- (meta2d.store.data as Meta2dBackData).name = data.name;
- setDot();
- };
- const initMeta2dName = () => {
- data.name = (meta2d.store.data as Meta2dBackData).name || '';
- };
- nextTick(() => {
- meta2d.on('opened', initMeta2dName);
- });
- onUnmounted(() => {
- meta2d.off('opened', initMeta2dName);
- });
- const login = () => {
- return `${assets.account}?cb=${encodeURIComponent(location.href)}`;
- };
- function load(isNew = false) {
- const input = document.createElement('input');
- input.type = 'file';
- input.onchange = (event) => {
- const elem = event.target as HTMLInputElement;
- if (elem.files && elem.files[0]) {
- blank();
- // 路由跳转 可能在 openFile 后执行
- if (elem.files[0].name.endsWith('.json')) {
- openJson(elem.files[0]);
- if (isNew) {
- router.push({
- path: '/',
- query: {
- r: Date.now() + '',
- },
- });
- }
- } else if (elem.files[0].name.endsWith('.svg')) {
- MessagePlugin.info(
- '可二次编辑但转换存在损失,若作为图片使用,请使用右侧属性面板的上传图片功能'
- );
- openSvg(elem.files[0]);
- } else if (elem.files[0].name.endsWith('.zip')) {
- router.push({
- path: '/',
- query: {
- r: Date.now() + '',
- },
- });
- setTimeout(() => {
- openZip(elem.files[0]);
- }, 500);
- } else {
- MessagePlugin.info('打开文件只支持 json,svg,zip 格式');
- }
- }
- };
- input.click();
- }
- const openJson = async (file: File) => {
- 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);
- for (const k of delAttrs) {
- delete (data as any)[k];
- }
- 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) => {
- 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);
- const delAttrs = [
- 'userId',
- 'shared',
- 'team',
- 'owner',
- 'username',
- 'editor',
- 'editorId',
- 'editorName',
- 'createdAt',
- 'folder',
- 'image',
- 'id',
- '_id',
- 'view',
- 'updatedAt',
- 'star',
- 'recommend',
- ];
- for (const k of delAttrs) {
- delete (data as any)[k];
- }
- meta2d.open(data);
- }
- } catch (e) {
- return false;
- }
- };
- 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 = () => {
- meta2d.activeAll();
- };
- 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>
|