Jelajahi Sumber

feat:loadZip

ananzhusen 1 tahun lalu
induk
melakukan
d91ee72437
3 mengubah file dengan 540 tambahan dan 1 penghapusan
  1. 1 0
      package.json
  2. 359 0
      src/services/load3d.ts
  3. 180 1
      src/views/components/Header.vue

+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "@meta2d/svg": "^1.0.3",
     "@meta2d/utils": "^1.0.0",
     "axios": "^0.26.0",
+    "crypto-js": "^4.1.1",
     "dayjs": "^1.11.5",
     "exceljs": "^4.3.0",
     "fast-xml-parser": "^4.0.1",

+ 359 - 0
src/services/load3d.ts

@@ -0,0 +1,359 @@
+import JSZip from 'jszip';
+import { s8 } from './random';
+import axios from 'axios';
+import CryptoJS from 'crypto-js';
+import { deepClone } from '@meta2d/core';
+
+export const MESH_SUFFIXES = ['.glb', '.l3db'];
+export const TEMPLATE_SUFFIXES = ['.l3dm'];
+export const IMAGE_SUFFIXES = ['.png', '.jpg', '.jpeg', '.svg', '.bmp', '.gif'];
+export const HDR_SUFFIXES = ['.hdr', '.dds', '.env'];
+
+export async function load3d(zip: JSZip, key: string) {
+  const _filename = key.split('/')[0];
+  const dataStr = await zip.file(key).async('string');
+  const { data, map } = JSON.parse(dataStr);
+  const newUrlMap: { [oldUrl: string]: string } = {};
+  await Promise.all(
+    Object.keys(map).map(async (url) => {
+      try {
+        const file = await zip
+          .file(_filename + '/files/' + map[url])
+          ?.async('arraybuffer');
+        if (file) {
+          const names = filename(url, true).split('.');
+          names.splice(-1, 0, s8());
+          const name = names.join('.');
+          const newFile = new File([file], name);
+          const suffix = '.' + names[names.length - 1];
+          let directory = '';
+          let type = '';
+          let tags = '';
+          if (MESH_SUFFIXES.includes(suffix)) {
+            directory = '/3D/模型/未分类';
+            type = '3D模型';
+            tags = '我的模型';
+          } else if (TEMPLATE_SUFFIXES.includes(suffix)) {
+            directory = '/3D/面板/未分类';
+            type = 'L3DM';
+            tags = '我的面板';
+          } else if (IMAGE_SUFFIXES.includes(suffix)) {
+            directory = '/3D/图片/未分类';
+            type = '图片';
+            tags = '我的图片';
+          } else if (HDR_SUFFIXES.includes(suffix)) {
+            directory = '/3D/HDR/未分类';
+            type = 'HDR';
+            tags = '我的HDR';
+          }
+          if (directory) {
+            const res = await uploadFile({
+              file: newFile,
+              directory,
+              type,
+              tags,
+            });
+            if (res) {
+              newUrlMap[url] = res.url;
+              // uploaded++;
+              return;
+            }
+          }
+        }
+        // failed++;
+      } catch (e) {
+        // failed++;
+      } finally {
+      }
+    })
+  );
+  const newData = meta3dReplaceUrl(data, newUrlMap);
+
+  //上传
+  const projectData = meta3dCrypto(JSON.stringify(newData));
+  const res: any = await axios.post('/api/data/3d/add', {
+    data: {
+      data: projectData,
+      scenes: [],
+      updatedCount: 1,
+    },
+    image: 'xxx',
+    name: newData.name,
+  });
+  if (res) {
+    return res.id;
+  } else {
+    return '';
+  }
+}
+export function s16() {
+  let chars = [
+    '0',
+    '1',
+    '2',
+    '3',
+    '4',
+    '5',
+    '6',
+    '7',
+    '8',
+    '9',
+    'A',
+    'B',
+    'C',
+    'D',
+    'E',
+    'F',
+    'G',
+    'H',
+    'I',
+    'J',
+    'K',
+    'L',
+    'M',
+    'N',
+    'O',
+    'P',
+    'Q',
+    'R',
+    'S',
+    'T',
+    'U',
+    'V',
+    'W',
+    'X',
+    'Y',
+    'Z',
+    'a',
+    'b',
+    'c',
+    'd',
+    'e',
+    'f',
+    'g',
+    'h',
+    'i',
+    'j',
+    'k',
+    'l',
+    'm',
+    'n',
+    'o',
+    'p',
+    'q',
+    'r',
+    's',
+    't',
+    'u',
+    'v',
+    'w',
+    'x',
+    'y',
+    'z',
+  ];
+  let strs = '';
+  for (let i = 0; i < 16; i++) {
+    let id = Math.ceil(Math.random() * 61);
+    strs += chars[id];
+  }
+  return strs;
+}
+
+const a = (word: string, k: string, i: string) => {
+  let srcs = CryptoJS.enc.Utf8.parse(word);
+  let encrypted = CryptoJS.AES.encrypt(srcs, CryptoJS.enc.Utf8.parse(k), {
+    iv: CryptoJS.enc.Utf8.parse(i),
+    mode: CryptoJS.mode.CBC,
+    padding: CryptoJS.pad.Pkcs7,
+  });
+  return encrypted.ciphertext.toString().toUpperCase();
+};
+
+const c = (word: string): string => {
+  const k = s16().toUpperCase();
+  const i = s16().toUpperCase();
+  const d = a(word, k, i);
+  return k + d + i;
+};
+
+const meta3dCrypto = (data: string) => {
+  return c(data);
+};
+
+export const getUrlWithoutSearch = (url: string) => {
+  return url.split('?')[0];
+};
+
+export function filename(str: string, withSuffix = false) {
+  const paths = getUrlWithoutSearch(str).split('/');
+  const name = paths[paths.length - 1];
+  if (withSuffix) {
+    return name;
+  }
+  const i = name.lastIndexOf('.');
+  if (i === -1) {
+    return name;
+  }
+  return name.substring(0, i);
+}
+
+export const uploadFile = async (info: {
+  file: File;
+  directory: string;
+  type: UploadFileType;
+  tags?: string;
+  remarks?: string;
+  name?: string;
+}) => {
+  const { file, directory, type, tags = '', remarks = '', name = '' } = info;
+  const formData = new FormData();
+  formData.append('file', file);
+  formData.append('directory', directory);
+  formData.append('type', type);
+  formData.append('tags', tags);
+  formData.append('remarks', remarks);
+  formData.append('shared', 'true');
+  if (name) {
+    formData.append('name', name);
+  }
+  const res = (await axios.post(
+    '/api/file/upload',
+    formData
+  )) as ResponseResult<FileData>;
+  if (res.error) {
+  }
+  return res.data;
+};
+
+export type UploadFileType =
+  | '文档'
+  | '图片'
+  | '3D模型'
+  | '视频'
+  | '音乐'
+  | '其他'
+  | string;
+
+export interface ResponseResult<T> {
+  success: boolean;
+  data?: T;
+  error?: string;
+  detail?: string;
+}
+
+export interface FileData {
+  createdAt: string;
+  directory: string;
+  fullname: string;
+  id: string;
+  name: string;
+  ownerId: string;
+  ownerName: string;
+  shared: boolean;
+  size: number;
+  tags: string[];
+  type: UploadFileType;
+  updatedAt: string;
+  url: string;
+}
+const b = (word: string, k: string, i: string) => {
+  let encryptedHexStr = CryptoJS.enc.Hex.parse(word);
+  let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
+  let decrypt = CryptoJS.AES.decrypt(srcs, CryptoJS.enc.Utf8.parse(k), {
+    iv: CryptoJS.enc.Utf8.parse(i),
+    mode: CryptoJS.mode.CBC,
+    padding: CryptoJS.pad.Pkcs7,
+  });
+  let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
+  return decryptedStr.toString();
+};
+
+const d = (word: string): string => {
+  const k = word.substring(0, 16);
+  const i = word.substring(word.length - 16);
+  return b(word.substring(16, word.length - 16), k, i);
+};
+
+const parseData = (data: Partial<any> | string): Partial<any> => {
+  if (typeof data === 'string') {
+    data = data.startsWith('{') ? data : d(data);
+    return JSON.parse(data);
+  }
+  return data;
+};
+
+export function filepath(str: string) {
+  const paths = str.split('/');
+  paths[paths.length - 1] = '';
+  return paths.join('/');
+}
+
+const meta3dReplaceUrl = (
+  data: any | string,
+  urlMap: { [url: string]: string }
+) => {
+  const _data = parseData(data);
+  const { scenes } = _data;
+  const transUrl = (url: string): string => {
+    if (url.startsWith('data:')) {
+      const str = url.substring(5);
+      const [image, ...arr] = str.split('#').reverse();
+      url = arr.reverse().join('#');
+      const newUrl = transUrl(url);
+      return 'data:' + newUrl + '#' + image;
+    }
+    if (url in urlMap) {
+      return urlMap[url];
+    }
+    return url;
+  };
+  const loadFile = (data: any, glbMap: any) => {
+    if (data.__glbUUID && glbMap) {
+      const glbInfo = glbMap[data.__glbUUID];
+      if (glbInfo) {
+        const { url, name } = glbInfo;
+        const fullUrl = url + name;
+        if (fullUrl in urlMap) {
+          glbInfo.url = filepath(urlMap[fullUrl]);
+          glbInfo.name = filename(urlMap[fullUrl], true);
+        }
+      }
+    }
+    const urlProps = [
+      'url',
+      'imageSource',
+      'skyboxUrl',
+      'hdrUrl',
+      'colorGradingTexture',
+    ];
+    for (const urlProp of urlProps) {
+      if (urlProp in data === false) {
+        continue;
+      }
+      const url = data[urlProp] || '';
+      data[urlProp] = transUrl(url);
+    }
+    if (data.images?.length) {
+      for (const image of data.images) {
+        const { source } = image;
+        image.source = transUrl(source);
+      }
+    }
+  };
+
+  for (const sceneData of scenes) {
+    if (!sceneData) {
+      continue;
+    }
+    const { nodes = [], scene = {}, glbMap = {}, textures = {} } = sceneData;
+    for (const node of [
+      scene,
+      ...nodes,
+      ...Object.keys(textures).map((id) => textures[id]),
+    ]) {
+      loadFile(node, glbMap);
+    }
+  }
+
+  return _data;
+};

+ 180 - 1
src/views/components/Header.vue

@@ -504,7 +504,7 @@ 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 { cdn, upCdn, addCollection } from '@/services/api';
 // import JSZip from 'jszip';
 import axios from 'axios';
 import { switchTheme } from '@/services/theme';
@@ -561,6 +561,7 @@ import Pay from './Pay.vue';
 import {getNetJsDiagram} from '@/services/material';
 import { useMeta2dData } from '@/services/common';
 import { upload } from '@/services/file';
+import { load3d } from '@/services/load3d';
 
 const { enterprise } = useEnterprise();
 const router = useRouter();
@@ -813,6 +814,184 @@ const openZip = async (file: File) => {
     return;
   }
 
+  // if (!user.isVip) {
+  //   gotoAccount();
+  //   return;
+  // }
+  if (!user.vip) {
+    MessagePlugin.info('需要开通普通会员~');
+    gotoAccount();
+    return;
+  }
+  MessagePlugin.loading('正在读取...',0);
+  const { default: JSZip } = await import('jszip');
+  const zip = new JSZip();
+  await zip.loadAsync(file,{base64:true});
+  let fileName = file.name.slice(0, -4)
+  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;
+  }
+  const imgReplaceArr = [];
+  const _2dDataMap ={};
+  const _3dDataMap ={};
+  MessagePlugin.loading('正在上传文件...',0);
+  for (const key in zip.files) {
+    if (zip.files[key].dir) {
+      continue;
+    }
+    let _keyLower = key.toLowerCase();
+    if (
+      _keyLower.endsWith('.png') ||
+      _keyLower.endsWith('.svg') ||
+      _keyLower.endsWith('.gif') ||
+      _keyLower.endsWith('.jpg') ||
+      _keyLower.endsWith('.jpeg')
+    ) {
+      let filename = key.substr(key.lastIndexOf('/') + 1);
+      // 上传文件
+      const form = new FormData();
+      form.append('name', filename);
+      form.append('directory', '/大屏/图片/默认');
+      form.append('shared', true + '');
+      form.append('file', await zip.file(key).async('blob'));
+      form.append('conflict', 'new');
+      const result: any = await axios.post('/api/image/upload', form);
+      let arr =  key.split('/');
+      arr.shift();
+      if(arr[0]===fileName){
+        arr.shift();
+      }
+      let _key ='/'+ arr.join('/');
+      if (result) {
+        imgReplaceArr.push({ key: _key, url: result.url });
+      }
+    }else if(_keyLower.endsWith('.json')){
+      let arr =  key.split('/');
+      arr.shift();
+      if(arr[0]===fileName){
+        arr.shift();
+      }
+      let data = await zip.file(key).async('string');
+      if(!arr[0].endsWith('.json')){
+        _2dDataMap[arr[0]] = data;
+      }
+    }else{
+      // console.log("key",key);
+      if(key.indexOf('3d')!==-1&&key.indexOf('files/')===-1){
+        let id_3d =   await load3d(zip,key);
+        if(id_3d){
+          // dataStr = dataStr.replaceAll(key, id_3d);
+          let originId = key.split('3d-')[1];
+          _3dDataMap[originId] = id_3d;
+        }
+      }
+    }
+  }
+
+  //替换2d图片数据
+  imgReplaceArr.forEach((item) => {
+    dataStr = dataStr.replaceAll(item.key, item.url);
+    for (let key in _2dDataMap) {
+      _2dDataMap[key] = _2dDataMap[key].replaceAll(item.key, item.url);
+    }
+  });
+
+  try {
+    let domain = location.origin+ '/view';
+    let data: Meta2dBackData = JSON.parse(dataStr);
+    for(let i=0;i<data.pens.length;i++){
+      const pen = data.pens[i];
+      if(pen.name==='iframe'){
+        let url = pen.iframe.split('?')[1]
+        let params = queryURLParams(url);
+        if(pen.iframe.indexOf('/2d/')!==-1 || pen.iframe.indexOf('/v/')!==-1 || pen.iframe.indexOf('/2d?')!==-1 ||pen.iframe.indexOf('/v?')!==-1){
+          const idata = JSON.parse(_2dDataMap[params.id]);
+          const ret:any = await addCollection('v',{
+            data:idata,
+            image:idata.image||'xxx',
+            name:idata.name,
+            folder:idata.folder,
+            userFlag:2,
+            case:idata.case,
+          });
+          if(!ret){
+            continue;
+          }
+          pen.iframe = `${domain}/v/?id=${ret.id}`;
+        }else if(pen.iframe.indexOf('/3d')!==-1){
+          pen.iframe = `${domain}/3d/?id=${_3dDataMap[params.id]}`;
+        }
+      }
+    }
+    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];
+      }
+      if(!data.background){
+        data.background = '#1e2430';
+      }
+      if(!data.color){
+        data.color = '#bdc7db';
+      }
+
+      if(!data.width){data.width= 1920};
+      if(!data.height){data.height= 1080};
+      dealDataBeforeOpen(data)
+      meta2d.open(data);
+      MessagePlugin.closeAll();
+      MessagePlugin.success('导入成功!');
+    }
+  } catch (e) {
+    return false;
+  }
+};
+
+const _openZip = async (file: File) => {
+  if (!(user && user.id)) {
+    MessagePlugin.warning(noLoginTip);
+    return;
+  }
+
   // if (!user.isVip) {
   //   gotoAccount();
   //   return;