Ver código fonte

修改火调

wangfumin 4 semanas atrás
pai
commit
806b43aec4

+ 7 - 0
package-lock.json

@@ -19,6 +19,7 @@
         "element-plus": "^2.3.8",
         "html2canvas": "^1.4.1",
         "js-base64": "^3.7.5",
+        "js-md5": "^0.8.3",
         "mime": "^3.0.0",
         "mitt": "^3.0.1",
         "public-service": "file:",
@@ -2447,6 +2448,12 @@
       "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==",
       "license": "BSD-3-Clause"
     },
+    "node_modules/js-md5": {
+      "version": "0.8.3",
+      "resolved": "https://registry.npmmirror.com/js-md5/-/js-md5-0.8.3.tgz",
+      "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==",
+      "license": "MIT"
+    },
     "node_modules/json-parse-better-errors": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
     "element-plus": "^2.3.8",
     "html2canvas": "^1.4.1",
     "js-base64": "^3.7.5",
+    "js-md5": "^0.8.3",
     "mime": "^3.0.0",
     "mitt": "^3.0.1",
     "public-service": "file:",

+ 3 - 3
src/app/cjzfire/view/login/index.vue

@@ -151,9 +151,9 @@ const submitClick = async () => {
       localStorage.setItem("remember", "0");
     }
 
-    const params = strToParams(window.location.search);
-    if ("redirect" in params) {
-      const url = new URL(unescape(params.redirect));
+    const params: any = router.currentRoute.value.query;
+    if ("redirect" in params && params.redirect) {
+      const url = new URL(unescape(params.redirect as string));
       url.searchParams.delete("token");
       url.searchParams.append("token", user.value.token);
       window.localStorage.setItem('token', user.value.token)

+ 4 - 4
src/app/criminal/view/login/index.vue

@@ -123,11 +123,11 @@ const submitClick = async () => {
       localStorage.setItem("remember", "0");
     }
 
-    const params = strToParams(window.location.search);
-    if ("redirect" in params) {
-      const url = new URL(unescape(params.redirect));
+    const params: any = router.currentRoute.value.query;
+    if ("redirect" in params && params.redirect) {
+      const url = new URL(unescape(params.redirect as string));
       url.searchParams.delete("token");
-      url.searchParams.append("token", user.value.token);
+      // url.searchParams.append("token", user.value.token);
       window.localStorage.setItem('token', user.value.token)
       window.location.replace(url);
     } else {

+ 3 - 3
src/app/ga/view/login/index.vue

@@ -143,9 +143,9 @@ const submitClick = async () => {
       localStorage.setItem("remember", "0");
     }
 
-    const params = strToParams(window.location.search);
-    if ("redirect" in params) {
-      const url = new URL(unescape(params.redirect));
+    const params: any = router.currentRoute.value.query;
+    if ("redirect" in params && params.redirect) {
+      const url = new URL(unescape(params.redirect as string));
       url.searchParams.delete("token");
       url.searchParams.append("token", user.value.token);
       window.localStorage.setItem('token', user.value.token)

+ 3 - 3
src/app/xmfire/view/login/index.vue

@@ -148,9 +148,9 @@ const submitClick = async () => {
       localStorage.setItem("remember", "0");
     }
 
-    const params = strToParams(window.location.search);
-    if ("redirect" in params) {
-      const url = new URL(unescape(params.redirect));
+    const params: any = router.currentRoute.value.query;
+    if ("redirect" in params && params.redirect) {
+      const url = new URL(unescape(params.redirect as string));
       url.searchParams.delete("token");
       url.searchParams.append("token", user.value.token);
       window.localStorage.setItem('token', user.value.token)

+ 3 - 1
src/request/config.ts

@@ -25,7 +25,8 @@ import {
   userReg,
   getCaseHasDownloadProcess,
   ffmpegMergeImage,
-  addOrUpdateCaseTabulation
+  addOrUpdateCaseTabulation,
+  sceneDeptShare
 } from "./urls";
 import { recordStatus } from "./urls";
 
@@ -66,6 +67,7 @@ export const PostUrls = [
   getMessageList,
   getSceneList,
   getModelSceneList,
+  sceneDeptShare,
   addOrUpdateCaseTabulation,
 ];
 

+ 33 - 2
src/request/index.ts

@@ -48,10 +48,40 @@ axios.interceptors.request.use(async (config) => {
   config.headers.token = token;
   config.headers.userid = userId;
 
+  // 当链接存在 share=1 时,为所有请求头注入 caseId 与 sharePassword
+  try {
+    const currentRoute = router.currentRoute?.value;
+    const shareParam: any = currentRoute?.query?.share;
+    const isShare = Array.isArray(shareParam)
+      ? shareParam.includes("1")
+      : shareParam === "1";
+
+    if (isShare) {
+      const caseIdParam: any = currentRoute?.params?.caseId;
+      const sharePwdParam: any = currentRoute?.query?.p;
+      const caseIdHeader = Array.isArray(caseIdParam)
+        ? caseIdParam[0]
+        : caseIdParam;
+      const sharePasswordHeader = Array.isArray(sharePwdParam)
+        ? sharePwdParam[0]
+        : sharePwdParam;
+
+      if (caseIdHeader != null) {
+        (config.headers as any).caseId = caseIdHeader;
+      }
+      if (sharePasswordHeader != null) {
+        (config.headers as any).sharePassword = sharePasswordHeader;
+      }
+    }
+  } catch (e) {
+    // 忽略注入错误,保证请求不中断
+  }
+
   const hasIgnore = config.params ? "ingoreRes" in config.params : false;
   if (!hasIgnore) {
     if (!token && !~notLoginUrls.indexOf(config.url)) {
-      router.replace({ name: RouteName.login });
+      const redirect = window.location.href;
+      router.replace({ name: RouteName.login, query: { redirect } });
       throw "用户未登录";
     }
   }
@@ -105,8 +135,9 @@ const responseInterceptor = (res: AxiosResponse<any, any>) => {
       ~unAuthCode.indexOf(res.data.code) ||
       errMsg === "token已经失效,请重新登录"
     ) {
-      // router.replace({ name: RouteName.login });
       getAuth().clear();
+      const redirect = window.location.href;
+      router.replace({ name: RouteName.login, query: { redirect } });
     }
     throw res.data.msg;
   }

+ 2 - 0
src/request/urls.ts

@@ -72,6 +72,8 @@ export const downSceneHash = `/fusion/scene/downMD5`;
 export const checkGenMeshScene = "/fusion/scene/sceneDetail";
 export const genMeshSceneByCloud = "/fusion/scene/buildSceneObj";
 export const getMix3dList = "/fusion/caseFusion/pageList";
+// 场景共享到上级组织
+export const sceneDeptShare = "/fusion/sceneDeptShare/share";
 
 // 统计
 export const sceneStatistics = `/fusion/data/sceneGroupByDept`;

+ 2 - 2
src/view/case/moreMenu.vue

@@ -65,7 +65,7 @@ const menus = computed(() => {
       label: "权限",
       permiss: 'edit',
       onClick: async () => {
-        const scenes = await getCaseSceneList(caseId);
+        // const scenes = await getCaseSceneList(caseId);
         // if (!scenes.length) {
         //   alert("当前案件下无场景,请先添加场景。");
         // } else {
@@ -79,7 +79,7 @@ const menus = computed(() => {
       label: "下载",
       permiss: 'edit',
       onClick: async () => {
-        const scenes = await getCaseSceneList(caseId);
+        // const scenes = await getCaseSceneList(caseId);
         // if (!scenes.length) {
         //   alert("当前案件下无场景,请先添加场景。");
         // } else {

+ 41 - 13
src/view/case/newShare.vue

@@ -40,6 +40,9 @@
     <div v-if="viewScope === 'public'" class="public-share">
       <p class="share-title">分享</p>
       <ShareForm :caseId="caseId" :projectName="projectName" ref="shareRef" />
+      <div class="share-actions">
+        <el-button type="primary" @click="copyPublicShare">复制链接和密码</el-button>
+      </div>
     </div>
   </el-form>
 </template>
@@ -48,10 +51,10 @@
 import { computed, onMounted, ref } from "vue";
 import ShareForm from "./share.vue";
 import { QuiskExpose } from "@/helper/mount";
-import { copyText } from "@/util";
-import { getQuery } from "@/view/case/help";
 import { Organization, getOrganizationTree } from "@/store/organization";
 import { ElMessage } from "element-plus";
+import { axios } from "@/request";
+import { sceneDeptShare } from "@/request/urls";
 
 const props = defineProps<{ caseId: number, projectName?: string }>();
 
@@ -94,19 +97,34 @@ const updateSuperiorValue = (value: string[]) => {
 const viewScope = ref<"login" | "public">("login");
 const shareRef = ref<InstanceType<typeof ShareForm> | null>(null);
 
+// 仅复制公开分享的链接与密码(由子组件进行校验与保存)
+const copyPublicShare = async () => {
+  await (shareRef.value as any)?.submit();
+};
+
+// 确认提交:仅触发接口调用与保存设置
+const handleConfirm = async () => {
+  const isAuthMap: Record<"none" | "read" | "edit", number> = {
+    none: 0,
+    read: 1,
+    edit: 2,
+  };
+  const shareAuthVal = viewScope.value === "login" ? 0 : 1;
+  const mapShowVal = visibleInDashboard.value ? 1 : 0;
+  await axios.post(sceneDeptShare, {
+    deptId: selectedParentId.value,
+    isAuth: isAuthMap[orgSharePerm.value],
+    caseId: props.caseId,
+    shareAuth: shareAuthVal,
+    mapShow: mapShowVal,
+  });
+  ElMessage.success("设置已保存");
+  return "success";
+};
+
 defineExpose<QuiskExpose>({
   async submit() {
-    // 这里只做前端配置与链接复制;实际保存逻辑可按后端接口接入
-    if (viewScope.value === "public") {
-      // 调用子组件的复制逻辑(复制链接+密码)
-      return await (shareRef.value as any)?.submit();
-    } else {
-      // 登录可见:仅复制查看链接
-      const link = getQuery(props.caseId, true);
-      copyText(`链接:${link}`);
-      ElMessage.success("链接复制成功");
-      return `链接:${link}`;
-    }
+    return await handleConfirm();
   },
 });
 
@@ -126,6 +144,9 @@ onMounted(async () => {
 .new-share {
   width: 650px;
   margin: 0 auto;
+  :deep(.el-input__wrapper) {
+    width: 420px;
+  }
 }
 .share-from{
   margin-top: 20px;
@@ -175,4 +196,11 @@ onMounted(async () => {
     text-align: right;
   }
 }
+
+.share-actions {
+  margin-top: 10px;
+  padding-left: 160px;
+  display: flex;
+  gap: 12px;
+}
 </style>

+ 8 - 1
src/view/case/quisk.ts

@@ -14,6 +14,7 @@ import { quiskMountFactory } from "@/helper/mount";
 import { nextTick } from "vue";
 import { axios, checkCaseHasDownload, checkHasDownload } from "@/request";
 import CaseDownload from "./download.vue";
+import SceneShare from "./sceneShare.vue";
 
 export const addCaseFile = quiskMountFactory(AddCaseFile, {
   title: "上传附件",
@@ -64,7 +65,13 @@ export const selectMapImage = quiskMountFactory(SelectMapImage, {
 
 export const shareCase = quiskMountFactory(ShareCase, {
   title: "权限",
-  enterText: "复制链接及密码",
+  enterText: "确定",
+  width: 750,
+})<string>;
+
+export const sceneShare = quiskMountFactory(SceneShare, {
+  title: "权限",
+  enterText: "确定",
   width: 750,
 })<string>;
 

+ 107 - 0
src/view/case/sceneShare.vue

@@ -0,0 +1,107 @@
+<template>
+  <div class="scene-share">
+    <el-form label-width="160px">
+      <el-form-item label="上级组织共享权限">
+        <el-cascader
+          class="org-picker"
+          :modelValue="superiorValue"
+          @update:modelValue="(val: string[]) => updateSuperiorValue(val)"
+          :options="organTrees"
+          :props="{ checkStrictly: true, label: 'name', value: 'id' }"
+          placeholder="上级组织名称"
+        />
+      </el-form-item>
+
+      <el-form-item label="权限">
+        <el-radio-group v-model="isAuth">
+          <el-radio :label="0">不共享</el-radio>
+          <el-radio :label="1">允许上级组织查看</el-radio>
+          <el-radio :label="2">允许上级组织查看和编辑</el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+  </div>
+  
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, ref } from 'vue';
+import { Organization, getOrganizationTree } from '@/store/organization';
+import { QuiskExpose } from '@/helper/mount';
+import { axios } from '@/request';
+import { sceneDeptShare } from '@/request/urls';
+
+const props = defineProps<{ num?: string; isObj?: number; caseId?: number }>();
+
+// 组织树与选择的上级部门
+const organTrees = ref<Organization[]>([]);
+const selectedParentId = ref<string>('');
+
+// 权限:0 不共享;1 查看;2 编辑
+const isAuth = ref<number>(0);
+
+// 根据 id 计算级联选中路径
+const queryPathById = (
+  id: string,
+  current: Organization[]
+): string[] | null => {
+  for (const item of current) {
+    if (item.id === id) {
+      return [item.id];
+    } else if (item.children) {
+      const cItem = queryPathById(id, item.children);
+      if (cItem) {
+        return [item.id, ...cItem];
+      }
+    }
+  }
+  return null;
+};
+
+const superiorValue = computed(() => {
+  return selectedParentId.value
+    ? queryPathById(selectedParentId.value, organTrees.value) || []
+    : [];
+});
+
+const updateSuperiorValue = (value: string[]) => {
+  selectedParentId.value = value[value.length - 1];
+};
+
+defineExpose<QuiskExpose>({
+  async submit() {
+    const payload: any = {
+      deptId: selectedParentId.value,
+      isAuth: isAuth.value,
+    };
+    if (props.caseId) {
+      payload.caseId = props.caseId;
+    } else {
+      payload.num = props.num;
+      payload.isObj = props.isObj;
+    }
+    await axios.post(sceneDeptShare, payload);
+    return 'success';
+  },
+});
+
+onMounted(async () => {
+  // 仅加载组织树,前端不做禁用控制,后端负责校验
+  organTrees.value = await getOrganizationTree('1');
+});
+</script>
+
+<style scoped lang="scss">
+.scene-share {
+  padding: 8px 12px;
+  :deep(.el-radio) {
+    height: 30px;
+  }
+  :deep(.el-input__wrapper) {
+    width: 420px;
+  }
+}
+.org-picker {
+  min-width: 280px;
+}
+</style>

+ 12 - 6
src/view/case/share.vue

@@ -22,11 +22,11 @@
 </template>
 <script setup lang="ts">
 import { appConstant } from "@/app";
-import { computed, onMounted, ref, watchEffect } from "vue";
+import { computed, onMounted, ref } from "vue";
 import { EPSW } from "@/constant/REG";
 import { ElMessage } from "element-plus";
 import { copyText } from "@/util";
-import { getQuery } from "@/view/case/help";
+import md5 from "js-md5";
 import { Fire, getFire } from "@/app/fire/store/fire";
 import { getCaseSharePWD, setCaseSharePWD } from "@/store/case";
 import { QuiskExpose } from "@/helper/mount";
@@ -35,7 +35,11 @@ const props = defineProps<{ caseId: number, projectName?: string }>();
 const randCode = ref("");
 const oldRandCode = ref("");
 
-const shareLink = computed(() => getQuery(props.caseId));
+const shareLink = computed(() => {
+  const base = location.origin;
+  const p = md5(randCode.value || "");
+  return `${base}/#/fireDetails/${props.caseId}?editOrShow=edit&fromRoute=fire&share=1&p=${p}`;
+});
 
 const filterPSW = (val: string) => {
   if (val.length > 4) {
@@ -66,10 +70,12 @@ defineExpose<QuiskExpose>({
   },
 });
 
-watchEffect(async () => {
-  if (shareLink.value) {
+onMounted(async () => {
+  try {
     const code = await getCaseSharePWD({ caseId: props.caseId });
-    oldRandCode.value = randCode.value = code;
+    oldRandCode.value = randCode.value = code || "";
+  } catch (e) {
+    // 忽略错误,保持手动输入密码
   }
 });
 </script>

+ 10 - 4
src/view/newFireCase/dyManager/sceneContent.vue

@@ -35,9 +35,9 @@
       <span class="oper-span"  @click="openSceneUrl(row, OpenType.edit)">
         编辑
       </span>
-      <!-- <span class="oper-span" @click="genMeshScene(row)">
-        生成obj
-      </span> -->
+      <span class="oper-span" @click="genMeshScene(row)">
+        权限
+      </span>
       <span class="oper-span delBtn" @click="delSceneHandler(row)">
         删除
       </span>
@@ -57,7 +57,6 @@ import {
   QuoteSceneStatus,
   delQuoteScene,
   SceneType,
-  genMeshScene,
   LocationEnum,
   copyQuoteScene,
   downQuoteSceneHash,
@@ -67,6 +66,7 @@ import { QuoteSceneStatusDesc } from "@/constant/scene";
 import { OpenType, openSceneUrl } from "../../case/help";
 import { confirm } from "@/helper/message";
 import { sceneDownload } from "./quisk";
+import { sceneShare as openSceneShare } from "@/view/case/quisk";
 import { downSceneHash } from "@/request";
 
 const props = defineProps<{ pagging: ScenePagging }>();
@@ -86,4 +86,10 @@ const downHash = async (scene: QuoteScene) => {
 const sceneDownloadHandler = (scene: QuoteScene) => {
   sceneDownload({ scene });
 };
+
+// 权限弹窗:与 mesh 列表一致,传入 num 与 isObj
+const genMeshScene = async (scene: QuoteScene) => {
+  const isObj = (scene as any).isObj ?? Number(![SceneType.SWSS, SceneType.SWYDSS].includes(scene.type as any));
+  await openSceneShare({ num: scene.num, isObj });
+};
 </script>

+ 10 - 4
src/view/newFireCase/meshManager/sceneContent.vue

@@ -41,9 +41,9 @@
       <span class="oper-span"  @click="openSceneUrl(row, OpenType.edit)">
         编辑
       </span>
-      <!-- <span class="oper-span" @click="genMeshScene(row)">
-        生成obj
-      </span> -->
+      <span class="oper-span" @click="genMeshScene(row)">
+        权限
+      </span>
       <span class="oper-span delBtn" @click="delSceneHandler(row)">
         删除
       </span>
@@ -63,7 +63,6 @@ import {
   QuoteSceneStatus,
   delQuoteScene,
   SceneType,
-  genMeshScene,
   LocationEnum,
   copyQuoteScene,
   downQuoteSceneHash,
@@ -73,6 +72,7 @@ import { QuoteSceneStatusDesc } from "@/constant/scene";
 import { OpenType, openSceneUrl } from "../../case/help";
 import { confirm } from "@/helper/message";
 import { sceneDownload } from "./quisk";
+import { sceneShare as openSceneShare } from "@/view/case/quisk";
 import { downSceneHash } from "@/request";
 
 const props = defineProps<{ pagging: ScenePagging }>();
@@ -92,4 +92,10 @@ const downHash = async (scene: QuoteScene) => {
 const sceneDownloadHandler = (scene: QuoteScene) => {
   sceneDownload({ scene });
 };
+
+// 权限弹窗:传递 num 和 isObj(由行数据或类型推断)
+const genMeshScene = async (scene: QuoteScene) => {
+  const isObj = (scene as any).isObj ?? Number(![SceneType.SWSS, SceneType.SWYDSS].includes(scene.type as any));
+  await openSceneShare({ num: scene.num, isObj });
+};
 </script>

+ 11 - 0
src/view/newFireCase/mix3dManager/sceneContent.vue

@@ -31,6 +31,9 @@
       <span class="oper-span" @click="openSceneUrl(row, OpenType.edit)">
         编辑
       </span>
+      <span class="oper-span" @click="openSceneShare(row)">
+        权限
+      </span>
       <span class="oper-span delBtn" @click="delSceneHandler(row)">
         删除
       </span>
@@ -54,6 +57,7 @@ import { QuoteSceneStatusDesc } from "@/constant/scene";
 import { OpenType, openSceneUrl } from "../../case/help";
 import { confirm } from "@/helper/message";
 import { sceneDownload } from "./quisk";
+import { sceneShare as sceneShareDialog } from "@/view/case/quisk";
 import { downSceneHash } from "@/request";
 
 const props = defineProps<{ pagging: ScenePagging }>();
@@ -76,4 +80,11 @@ const sceneDownloadHandler = (scene: QuoteScene) => {
 const addMix3d = () => {
   console.log(1111)
 };
+
+// 权限:与 meshManager 一致,但改为传入 caseId,且不传 isObj
+const openSceneShare = async (row: QuoteScene | any) => {
+  const caseId = (row && (row.caseId ?? row.fusionId ?? row.id)) as number;
+  if (!caseId) return;
+  await sceneShareDialog({ caseId });
+};
 </script>

+ 15 - 2
src/view/newFireCase/newFireDetails/components/mix3d.vue

@@ -39,8 +39,11 @@
         </el-menu>
       </div>
      <div class="right-bar">
-        <div class="iframe-container">
-          <iframe :key="activeNum" :src="`${url}/code/index.html?caseId=${activeScene?.fusionId}&pure=1&token=${user?.token}#/show/summary`" width="96%" height="96%" frameborder="0"></iframe>
+      <div v-if="activeScene?.fusionId" class="iframe-container">
+        <iframe :key="activeNum" :src="`${url}/code/index.html?caseId=${activeScene?.fusionId}&pure=1&token=${user?.token}#/show/summary`" width="96%" height="96%" frameborder="0"></iframe>
+      </div>
+      <div v-else class="iframe-container-nodata">
+        暂无数据
       </div>
      </div>
      <!-- 导入多元融合弹窗 -->
@@ -347,6 +350,7 @@ onMounted(async () => {
     width: 510px;
     height: 100%;
     overflow: auto;
+    border-right: 1px solid #f0f0f0;
     .import-row{
       .import-btn{
         display: flex;
@@ -454,6 +458,15 @@ onMounted(async () => {
         z-index: 2;
       }
     }
+    .iframe-container-nodata{
+      width: 100%;
+      height: 80%;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      color: #909399;
+    }
   }
 }
 .scene-edit-dialog {

+ 14 - 1
src/view/newFireCase/newFireDetails/components/scene.vue

@@ -43,9 +43,12 @@
      </div>
      <div class="right-bar" :class="{ fullscreen: isFullscreen }" ref="rightBarRef">
         <div class="exit-tip" v-if="isFullscreen && showExitTip">按Esc退出全屏</div>
-        <div class="iframe-container">
+        <div v-if="activeWebSite" class="iframe-container">
           <iframe :src="`${activeWebSite}`" width="100%" height="100%" frameborder="0"></iframe>
         </div>
+        <div v-else class="iframe-container-nodata">
+          暂无数据
+        </div>
       </div>
      </div>
     <!-- 编辑弹窗 -->
@@ -443,6 +446,7 @@ onMounted(async () => {
   .let-bar{
     width: 510px;
     height: 100%;
+    border-right: 1px solid #f0f0f0;
     overflow: auto;
     .import-row{
       .import-btn{
@@ -552,6 +556,15 @@ onMounted(async () => {
         z-index: 4;
       }
     }
+    .iframe-container-nodata{
+      width: 100%;
+      height: 80%;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      color: #909399;
+    }
   }
 }
 .scene-edit-dialog {

+ 1 - 1
src/view/newFireCase/newFireDetails/components/screenShot.vue

@@ -42,7 +42,7 @@
             </el-tooltip>
           </div>
         </div>
-        <div v-if="!files.length" class="empty-tip">暂无文件</div>
+        <div v-if="!files.length" class="empty-tip">暂无录制内容</div>
       </div>
     </div>
   </div>

+ 52 - 1
src/view/newFireCase/newFireDetails/editIndex.vue

@@ -33,7 +33,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, watch } from "vue";
+import { ref, computed, watch, onMounted, onUnmounted } from "vue";
 import { useRoute, useRouter } from 'vue-router';
 import BasicInfo from './components/basicInfo.vue';
 import ScreenShot from './components/screenShot.vue';
@@ -41,6 +41,8 @@ import Scene from './components/scene.vue';
 import Mix3d from './components/mix3d.vue';
 import SiteInspection from './components/siteInspection.vue';
 import OtherFiles from './components/otherFiles.vue';
+import { ElMessage } from "element-plus";
+import { Fire, setFire } from "@/app/fire/store/fire";
 
 const props = defineProps<{
   caseId: number;
@@ -141,4 +143,53 @@ const menus = computed(() => {
     },
   ];
 });
+
+// 监听头部重命名事件:当切换到非“基本信息”标签页时也能保存
+const renameHandler = async (evt: any) => {
+  const newTitle = (evt?.detail?.title || '').trim();
+  if (!newTitle) return;
+
+  // 仅当当前标签不是 info(基本信息)时,在此处处理保存,避免与 basicInfo 的保存重复
+  if (currentMenuKey.value === 'info') return;
+
+  try {
+    if (props.fromRoute === 'fire') {
+      const fireObj = { ...(tempFire.value as any) } as Fire;
+      if (!fireObj || !fireObj.id) {
+        ElMessage.error('保存失败:缺少项目ID');
+        return;
+      }
+      fireObj.projectName = newTitle;
+      await setFire(fireObj);
+      // 保存成功后,派发标题更新事件同步到父组件和头部
+      try {
+        window.dispatchEvent(
+          new CustomEvent('fireDetails:updateTitle', { detail: { title: newTitle } })
+        );
+      } catch (e) {}
+      ElMessage.success('标题已保存');
+    } else if (props.fromRoute === 'criminal') {
+      // 刑事案件:暂时仅更新界面展示;如需持久化,请提供保存接口
+      const recordObj = { ...(tempFire.value as any) };
+      recordObj.caseTitle = newTitle;
+      tempFire.value = recordObj;
+      try {
+        window.dispatchEvent(
+          new CustomEvent('fireDetails:updateTitle', { detail: { title: newTitle } })
+        );
+      } catch (e) {}
+      ElMessage.success('标题已更新');
+    }
+  } catch (e) {
+    console.error('重命名保存失败', e);
+    ElMessage.error('保存失败,请稍后重试');
+  }
+};
+
+onMounted(() => {
+  window.addEventListener('fireDetails:renameTitle', renameHandler as any);
+});
+onUnmounted(() => {
+  window.removeEventListener('fireDetails:renameTitle', renameHandler as any);
+});
 </script>

+ 84 - 22
src/view/newFireCase/newFireDetails/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="new-fire-details">
+  <div class="new-fire-details" v-if="allowEnter">
     <!-- 顶部标题栏 -->
     <headerTop :caseId="caseId" :currentRecord="currentRecord" :editOrShow="editOrShow" :showSave="showSave" @save="saveEditSub" @back="backEditSub" />
     <editFilePage :caseId="caseId" :currentRecord="currentRecord" :editOrShow="editOrShow" ref="editFilePageRef" />
@@ -10,7 +10,7 @@
     <!-- 编辑页 -->
     <editIndex :caseId="caseId" :currentRecord="currentRecord" :fromRoute="fromRoute" :processingIds="processingIds" :recentAddedItem="recentAddedItem" @start="startShot" @playVideo="playVideo" v-else />
   </div>
-  <shot v-if="isShot" @close="closeHandler" @append="appendFragment" @playVideo="playVideo"
+  <shot v-if="isShot && allowEnter" @close="closeHandler" @append="appendFragment" @playVideo="playVideo"
     @updateCover="(cover: string) => $emit('updateCover', cover)" @deleteRecord="$emit('delete')" :record="record" />
   <el-dialog
       v-model="previewVisible"
@@ -29,12 +29,27 @@
         <source :src="palyUrl" />
       </video>
   </el-dialog>
+
+  <!-- 分享访问密码弹窗 -->
+  <el-dialog v-model="passwordDialogVisible" title="请输入访问密码" width="360px" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
+    <div style="padding: 4px 8px;">
+      <el-input v-model="inputPwd" maxlength="4" placeholder="请输入4位数字密码" style="width: 200px;" />
+      <p style="margin-top: 10px; color: #969799;">请填写分享链接设置的4位数字密码</p>
+    </div>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="cancelAccess">取消</el-button>
+        <el-button type="primary" @click="confirmAccess">确定</el-button>
+      </span>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup lang="ts">
 import { ref, computed, onMounted, onUnmounted } from "vue";
 import { ElMessage } from "element-plus";
 import { useRoute, useRouter } from 'vue-router';
+import md5 from 'js-md5';
 import showIndex from './showIndex.vue';
 import editIndex from './editIndex.vue';
 import { copyCase, getCaseSceneList, getCaseInfo, updateCaseInfo } from "@/store/case";
@@ -71,28 +86,75 @@ const pageTitle = computed(() => {
   const cr: any = currentRecord.value || {};
   return cr?.caseTitle || cr?.tmProject?.projectName || '';
 });
+// 分享访问密码校验
+const allowEnter = ref(true);
+const passwordDialogVisible = ref(false);
+const inputPwd = ref('');
+const linkPwd = computed(() => {
+  try {
+    const p = String(route.query.p || '');
+    return p;
+  } catch (e) {
+    return '';
+  }
+});
+
+// 加载案件基本信息
+const loadCaseInfo = async () => {
+  try {
+    const caseInfo = await getCaseInfo(caseId.value!);
+    if (caseInfo && fromRoute.value == 'fire') {
+      currentRecord.value = caseInfo;
+      currentRecord.value.tmProject.mapUrl = caseInfo.mapUrl || '';
+      currentRecord.value.tmProject.latAndLong = caseInfo.latAndLong || '';
+    } else if (fromRoute.value == 'criminal') {
+      currentRecord.value = caseInfo;
+    }
+  } catch (error) {
+    console.error(error);
+    const msg = typeof error === 'string' ? error : (error?.msg || error?.message || '');
+    if (msg.includes('未登录') || msg.includes('失效')) {
+      return;
+    }
+    vueRouter.replace({ name: RouteName.noCase, query: {} });
+  }
+};
+
+const cancelAccess = () => {
+  // 取消访问,返回上一页或者跳转无权限页
+  vueRouter.replace({ name: RouteName.noCase, query: {} });
+};
+const confirmAccess = () => {
+  const userPwd = String(inputPwd.value || '').trim();
+  if (!userPwd || userPwd.length !== 4) {
+    ElMessage.error('请输入4位数字密码');
+    return;
+  }
+  if (md5(userPwd) !== linkPwd.value) {
+    ElMessage.error('密码错误');
+    return;
+  }
+  // 验证通过,关闭弹窗并允许进入页面
+  passwordDialogVisible.value = false;
+  allowEnter.value = true;
+  ElMessage.success('验证成功');
+  // 验证成功后拉取基本信息
+  loadCaseInfo();
+};
 onMounted(() => {
-  setTimeout(async() => {
-    try {
-      console.log(caseId.value, fromRoute.value, 8888)
-      const caseInfo = await getCaseInfo(caseId.value!);
-      if (caseInfo && fromRoute.value == 'fire') {
-        currentRecord.value = caseInfo;
-        currentRecord.value.tmProject.mapUrl = caseInfo.mapUrl || '';
-        currentRecord.value.tmProject.latAndLong = caseInfo.latAndLong || '';
-        console.log(currentRecord.value, 8888)
-      } else if (fromRoute.value == 'criminal') {
-        currentRecord.value = caseInfo;
-      }
-    } catch (error) {
-      console.error(error);
-      // throw "该案件不存在!";
-      //跳转到无权限页面
-      vueRouter.replace({
-        name: RouteName.noCase,
-        query: {}
-      });
+  // 分享访问控制:带 share=1 则弹窗输入密码后再进入
+  try {
+    const shareFlag = String(route.query.share || '') === '1';
+    if (shareFlag) {
+      allowEnter.value = false;
+      passwordDialogVisible.value = true;
     }
+  } catch (e) {}
+
+  // 仅在允许进入时拉取基本信息
+  setTimeout(() => {
+    if (!allowEnter.value) return;
+    loadCaseInfo();
   }, 0);
   
   // 监听标题更新事件:由 basicInfo 自动保存成功派发

+ 6 - 4
src/view/system/login.vue

@@ -138,12 +138,14 @@ const submitClick = async () => {
       localStorage.setItem("remember", "0");
     }
 
-    const params = strToParams(window.location.search);
-    if ("redirect" in params) {
-      const url = new URL(unescape(params.redirect));
+    const params: any = router.currentRoute.value.query;
+    // console.log(params, 999);
+    if ("redirect" in params && params.redirect) {
+      const url = new URL(unescape(params.redirect as string));
       url.searchParams.delete("token");
-      url.searchParams.append("token", user.value.token);
+      // url.searchParams.append("token", user.value.token);
       window.localStorage.setItem('token', user.value.token)
+      console.log(url, 888);
       window.location.replace(url);
     } else {
       // router.replace({ name: RouteName.scene });

+ 6 - 0
yarn.lock

@@ -1140,6 +1140,11 @@ js-base64@^3.7.5:
   resolved "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.7.tgz"
   integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==
 
+js-md5@^0.8.3:
+  version "0.8.3"
+  resolved "https://registry.npmmirror.com/js-md5/-/js-md5-0.8.3.tgz"
+  integrity sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==
+
 json-parse-better-errors@^1.0.1:
   version "1.0.2"
   resolved "https://registry.npmmirror.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz"
@@ -1433,6 +1438,7 @@ proxy-from-env@^1.1.0:
     qrcode.vue "^3.4.1"
     qs "^6.11.2"
     sass "^1.64.2"
+    screenfull "^6.0.2"
     simaqcore "^1.2.0"
     sortablejs "^1.15.2"
     swiper "^11.1.4"