ソースを参照

fix: 断点续传制作

bill 7 ヶ月 前
コミット
75619b55bd

+ 1 - 1
.env.development

@@ -1,5 +1,5 @@
 VITE_APP_APP="fire"
-VITE_SEVER_URL="https://hccj.xjxf.com:18008"
+VITE_SEVER_URL="http://192.168.9.27:1800/"
 # VITE_SEVER_URL="https://xj-mix3d.4dkankan.com"
 VITE_DEVCODE_URL="https://192.168.0.25/code"
 VITE_SWKK_URL="https://test.4dkankan.com"

+ 2 - 0
package.json

@@ -13,6 +13,7 @@
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@element-plus/icons-vue": "^2.1.0",
     "@types/qs": "^6.9.7",
+    "@types/spark-md5": "^3.0.5",
     "@vueuse/components": "^10.11.0",
     "@vueuse/core": "^10.11.0",
     "@vueuse/router": "^10.11.0",
@@ -26,6 +27,7 @@
     "qs": "^6.11.2",
     "sass": "^1.64.2",
     "sortablejs": "^1.15.2",
+    "spark-md5": "^3.0.2",
     "swiper": "^11.1.4",
     "three": "^0.158.0",
     "unplugin-element-plus": "^0.7.2",

+ 12 - 0
pnpm-lock.yaml

@@ -5,6 +5,7 @@ specifiers:
   '@element-plus/icons-vue': ^2.1.0
   '@types/node': ^20.4.5
   '@types/qs': ^6.9.7
+  '@types/spark-md5': ^3.0.5
   '@vitejs/plugin-vue': ^4.2.3
   '@vueuse/components': ^10.11.0
   '@vueuse/core': ^10.11.0
@@ -19,6 +20,7 @@ specifiers:
   qs: ^6.11.2
   sass: ^1.64.2
   sortablejs: ^1.15.2
+  spark-md5: ^3.0.2
   swiper: ^11.1.4
   three: ^0.158.0
   typescript: ^5.0.2
@@ -35,6 +37,7 @@ dependencies:
   '@amap/amap-jsapi-loader': 1.0.1
   '@element-plus/icons-vue': 2.3.1_vue@3.4.31
   '@types/qs': 6.9.15
+  '@types/spark-md5': 3.0.5
   '@vueuse/components': 10.11.0_vue@3.4.31
   '@vueuse/core': 10.11.0_vue@3.4.31
   '@vueuse/router': 10.11.0_t6yugvvfk3qpnriubpgidwndjm
@@ -48,6 +51,7 @@ dependencies:
   qs: 6.12.3
   sass: 1.77.7
   sortablejs: 1.15.2
+  spark-md5: 3.0.2
   swiper: 11.1.4
   three: 0.158.0
   unplugin-element-plus: 0.7.2
@@ -370,6 +374,10 @@ packages:
     resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
     dev: false
 
+  /@types/spark-md5/3.0.5:
+    resolution: {integrity: sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==}
+    dev: false
+
   /@types/web-bluetooth/0.0.16:
     resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
     dev: false
@@ -1139,6 +1147,10 @@ packages:
     resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
     engines: {node: '>=0.10.0'}
 
+  /spark-md5/3.0.2:
+    resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
+    dev: false
+
   /swiper/11.1.4:
     resolution: {integrity: sha512-1n7kbYJB2dFEpUHRFszq7gys/ofIBrMNibwTiMvPHwneKND/t9kImnHt6CfGPScMHgI+dWMbGTycCKGMoOO1KA==}
     engines: {node: '>= 4.7.0'}

+ 189 - 0
src/components/chunk-upload/index.vue

@@ -0,0 +1,189 @@
+<template>
+  <el-upload
+    class="upload-demo"
+    :multiple="false"
+    :limit="1"
+    :accept="api.accept.value"
+    :show-file-list="false"
+    :http-request="() => {}"
+    :file-list="api.fileList.value"
+    :disable="api.percentage.value || disabled"
+    :before-upload="api.upload"
+  >
+    <el-button v-pdpath="'sync'" type="primary">
+      <el-icon><Upload /></el-icon>{{ api.percentage.value ? "文件上传中" : "上传数据" }}
+    </el-button>
+  </el-upload>
+
+  <el-dialog
+    :model-value="!!api.percentage.value"
+    :show-close="false"
+    title="文件上传中"
+    :close-on-click-modal="false"
+  >
+    <el-progress :percentage="api.percentage.value" />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { useUpload } from "@/hook/upload";
+import { computed } from "vue";
+import { uploadIngs } from "./uploading";
+import SparkMD5 from "spark-md5";
+import { axios, initChunkUpload, mergeChunkUpload } from "@/request";
+import { ElMessage } from "element-plus";
+
+const props = defineProps<{
+  maxSize: number;
+  maxParallel?: number;
+  formats: string[];
+  formatDesc: string;
+  disabled?: boolean;
+  attach?: object;
+  customUpload?: boolean;
+}>();
+
+const emit = defineEmits<{ (e: "init", id: number): void; (e: "success"): void }>();
+
+type Chunk = {
+  uploadUrl: string;
+  partNumber: number;
+};
+type Init = {
+  uploadChunkVos: Chunk[];
+  id: number;
+  chunkCount: number;
+  chunkSize: number;
+};
+
+const getFileMd5 = (file: File) => {
+  const fileReader = new FileReader();
+  fileReader.readAsBinaryString(file);
+  const spark = new SparkMD5();
+  return new Promise<string>((resolve) => {
+    fileReader.onload = (e) => {
+      spark.appendBinary(e.target!.result as any);
+      resolve(spark.end());
+    };
+  });
+};
+
+const chunksUpload = async (
+  file: File,
+  attach: any,
+  onPercentage: (percentage: number) => void
+) => {
+  const fileMd5 = await getFileMd5(file);
+  if (attach.oldMd5 && attach.oldMd5 !== fileMd5) {
+    ElMessage.error("请选择与第一次上传相同的文件!");
+    throw "请选择与第一次上传相同的文件!";
+  }
+  const { data: init } = await axios.post<Init>(initChunkUpload, {
+    fileMd5,
+    fileName: file.name,
+    fileSize: file.size,
+    ...attach,
+  });
+  emit("init", init.id);
+  const chunkCount = init.chunkCount;
+  const chunkSize = init.chunkSize;
+  const chunks = init.uploadChunkVos;
+  const chunkLen = chunks.length;
+  const chunkProgress = chunks.map(() => 0);
+  const fileSize = file.size;
+  const parallelCount = props.maxParallel || 1;
+  let merge = false;
+
+  const setPercentage = () => {
+    let p = 1 - chunkLen / chunkCount;
+    p += chunkProgress.reduce((t, c) => t + c / chunkLen, 0);
+    p += merge ? 0 : -0.1;
+    p = Math.min(1, Math.max(0, p));
+    onPercentage(Math.ceil(p * 100));
+    uploadIngs[init.id] = p;
+  };
+
+  setPercentage();
+
+  const uploadChunks = (chunks: Chunk[], ndx: number) => {
+    return chunks.map((item, i) => {
+      const start = (item.partNumber - 1) * chunkSize;
+      let end = start + chunkSize;
+      end = end + chunkSize > fileSize ? fileSize : end;
+      let done = false;
+      return axios({
+        method: "PUT",
+        url: item.uploadUrl,
+        data: file.slice(start, end),
+        onUploadProgress(event) {
+          chunkProgress[ndx + i] = done
+            ? 1
+            : Math.min(1, Math.round(event.loaded / (event.total || 1)));
+          setPercentage();
+        },
+      }).then(() => {
+        done = true;
+      });
+    });
+  };
+
+  const groupSize = Math.ceil(chunkLen / parallelCount);
+  let ndx = 0;
+  for (let i = 0; i < groupSize; i++) {
+    const chunkGroup = chunks.slice(
+      i * parallelCount,
+      Math.min((i + 1) * parallelCount, chunks.length)
+    );
+    console.log(chunkGroup);
+    await Promise.all(uploadChunks(chunkGroup, i * parallelCount));
+  }
+  console.log(chunkProgress);
+
+  setPercentage();
+  await axios.post(mergeChunkUpload, {
+    id: init.id,
+    fileMd5: fileMd5,
+    fileName: file.name,
+  });
+  merge = true;
+  setPercentage();
+
+  await new Promise((r) => setTimeout(r, 1000));
+  uploadIngs[init.id] = 1;
+  emit("success");
+};
+
+const expose = {
+  file: null,
+  upload: () => {},
+} as any;
+const api = computed(() =>
+  useUpload({
+    maxSize: props.maxSize,
+    formats: props.formats,
+    upload: (file, onPercentage) => {
+      if (!props.customUpload) {
+        return chunksUpload(file, props.attach || {}, onPercentage);
+      } else {
+        expose.file = file;
+        return new Promise<string>((resolve, reject) => {
+          expose.upload = async () => {
+            try {
+              await chunksUpload(file, props.attach || {}, onPercentage);
+            } catch (e) {
+              reject(e);
+              throw e;
+            }
+
+            delete expose.file;
+            delete expose.upload;
+            resolve("success");
+          };
+        });
+      }
+    },
+  })
+);
+
+defineExpose(expose);
+</script>

+ 3 - 0
src/components/chunk-upload/uploading.ts

@@ -0,0 +1,3 @@
+import { reactive } from "vue";
+
+export const uploadIngs: Record<string, number> = reactive({});

+ 10 - 4
src/constant/scene.ts

@@ -9,6 +9,8 @@ export const SceneTypeDesc: { [key in SceneType]: string } = {
   [SceneType.SWSSMX]: "激光转台Mesh场景",
   [SceneType.SWYDSS]: "激光移动点云场景",
   [SceneType.SWYDMX]: "激光移动Mesh场景",
+  [SceneType.C_SWKK]: "Mesh场景",
+  [SceneType.C_SWSS]: "点云场景",
   // [SceneType.QJKK]: '全景看看',
 };
 
@@ -20,6 +22,8 @@ export const SceneTypeDomain: { [key in SceneType]: string } = {
   [SceneType.SWSSMX]: window.location.href,
   [SceneType.SWYDSS]: window.location.href,
   [SceneType.SWYDMX]: window.location.href,
+  [SceneType.C_SWKK]: window.location.href,
+  [SceneType.C_SWSS]: window.location.href,
 };
 
 export const SceneTypePaths: { [key in SceneType]: string[] } = {
@@ -29,7 +33,9 @@ export const SceneTypePaths: { [key in SceneType]: string[] } = {
     `/livestream/fd/${appConstant.name}.html`,
   ],
   [SceneType.SWKJ]: ["/swkk/spg.html", "/swkk/epg.html"],
+  [SceneType.C_SWKK]: ["/swkk/spg.html", "/swkk/epg.html"],
   [SceneType.SWSS]: ["/swss/index.html", "/swss/index.html"],
+  [SceneType.C_SWSS]: ["/swss/index.html", "/swss/index.html"],
   [SceneType.SWMX]: import.meta.env.DEV
     ? ["/dev-code/index.html", "/dev-code/index.html"]
     : ["/code/index.html", "/code/index.html"],
@@ -50,13 +56,13 @@ export const QuoteSceneStatusDesc: { [key in QuoteSceneStatus]: string } = {
 
 export const ModelSceneStatusDesc: { [key in ModelSceneStatus]: string } = {
   [ModelSceneStatus.CANCEL]: "已取消",
-  [ModelSceneStatus.ERR]: "上传失败",
-  [ModelSceneStatus.RUN]: "上传中",
+  [ModelSceneStatus.ERR]: "转换失败",
+  [ModelSceneStatus.RUN]: "转换中",
   [ModelSceneStatus.REV]: "转换中",
   [ModelSceneStatus.SUCCESS]: "成功",
 };
 
 // export const ModelSupportType = ["obj", "ply", "las", "osgb", "b3dm", "laz"];
-export const ModelSupportType = ["obj", "ply", "las",  "b3dm", "laz"];
+export const ModelSupportType = ["obj", "ply", "las", "b3dm", "laz"];
 export const ModelSupportFormats = [".zip"];
-export const ModelMaxSize = 1024 * 1024 * 1024;
+export const ModelMaxSize = 1024 * 1024 * 1024 * 1024;

+ 1 - 1
src/hook/upload.ts

@@ -8,7 +8,7 @@ export type UploadProps<T> = {
   upload?: (
     file: File,
     onPercentage: (percentage: number) => void
-  ) => Promise<T>;
+  ) => Promise<any>;
 };
 
 const defaultUpload = (

+ 7 - 9
src/request/index.ts

@@ -26,7 +26,7 @@ export type AuthHook = () => {
   clear: () => void;
 };
 export const setAuthHook = (hook: AuthHook) => (getAuth = hook);
-let getAuth: AuthHook = () => ({ token: "", userId: "0", clear: () => { } });
+let getAuth: AuthHook = () => ({ token: "", userId: "0", clear: () => {} });
 
 axios.defaults.baseURL = baseURL;
 
@@ -74,15 +74,13 @@ axios.interceptors.request.use(async (config) => {
     const fromData = new FormData();
 
     Object.keys(config.data).forEach((key) => {
-      if (key === 'files') {
-        Array.from(config.data[key]).forEach(file => {
-          fromData.append('files', file as any as File);
-        })
+      if (key === "files") {
+        Array.from(config.data[key]).forEach((file) => {
+          fromData.append("files", file as any as File);
+        });
       } else {
         fromData.append(key, config.data[key]);
       }
-
-
     });
     config.data = fromData;
     config.headers["Content-Type"] = "multipart/form-data";
@@ -94,9 +92,9 @@ axios.interceptors.request.use(async (config) => {
 
 const responseInterceptor = (res: AxiosResponse<any, any>) => {
   closeLoading();
-  const hasIgnore = res.config.params
+  const hasIgnore = res.config?.params
     ? "ingoreRes" in res.config.params
-    : false;
+    : res.config?.url?.includes("/fusion-chunk/");
   if (!successCode.includes(res.data.code) && !hasIgnore) {
     let errMsg = res.data.msg || res.data.message;
     openErrorMsg(errMsg);

+ 6 - 1
src/request/loading.ts

@@ -4,7 +4,12 @@ import { notOpenUrls } from "./config";
 let loading: ReturnType<typeof ElLoading.service> | null;
 
 export const openLoading = (url?: string) => {
-  if (loading || (url && ~notOpenUrls.indexOf(url))) return;
+  if (
+    loading ||
+    (url && ~notOpenUrls.indexOf(url)) ||
+    url?.includes("/fusion-chunk/")
+  )
+    return;
 
   loading = ElLoading.service({
     lock: true,

+ 7 - 0
src/request/urls.ts

@@ -6,6 +6,11 @@ export const getListByDeptId = "/fusion-xj/web/role/getAllRoleList";
 
 /** ------------------------------------------ */
 
+/** -------分片上传------ */
+export const initChunkUpload = `/fusion-xj/upload/init-chunk-upload`;
+export const mergeChunkUpload = `/fusion-xj/upload/compose-file`;
+export const getChunkFile = `/fusion-xj/upload/getByFileMd5`;
+
 /**  ----------------用户接口----------------   */
 // 登录
 export const userLogin = "/fusion-xj/fdLogin";
@@ -270,3 +275,5 @@ export const cameraVersionAppList = `/fusion-xj/cameraVersionApp/list`;
 
 //相片合成
 export const ffmpegMergeImage = `/fusion-xj/caseImg/ffmpegImage`;
+
+// export const notResDecUrls = [initChunkUpload]

+ 6 - 5
src/store/case.ts

@@ -21,7 +21,7 @@ import {
   copyExample,
   saveCaseImgTag,
   getCaseImgTag,
-  ffmpegMergeImage
+  ffmpegMergeImage,
 } from "@/request";
 import { ModelScene, QuoteScene, Scene, SceneType } from "./scene";
 import { CaseFile } from "./caseFile";
@@ -55,7 +55,6 @@ export type AllSaveFile = {
   imgUrls: filesItem[];
 };
 
-
 export const setCaseSharePWD = (params: { caseId: number; randCode: string }) =>
   axios.post(setCasePsw, params);
 
@@ -89,6 +88,8 @@ export const getSceneKey = (scene: Scene) =>
 export type CaseScenes = { type: SceneType; numList: (string | number)[] }[];
 export const getCaseScenes = (scenes: Scene[]) => {
   const typeIdents = [
+    { type: SceneType.C_SWKK, numList: [] },
+    { type: SceneType.C_SWSS, numList: [] },
     { type: SceneType.SWKJ, numList: [] },
     { type: SceneType.SWKK, numList: [] },
     { type: SceneType.SWMX, numList: [] },
@@ -146,7 +147,7 @@ export const exportCaseDetailInfo = (caseId: number) =>
     responseType: "blob",
   });
 
-// 
+//
 
 export const saveCaseImgTagData = (params: any) =>
   axios.post(saveCaseImgTag, { ...params });
@@ -154,5 +155,5 @@ export const saveCaseImgTagData = (params: any) =>
 export const getCaseImgTagData = (caseId: number) =>
   axios.get(getCaseImgTag, { params: { caseId } });
 
-
-export const submitMergePhotos = (data) => axios.post(ffmpegMergeImage, { ...data })
+export const submitMergePhotos = (data) =>
+  axios.post(ffmpegMergeImage, { ...data });

+ 29 - 0
src/store/scene.ts

@@ -27,6 +27,19 @@ interface BaseScene {
   time: string;
   type: SceneType;
   id: string;
+  uploadStatus: UploadStatus;
+  fileMd5: string;
+  fileType: ZipType;
+}
+
+export enum ZipType {
+  f_e57 = 0,
+  f_e57_s = 1,
+  n_e57 = 2,
+  a_az = 3,
+  m_obj = 4,
+  m_cot = 5,
+  m_qx = 6,
 }
 
 // 只有当location 为4 时,才能生成obj
@@ -53,6 +66,18 @@ export type QuoteScene = BaseScene & {
   status: QuoteSceneStatus;
 };
 
+export enum UploadStatus {
+  ING = 0,
+  SUCCESS = 1,
+  ERRPR = -1,
+}
+
+export const UploadStatusDesc = {
+  [UploadStatus.ING]: "上传中",
+  [UploadStatus.SUCCESS]: "上传成功",
+  [UploadStatus.ERRPR]: "上传失败",
+};
+
 // 普通场景状态
 export enum QuoteSceneStatus {
   DEL = -1,
@@ -73,6 +98,7 @@ export interface ModelScene extends BaseScene {
   modelSize: string;
   modelDateType: string;
   progress?: number;
+
   createTime: string;
 }
 
@@ -87,6 +113,9 @@ export enum SceneType {
 
   SWYDSS,
   SWYDMX,
+
+  C_SWKK,
+  C_SWSS,
 }
 
 // 模型场景状态

+ 4 - 2
src/view/vrmodel/list.vue

@@ -35,10 +35,12 @@ defineProps<{ params: ReturnType<typeof useScenePaggingParams> }>();
 const headOptions = [
   // { value: SceneType.SWKK, name: SceneTypeDesc[SceneType.SWKK] },
   { value: SceneType.SWKJ, name: SceneTypeDesc[SceneType.SWKJ] },
-  { value: SceneType.SWSS, name: SceneTypeDesc[SceneType.SWSS] },
-  { value: SceneType.SWSSMX, name: SceneTypeDesc[SceneType.SWSSMX] },
+  // { value: SceneType.SWSS, name: SceneTypeDesc[SceneType.SWSS] },
+  // { value: SceneType.SWSSMX, name: SceneTypeDesc[SceneType.SWSSMX] },
   { value: SceneType.SWYDSS, name: SceneTypeDesc[SceneType.SWYDSS] },
   { value: SceneType.SWYDMX, name: SceneTypeDesc[SceneType.SWYDMX] },
+  { value: SceneType.C_SWKK, name: SceneTypeDesc[SceneType.C_SWKK] },
+  { value: SceneType.C_SWSS, name: SceneTypeDesc[SceneType.C_SWSS] },
   { value: SceneType.SWMX, name: SceneTypeDesc[SceneType.SWMX] },
 ];
 </script>

+ 85 - 86
src/view/vrmodel/modelContent.vue

@@ -2,27 +2,9 @@
   <div class="body-head">
     <h3 style="visibility: hidden">场景管理</h3>
 
-    <el-tooltip
-      class="item"
-      effect="dark"
-      :content="`请上传${format}(支持obj/ply/las/laz/b3dm格式的数据),大小在${size}以内 `"
-      placement="bottom-start"
-      ><el-upload
-        class="upload-demo"
-        :multiple="false"
-        :limit="1"
-        :accept="accept"
-        :show-file-list="false"
-        :http-request="() => {}"
-        :file-list="fileList"
-        :disabled="percentage || !operateIsPermissionByPath('sync')"
-        :before-upload="uploadCheck"
-      >
-        <el-button v-pdpath="'sync'" type="primary">
-          <el-icon><Upload /></el-icon>{{ percentage ? "文件上传中" : "上传数据" }}
-        </el-button>
-      </el-upload>
-    </el-tooltip>
+    <el-button @click="uploadHandler" v-pdpath="'sync'" type="primary">
+      <el-icon><Upload /></el-icon>{{ "上传数据" }}
+    </el-button>
   </div>
 
   <el-table
@@ -40,10 +22,26 @@
     <el-table-column label="原始数据格式" prop="modelDateType"></el-table-column>
     <el-table-column label="大小" prop="modelSize"></el-table-column>
     <el-table-column label="上传时间" v-slot:default="{ row }: { row: ModelScene }">
-      {{ getStatusText(row) }}
+      {{ row.createTime }}
+    </el-table-column>
+    <el-table-column label="状态" v-slot:default="{ row }: { row: ModelScene }">
+      {{
+        row.uploadStatus !== UploadStatus.SUCCESS && row.uploadStatus !== null
+          ? "上传失败"
+          : getStatusText(row)
+      }}
     </el-table-column>
+
     <el-table-column label="所属架构" prop="deptName"></el-table-column>
     <el-table-column label="操作" v-slot:default="{ row }" width="350px">
+      <span
+        class="oper-span"
+        @click="contineUpload(row)"
+        v-if="row.uploadStatus !== UploadStatus.SUCCESS && row.uploadStatus !== null"
+      >
+        继续上传
+      </span>
+
       <template v-if="row.createStatus === ModelSceneStatus.SUCCESS">
         <span class="oper-span" @click="downOrigin(row)" v-if="row.fileNewName">
           下载原始资源
@@ -73,15 +71,6 @@
       <span v-else class="oper-span" v-pdpath="['viewaaa']"> 模型转换中… </span>
     </el-table-column>
   </el-table>
-
-  <el-dialog
-    :model-value="!!percentage"
-    :show-close="false"
-    title="文件上传中"
-    :close-on-click-modal="false"
-  >
-    <el-progress :percentage="percentage" />
-  </el-dialog>
 </template>
 
 <script setup lang="ts">
@@ -89,36 +78,35 @@ import {
   ModelSceneStatus,
   ModelScene,
   cancelUploadModelScene,
-  uploadModelScene,
   delModelScene,
-  getModelSceneStatus,
   copyModelScene,
   downModelSceneHash,
+  UploadStatus,
 } from "@/store/scene";
-import {
-  ModelMaxSize,
-  ModelSceneStatusDesc,
-  ModelSupportFormats,
-} from "@/constant/scene";
+import { ModelSceneStatusDesc } from "@/constant/scene";
 import { confirm } from "@/helper/message";
-import { useUpload } from "@/hook/upload";
 import { ScenePagging } from "./pagging";
-import { watchPolling } from "@/hook/watchPolling";
 import { OpenType, openSceneUrl } from "../case/help";
-import { operateIsPermissionByPath } from "@/directive/permission";
-import { editModelScene } from "./quisk";
+import { editModelScene, uploadScene } from "./quisk";
 import saveAs from "@/util/file-serve";
+import { watchEffect } from "vue";
 
 const props = defineProps<{ pagging: ScenePagging }>();
 
 const getStatusText = (scene: ModelScene) => {
   let desc = ModelSceneStatusDesc[scene.createStatus];
-  if (scene.createStatus === ModelSceneStatus.RUN && scene.progress) {
-    desc += ` ${scene.progress}% `;
-  } else if (scene.createStatus === ModelSceneStatus.SUCCESS) {
-    desc = scene.createTime;
-  }
   return desc;
+  // if (scene.createStatus === ModelSceneStatus.RUN && scene.progress) {
+  //   desc += ` ${scene.progress}% `;
+  // } else if (scene.createStatus === ModelSceneStatus.SUCCESS) {
+  //   desc = scene.createTime;
+  // }
+  // return desc;
+};
+
+const uploadHandler = async () => {
+  await uploadScene({ type: props.pagging.state.query.type });
+  props.pagging.refresh();
 };
 
 const delOrCancel = async (scene: ModelScene) => {
@@ -131,6 +119,17 @@ const delOrCancel = async (scene: ModelScene) => {
   }
 };
 
+const contineUpload = async (row: ModelScene) => {
+  await uploadScene({
+    type: props.pagging.state.query.type,
+    name: row.modelTitle,
+    fileType: row.fileType,
+    oldMd5: row.fileMd5,
+    id: row.id,
+  });
+  props.pagging.refresh();
+};
+
 const editHanlder = async (scene: ModelScene) => {
   if (await editModelScene({ model: scene })) {
     props.pagging.refresh();
@@ -146,47 +145,47 @@ const downHash = async (scene: ModelScene) => {
   downModelSceneHash(scene);
 };
 
-const {
-  percentage,
-  upload: uploadCheck,
-  fileList,
-  size,
-  format,
-  removeFile,
-  accept,
-} = useUpload({
-  maxSize: ModelMaxSize,
-  formats: ModelSupportFormats,
-  upload: async (file, onPercentage) => {
-    try {
-      await uploadModelScene(file, onPercentage);
-      props.pagging.refresh();
-    } catch {}
-    removeFile();
-  },
-});
-
-// 处理后台正在处理的模型类
-const refreshStatus = (models: ModelScene[]) => {
-  const refreshStatusAll = models.map(async (scene) => {
-    const { status, progress } = await getModelSceneStatus(scene);
-    scene.createStatus = status;
-    scene.progress = progress;
-    if (status == ModelSceneStatus.SUCCESS) {
-      props.pagging.refresh();
-    }
-  });
-  return Promise.all(refreshStatusAll);
-};
-
 const downOrigin = async (model: ModelScene) => {
   await saveAs(model.fileNewName, model.modelTitle + ".zip");
 };
 
-watchPolling(() => {
-  const payload = (props.pagging.state.table.rows as ModelScene[]).filter(
-    (item) => item.createStatus === ModelSceneStatus.RUN
-  );
-  return { start: payload.length > 0, payload };
-}, refreshStatus);
+// 自动刷新状态
+let timeout: any;
+watchEffect(() => {
+  clearTimeout(timeout);
+  const rows = props.pagging.state.table.rows as ModelScene[];
+  const calcIng = rows.some((row) => {
+    if (row.uploadStatus !== UploadStatus.SUCCESS) {
+      return false;
+    }
+    return (
+      row.createStatus === ModelSceneStatus.RUN ||
+      row.createStatus === ModelSceneStatus.REV
+    );
+  });
+  if (calcIng) {
+    timeout = setTimeout(() => {
+      props.pagging.refresh();
+    }, 5000);
+  }
+});
+
+// // 处理后台正在处理的模型类
+// const refreshStatus = (models: ModelScene[]) => {
+//   const refreshStatusAll = models.map(async (scene) => {
+//     const { status, progress } = await getModelSceneStatus(scene);
+//     scene.createStatus = status;
+//     scene.progress = progress;
+//     if (status == ModelSceneStatus.SUCCESS) {
+//       props.pagging.refresh();
+//     }
+//   });
+//   return Promise.all(refreshStatusAll);
+// };
+// watchPolling(() => {
+//   const payload = (props.pagging.state.table.rows as ModelScene[]).filter(
+//     (item) => item.createStatus === ModelSceneStatus.RUN
+//   );
+//   return { start: payload.length > 0, payload };
+// }, refreshStatus);
 </script>

+ 15 - 5
src/view/vrmodel/quisk.ts

@@ -1,28 +1,38 @@
 import { QuoteScene, SceneType } from "@/store/scene";
 import EditModel from "./editModel.vue";
+import Upload from "./upload.vue";
+import type { UploadInfo } from "./upload.vue";
 import SceneDownload from "./sceneDownload.vue";
 import { quiskMountFactory } from "@/helper/mount";
 import { axios, checkHasDownload } from "@/request";
+import { number } from "echarts";
 
 export const editModelScene = quiskMountFactory(EditModel, {
   title: "编辑模型",
   width: 500,
 });
 
+export const uploadScene = quiskMountFactory(Upload, {
+  title: "上传数据",
+  width: 500,
+})<UploadInfo>;
+
 export type SceneDpwnloadProps = { scene: QuoteScene };
-export const sceneDownload = async(props: SceneDpwnloadProps) => {
+export const sceneDownload = async (props: SceneDpwnloadProps) => {
   const params = {
     num: props.scene.num,
-    isObj: Number(![SceneType.SWSS, SceneType.SWYDSS].includes(props.scene.type)),
+    isObj: Number(
+      ![SceneType.SWSS, SceneType.SWYDSS].includes(props.scene.type)
+    ),
   };
   const res = await axios.get(checkHasDownload, { params });
-  const hideFloor = Number(res.data.downloadStatus) !== 3
+  const hideFloor = Number(res.data.downloadStatus) !== 3;
 
   const sceneDownloadDialog = quiskMountFactory(SceneDownload, {
     title: "场景离线包下载",
     width: 500,
     hideFloor: hideFloor,
-    enterText: '下 载'
+    enterText: "下 载",
   });
   return await sceneDownloadDialog(props);
-}
+};

+ 74 - 10
src/view/vrmodel/sceneContent.vue

@@ -1,6 +1,10 @@
 <template>
   <div class="body-head">
     <h3 style="visibility: hidden">场景管理</h3>
+    
+    <el-button @click="uploadHandler" v-pdpath="'sync'" type="primary" v-if="custom">
+      <el-icon><Upload /></el-icon>{{ "上传数据" }}
+    </el-button>
   </div>
 
   <el-table
@@ -16,14 +20,30 @@
       </div>
     </el-table-column>
     <el-table-column label="场景标题" prop="name"></el-table-column>
-    <el-table-column label="S/N码" prop="snCode"></el-table-column>
-    <!-- <el-table-column label="浏览数量" prop="viewCount"></el-table-column> -->
-    <el-table-column label="拍摄时间" prop="createTime" v-slot:default="{ row }">
-      {{ row.createTime.substr(0, 16) }}
-    </el-table-column>
-    <el-table-column label="状态" v-slot:default="{ row }: { row: QuoteScene }">
-      {{ QuoteSceneStatusDesc[row.status] }}
-    </el-table-column>
+
+    <template v-if="custom">
+      <el-table-column label="原始数据格式" prop="name"></el-table-column>
+      <el-table-column label="大小" prop="name"></el-table-column>
+      <el-table-column label="上传时间" prop="name"></el-table-column>
+      <el-table-column label="状态" v-slot:default="{ row }: { row: QuoteScene }">
+        {{
+          row.uploadStatus !== UploadStatus.SUCCESS
+            ? "上传失败"
+            : QuoteSceneStatusDesc[row.status]
+        }}
+      </el-table-column>
+    </template>
+    <template v-else>
+      <el-table-column label="S/N码" prop="snCode"></el-table-column>
+      <!-- <el-table-column label="浏览数量" prop="viewCount"></el-table-column> -->
+      <el-table-column label="拍摄时间" prop="createTime" v-slot:default="{ row }">
+        {{ row.createTime.substr(0, 16) }}
+      </el-table-column>
+      <el-table-column label="状态" v-slot:default="{ row }: { row: QuoteScene }">
+        {{ QuoteSceneStatusDesc[row.status] }}
+      </el-table-column>
+    </template>
+
     <el-table-column label="所属架构" prop="deptName"></el-table-column>
     <el-table-column
       label="操作"
@@ -32,6 +52,13 @@
     >
       <span
         class="oper-span"
+        @click="contineUpload(row)"
+        v-if="row.uploadStatus !== UploadStatus.SUCCESS && custom"
+      >
+        继续上传
+      </span>
+      <span
+        class="oper-span"
         @click="downHash(row)"
         v-if="row.status === QuoteSceneStatus.SUCCESS"
       >
@@ -106,13 +133,15 @@ import {
   LocationEnum,
   copyQuoteScene,
   downQuoteSceneHash,
+  UploadStatus,
+  UploadStatusDesc,
 } from "@/store/scene";
 import { ScenePagging } from "./pagging";
 import { QuoteSceneStatusDesc } from "@/constant/scene";
 import { OpenType, openSceneUrl } from "../case/help";
 import { confirm } from "@/helper/message";
-import { sceneDownload } from "./quisk";
-import { downSceneHash } from "@/request";
+import { sceneDownload, uploadScene } from "./quisk";
+import { computed, watchEffect } from "vue";
 
 const props = defineProps<{ pagging: ScenePagging }>();
 const delSceneHandler = async (scene: QuoteScene) => {
@@ -121,6 +150,9 @@ const delSceneHandler = async (scene: QuoteScene) => {
     props.pagging.refresh();
   }
 };
+const custom = computed(() =>
+  [SceneType.C_SWKK, SceneType.C_SWSS].includes(props.pagging.state.query.type)
+);
 const copySceneHandler = async (scene: QuoteScene) => {
   await copyQuoteScene(scene);
   props.pagging.refresh();
@@ -131,4 +163,36 @@ const downHash = async (scene: QuoteScene) => {
 const sceneDownloadHandler = (scene: QuoteScene) => {
   sceneDownload({ scene });
 };
+const uploadHandler = async () => {
+  await uploadScene({ type: props.pagging.state.query.type });
+  props.pagging.refresh();
+};
+const contineUpload = async (row: QuoteScene) => {
+  await uploadScene({
+    type: props.pagging.state.query.type,
+    name: row.sceneName,
+    fileType: row.fileType,
+    oldMd5: row.fileMd5,
+    id: row.id,
+  });
+  props.pagging.refresh();
+};
+
+// 自动刷新状态
+let timeout: any;
+watchEffect(() => {
+  clearTimeout(timeout);
+  const rows = props.pagging.state.table.rows as QuoteScene[];
+  const calcIng = rows.some((row) => {
+    if (custom.value && row.uploadStatus !== UploadStatus.SUCCESS) {
+      return false;
+    }
+    return row.status === QuoteSceneStatus.RUN;
+  });
+  if (calcIng) {
+    timeout = setTimeout(() => {
+      props.pagging.refresh();
+    }, 5000);
+  }
+});
 </script>

+ 162 - 0
src/view/vrmodel/upload.vue

@@ -0,0 +1,162 @@
+<template>
+  <el-form ref="form" label-width="100px" class="camera-from">
+    <el-form-item label="标题:" class="mandatory">
+      <el-input placeholder="请输入" v-model="info.name" :disabled="props.id" />
+    </el-form-item>
+    <el-form-item label="类型:" class="mandatory" v-if="zipTypeOptions">
+      <el-select
+        v-model="info.fileType"
+        placeholder="请选择"
+        style="width: 100%"
+        :disabled="props.id"
+      >
+        <el-option
+          v-for="item in zipTypeOptions"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="类型:" class="mandatory">
+      <ChunkUpload
+        :formats="ModelSupportFormats"
+        :attach="uploadAttach"
+        custom-upload
+        format-desc=""
+        :max-size="ModelMaxSize"
+        @init="(id) => (info.uploadId = id)"
+        ref="uploadRef"
+      />
+      <p v-if="current" v-html="current.desc" class="upload-desc"></p>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+import { QuiskExpose } from "@/helper/mount";
+import { SceneType, ZipType } from "@/store/scene";
+import ChunkUpload from "@/components/chunk-upload/index.vue";
+import {
+  ModelMaxSize,
+  ModelSupportFormats,
+  QuoteSceneStatusDesc,
+} from "@/constant/scene";
+import { operateIsPermissionByPath } from "@/directive/permission";
+import { computed, reactive, ref, watchEffect } from "vue";
+import { ElMessage } from "element-plus";
+
+const props = defineProps<{
+  type: SceneType;
+  oldMd5?: string;
+  id?: string;
+  name?: string;
+  fileType?: ZipType;
+}>();
+
+const zipTypeOptions = computed(() => {
+  if (props.type === SceneType.C_SWKK) {
+    return [
+      {
+        label: "AZ系列数据包",
+        value: ZipType.a_az,
+        desc:
+          "请上传AZ系列设备导出的原始数据包文件夹使用英文或数字命名。打包成zip格式并上传。",
+      },
+    ];
+  } else if (props.type === SceneType.C_SWSS) {
+    return [
+      {
+        label: "法如e57",
+        value: ZipType.f_e57,
+        desc: "请从Faro Scene导出含全景图的e57数据,打包至zip格式并上传。",
+      },
+      {
+        label: "法如e57加全景图",
+        value: ZipType.f_e57_s,
+        desc:
+          "请从Faro Scene分别导出e57数据及全景图放在同一文件夹,文件夹使用英文或数字命名。打包成zip格式并上传。",
+      },
+      {
+        label: "标准e57",
+        value: ZipType.n_e57,
+        desc:
+          "请将标准e57数据打包文件夹,文件夹使用英文或数字命名,打包.zip格式上传。<a>查看标准e57数据样例</a>",
+      },
+    ];
+  } else if (props.type === SceneType.SWMX) {
+    return [
+      {
+        label: "模型",
+        value: ZipType.m_obj,
+        desc:
+          "请将obj、mtl、贴图文件放在同一文件夹,文件夹使用英文或数字命名,打包成zip格式上传。建议大小在100M内。",
+      },
+      {
+        label: "点云",
+        value: ZipType.m_cot,
+        desc: "请将ply/las/laz点云文件,打包成zip格式上传。大小在50GB以内。",
+      },
+      {
+        label: "倾斜模型",
+        value: ZipType.m_qx,
+        desc: "请将b3dm文件,打包成zip格式上传。大小在50GB以内。",
+      },
+    ];
+  }
+});
+const current = computed(
+  () =>
+    zipTypeOptions.value &&
+    zipTypeOptions.value.find(({ value }) => value === info.fileType)
+);
+const uploadAttach = computed(() => ({
+  fileType: info.fileType,
+  id: props.id,
+  oldMd5: props.oldMd5,
+  title: info.name,
+}));
+
+export type UploadInfo = {
+  name: string;
+  fileType?: ZipType;
+  uploadId: number;
+};
+
+const info = reactive({
+  name: props.name,
+  fileType: props.fileType,
+  uploadId: -1,
+}) as UploadInfo;
+const uploadRef = ref();
+defineExpose<QuiskExpose>({
+  submit: async () => {
+    if (!info.name.trim()) {
+      ElMessage.error("请填写标题");
+      throw "请填写标题";
+    }
+    if (info.fileType === void 0) {
+      ElMessage.error("请选择类型");
+      throw "请选择类型";
+    }
+    if (!uploadRef.value.file) {
+      ElMessage.error("请选择要上传的数据");
+      throw "请选择要上传的文件";
+    }
+    info.uploadId = -1;
+    await uploadRef.value.upload();
+    return info;
+  },
+});
+</script>
+
+<style lang="scss">
+.upload-desc {
+  margin-top: 10px;
+  color: #3d3d3d;
+  a {
+    color: #1890ff;
+    display: block;
+  }
+}
+</style>