wangfumin 2 ay önce
ebeveyn
işleme
5c72ce57b4

BIN
src/assets/image/otherFile.png


+ 4 - 3
src/helper/mount.ts

@@ -111,11 +111,12 @@ export const quiskMountFactory =
           const ret = ref.submit && ((await ref.submit()) as any);
           if (ret) {
             resolve(ret);
+            console.error('?')
+            destroy();
           } else {
-            resolve(true as any);
+            // 未通过校验或未返回成功,保持弹窗开启,不关闭
+            return;
           }
-          console.error('?')
-          destroy();
         },
       };
 

+ 17 - 1
src/request/index.ts

@@ -49,13 +49,18 @@ axios.interceptors.request.use(async (config) => {
   config.headers.userid = userId;
 
   // 当链接存在 share=1 时,为所有请求头注入 caseId 与 sharePassword
+  // 同时记录是否处于分享模式,以便后续跳过登录校验
+  let isShareMode = false;
   try {
     const currentRoute = router.currentRoute?.value;
     const shareParam: any = currentRoute?.query?.share;
+    console.log('currentRoute', currentRoute, shareParam)
     const isShare = Array.isArray(shareParam)
       ? shareParam.includes("1")
       : shareParam === "1";
 
+    isShareMode = !!isShare;
+
     if (isShare) {
       const caseIdParam: any = currentRoute?.params?.caseId;
       const sharePwdParam: any = currentRoute?.query?.p;
@@ -78,8 +83,19 @@ axios.interceptors.request.use(async (config) => {
   }
 
   const hasIgnore = config.params ? "ingoreRes" in config.params : false;
+  // 分享模式下(share=1 且已注入 sharePassword/caseId),跳过登录校验
+  const shareBypassLogin = (() => {
+    try {
+      const sharePwd = (config.headers as any)?.sharePassword;
+      const caseIdHeader = (config.headers as any)?.caseId;
+      return isShareMode && sharePwd != null && caseIdHeader != null;
+    } catch {
+      return false;
+    }
+  })();
+
   if (!hasIgnore) {
-    if (!token && !~notLoginUrls.indexOf(config.url)) {
+    if (!token && !~notLoginUrls.indexOf(config.url) && !shareBypassLogin) {
       const redirect = window.location.href;
       router.replace({ name: RouteName.login, query: { redirect } });
       throw "用户未登录";

+ 71 - 19
src/router/index.ts

@@ -13,6 +13,8 @@ import {
   getSysSetting,
   updateSysSetting,
 } from "@/request";
+import { ElMessageBox, ElMessage } from "element-plus";
+import md5 from "js-md5";
 
 export * from "./config";
 export * from "./routeName";
@@ -21,6 +23,7 @@ export const router = createRouter({
   routes: routes as any,
 });
 const appId = import.meta.env.VITE_APP_APP
+let verified = 0;
 const modules = import.meta.glob("@/assets/style/theme/*.scss", {
   query: "?inline",
 });
@@ -39,27 +42,76 @@ router.beforeEach((to, from, next) => {
     }
     return;
   }
-  try {
-    axios.get(getSysSetting, {
-      params: {
-        platformKey: appId
-      }
-    }).then(async(data) => {
-      // console.log('路由获取后台当前色', data.data.themeColour)
-      localStorage.setItem('f-themeColour', data.data.themeColour)
-      const key = Object.keys(modules).find((key) =>
-        key.includes(data.data.themeColour)
-      );
-      if (key) {
-        const res1: any = await modules[key]();
-        const res2: any = await import("@/assets/style/public.scss?inline");
-        $style.innerHTML = res1.default + res2.default;
-      }
+  const applyThemeAndNext = () => {
+    try {
+      axios
+        .get(getSysSetting, {
+          params: {
+            platformKey: appId,
+          },
+        })
+        .then(async (data) => {
+          localStorage.setItem('f-themeColour', data.data.themeColour);
+          const key = Object.keys(modules).find((key) =>
+            key.includes(data.data.themeColour)
+          );
+          if (key) {
+            const res1: any = await modules[key]();
+            const res2: any = await import('@/assets/style/public.scss?inline');
+            $style.innerHTML = res1.default + res2.default;
+          }
+          next();
+        })
+        .catch(() => {
+          // 主题加载失败也不阻塞路由
+          next();
+        });
+    } catch (error) {
       next();
-    });
-  } catch (error) {
-    next();
+    }
+  };
+
+  // 分享链接访问:在进入 fireDetails 前校验密码
+  if (to.name === 'fireDetails' && String((to.query as any)?.share || '') === '1') {
+    const linkPwd = String((to.query as any)?.p || '');
+    const caseId = String((to.params as any)?.caseId || '');
+
+    // 若已验证过,则不再弹窗,直接进入
+    if (verified) {
+      return applyThemeAndNext();
+    }
+    const promptAndValidate = async (): Promise<void> => {
+      try {
+        const { value } = await ElMessageBox.prompt('请输入访问密码', '访问校验', {
+          inputType: 'text',
+          inputPlaceholder: '请输入4位数字密码',
+          showClose: false,
+          closeOnClickModal: false,
+          closeOnPressEscape: false,
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+        });
+        const userPwd = String(value || '').trim();
+        console.log('userPwd', userPwd)
+        if (md5(userPwd) !== linkPwd) {
+          ElMessage.error('密码错误');
+          return promptAndValidate();
+        }
+        ElMessage.success('验证成功');
+        // 记录本次分享链接已通过验证,避免后续同路由内切换重复弹窗
+        verified = 1;
+        return applyThemeAndNext();
+      } catch (e) {
+        // 取消输入或关闭弹窗,跳转到无权限页
+        // return next({ name: RouteName.noCase });
+      }
+    };
+
+    return void promptAndValidate();
   }
+
+  // 默认流程:应用主题后进入
+  applyThemeAndNext();
 });
 
 const getConfig = (routes: Routes) => {

+ 1 - 1
src/store/editCsae.ts

@@ -291,7 +291,7 @@ export const getMix3dPaggingOffline = async (params: ScenePaggingParams): Promis
       return { list: [], total: 0 } as PaggingRes<any>;
     }
   }
-  return (await axios.get(getMix3dList, { params })).data as PaggingRes<any>;
+  return (await axios.post(getMix3dList, { params })).data as PaggingRes<any>;
 };
 
 // =============== 现场勘验:照片制卷列表(离线支持) ===============

+ 2 - 0
src/view/case/addPhotoFileAll.vue

@@ -179,6 +179,8 @@ defineExpose<QuiskExpose>({
         };
       });
       await saveOrAndSave({ imgUrls });
+      // 返回真值以便弹窗在 quiskMountFactory 中正确关闭
+      return imgUrls;
     } else {
       ElMessage.warning("上传中,请等候!");
       throw "上传中,请等候!";

+ 8 - 1
src/view/case/newShare.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-form ref="form" label-width="130px" class="new-share">
+  <el-form ref="form" label-width="140px" class="new-share">
     <!-- 在数据大屏可见 -->
     <el-form-item label="在数据大屏可见">
       <el-switch v-model="visibleInDashboard" />
@@ -8,6 +8,9 @@
 
     <!-- 上级组织共享权限 -->
     <el-form-item class="org-share-item" label="上级组织共享权限">
+      <template #label>
+        <span style="color: red;line-height: 36px;">*</span><span>上级组织共享权限</span>
+      </template>
       <div class="org-share-row">
         <el-cascader
           class="org-picker"
@@ -104,6 +107,10 @@ const copyPublicShare = async () => {
 
 // 确认提交:仅触发接口调用与保存设置
 const handleConfirm = async () => {
+  if (!selectedParentId.value) {
+    ElMessage.error("请选择上级组织");
+    return;
+  }
   const isAuthMap: Record<"none" | "read" | "edit", number> = {
     none: 0,
     read: 1,

+ 7 - 2
src/view/case/newphotos/index.vue

@@ -2,11 +2,12 @@
   <div class="photo">
     <div class="left">
       <div class="upload my-photo-upload">
-        <el-button type="primary" @click="addCaseFileHandlerAll">
+        <el-button type="primary" class="preview-btn" @click="addCaseFileHandlerAll">
           上传照片
         </el-button>
         <el-button
           type="primary"
+          class="preview-btn"
           @click="handleSwitchGrid"
           :icon="sortType ? FullScreen : Menu"
           >{{ sortType ? "横排" : "竖排" }}</el-button
@@ -540,7 +541,7 @@ onMounted(() => {
   }
 }
 </style>
-<style scoped>
+<style lang="scss" scoped>
 :global(.body-layer) {
   padding-right: 0 !important;
   overflow: hidden;
@@ -569,4 +570,8 @@ onMounted(() => {
     margin-right: 4px;
   }
 }
+.preview-btn {
+  background-color: transparent;
+  color: var(--primaryColor);
+}
 </style>

+ 31 - 11
src/view/newFireCase/newFireDetails/components/basicInfo.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="basic-info">
     <!-- 展示模式:按来源路由分支展示 -->
-    <div v-if="props.editOrShow === 'show'">
+    <div class="show-view-content" v-if="props.editOrShow === 'show'">
       <!-- criminal 展示 -->
       <div v-if="props.fromRoute === 'criminal'" class="camera-from show-view">
         <div class="form-title">案件信息</div>
@@ -26,7 +26,7 @@
     <!-- 编辑模式:按来源路由分支展示 -->
     <template v-else>
       <!-- criminal 编辑(复原 edit.vue) -->
-      <el-form v-if="props.fromRoute === 'criminal'" ref="form" label-width="84px" class="camera-from">
+      <el-form v-if="props.fromRoute === 'criminal'" ref="form" label-width="96px" class="camera-from">
         <div class="form-title">案件信息</div>
         <el-form-item label="案件名称">
           <el-input v-model="bindFire.caseTitle" maxlength="50" placeholder="请输入案件名称" />
@@ -41,7 +41,7 @@
       </el-form>
 
       <!-- fire 编辑(原有) -->
-      <el-form v-else ref="form" label-width="84px" class="camera-from">
+      <el-form v-else ref="form" label-width="100px" class="camera-from">
         <div class="form-title">案件信息</div>
         <el-form-item label="项目编号" class="mandatory">
           <el-input
@@ -187,6 +187,7 @@ const getSnapshot = () => {
 const autoSave = async () => {
   // criminal 路由不触发 fire 自动保存
   if (props.fromRoute === 'criminal') return;
+  if (props.editOrShow === 'show') return;
   if (!isValidForAutoSave()) return;
   const snapshot = getSnapshot();
   if (snapshot === lastSavedSnapshot) return;
@@ -241,6 +242,7 @@ const getSnapshotCriminal = () => {
 
 const autoSaveCriminal = async () => {
   if (props.fromRoute !== 'criminal') return;
+  if (props.editOrShow === 'show') return;
   if (!isValidForAutoSaveCriminal()) return;
   const snapshot = getSnapshotCriminal();
   if (snapshot === lastSavedSnapshotCriminal) return;
@@ -270,6 +272,7 @@ const triggerAutoSaveCriminal = debounce(() => autoSaveCriminal(), 800);
 
 // 深度监听表单对象;日期单独监听
 watch(bindFire, () => {
+  if (props.editOrShow === 'show') return;
   if (props.fromRoute === 'criminal') {
     triggerAutoSaveCriminal();
   } else {
@@ -277,6 +280,7 @@ watch(bindFire, () => {
   }
 }, { deep: true });
 watch(accidentDate, () => {
+  if (props.editOrShow === 'show') return;
   if (props.fromRoute !== 'criminal') triggerAutoSave();
 });
 
@@ -287,12 +291,14 @@ onMounted(() => {
     if (!title || !String(title).trim()) return;
     if (props.fromRoute === 'criminal') {
       (bindFire.value as any).caseTitle = title;
-      // criminal:重命名后立即自动保存
-      autoSaveCriminal();
+      if (props.editOrShow !== 'show') {
+        autoSaveCriminal();
+      }
     } else {
       (bindFire.value as any).projectName = title;
-      // fire 路由:直接调用 autoSave 立即保存
-      autoSave();
+      if (props.editOrShow !== 'show') {
+        autoSave();
+      }
     }
   };
   window.addEventListener('fireDetails:renameTitle', renameHandler as any);
@@ -303,6 +309,7 @@ onMounted(() => {
 
 defineExpose<QuiskExpose>({
   async submit() {
+    if (props.editOrShow === 'show') return;
     if (props.fromRoute === 'criminal') {
       if (!bindFire.value.caseTitle || !bindFire.value.caseTitle.trim()) {
         ElMessage.error("案件名称不能为空");
@@ -377,16 +384,19 @@ const handleMapConfirm = (LocationInfo: any) => {
 <style scoped lang="scss">
 .basic-info{
   width: 100%;
-  height: calc(100% - 200px);
+  height: calc(100% - 174px);
   background: #f5f7fa;
   padding: 24px 0;
+  .show-view-content{
+    height: calc(100% - 20px);
+  }
 }
 .camera-from {
   width: 60%;
-  height: calc(100% - 140px);
+  height: calc(100% - 166px);
   background: #FFFFFF;
   margin: 0 auto;
-  padding: 48px 100px 48px;
+  padding: 48px 280px 48px 280px;
   overflow: auto;
   .form-title{
     text-align: center;
@@ -401,11 +411,13 @@ const handleMapConfirm = (LocationInfo: any) => {
 </style>
 
 <style scoped lang="scss">
+// todo: 滚动留白
 .show-view {
   .info-row {
     display: flex;
     align-items: center;
-    margin-bottom: 16px;
+    margin-bottom: 40px;
+    // todo: 头部对齐
     .label {
       width: 84px;
       color: rgba(0, 0, 0, 0.85);
@@ -417,4 +429,12 @@ const handleMapConfirm = (LocationInfo: any) => {
     }
   }
 }
+.camera-from{
+  :deep(.el-form-item){
+    margin-bottom: 40px;
+  }
+  :deep(.el-form-item__label){
+    padding-right: 24px;
+  }
+}
 </style>

+ 16 - 4
src/view/newFireCase/newFireDetails/components/headerTop.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="new-header">
+  <div :class="['new-header', {'preview-header': showSave}]">
     <div class="left-title">
       <el-icon v-if="showSave" class="back-icon" @click="$emit('back')">
         <ArrowLeft />
@@ -8,10 +8,11 @@
     </div>
     <div class="right-title" v-if="editOrShow === 'edit'">
       <span class="change-btn" v-if="!showSave" @click="openRenameDialog"><i class="iconfont icon-rename" /></span>
-      <el-button plain v-if="!showSave" type="primary" @click="preview">预览</el-button>
-      <el-button type="primary" v-if="showSave" @click="$emit('save')">保存</el-button>
+      <el-button type="primary" class="preview-btn" v-if="!showSave"  @click="preview">预览</el-button>
+      <el-button type="primary" class="preview-btn" v-if="showSave" @click="$emit('save')">保存</el-button>
       <el-button
         type="primary"
+        class="preview-btn"
         v-if="showSave && exportReady"
         @click="handleExport"
         :loading="!isSenseLoaded"
@@ -167,15 +168,18 @@ const handleExport = () => {
       }
     }
     .edit-title{
+      display: flex;
+      align-items: center;
       margin-right: 16px;
     }
     .line{
+      font-size: 24px;
       margin-left: 16px;
       color: rgba(0,0,0,0.1);
     }
   }
   .change-btn{
-    margin-right: 26px;
+    margin-right: 40px;
     cursor: pointer;
     .icon-rename{
       font-size: 24px;
@@ -183,8 +187,16 @@ const handleExport = () => {
   }
   .right-title{
     font-size: 32px;
+    :deep(.preview-btn){
+      background-color: transparent;
+      color: var(--el-color-primary);
+      width: 88px;
+    }
   }
 }
+.preview-header{
+  border-bottom: 1px solid rgba(0,0,0,0.1);
+}
 .rename-dialog{
   .dialog-content{
     margin: 24px;

+ 31 - 13
src/view/newFireCase/newFireDetails/components/mix3d.vue

@@ -72,15 +72,17 @@
          </div>
        </div>
 
+       <!-- todo: 样式整改 -->
        <el-table
          ref="tableRef"
          :data="tableData"
          height="420"
          style="width: 100%"
+         border
          :row-class-name="rowClassName"
          @selection-change="onSelectionChange"
        >
-         <el-table-column type="selection" width="48" :selectable="isRowSelectable" />
+         <el-table-column type="selection" width="70" :selectable="isRowSelectable" />
          <el-table-column label="标题" min-width="140">
            <template #default="{ row }">
              {{ row.fusionTitle || row.title || row.name }}
@@ -121,7 +123,6 @@
 <script setup lang="ts">
 import { ref, computed, onMounted, nextTick } from "vue";
 import { useRoute, useRouter } from 'vue-router';
-import { DocumentAdd, Edit, Delete, FullScreen } from '@element-plus/icons-vue';
 import { Scene } from '@/store/scene';
 import { SceneTypeDesc } from '@/constant/scene';
 import { getFusionAndSceneList, addMix3dFusionIds, getMix3dPaggingOffline as getMix3dPagging } from '@/store/editCsae';
@@ -371,7 +372,8 @@ onMounted(async () => {
         display: flex;
         align-items: center;
         font-size: 14px;
-        padding: 20px 0 20px 48px;
+        padding: 26px 0 14px 48px;
+        cursor: pointer;
       }
     }
     .scene-list{
@@ -416,11 +418,11 @@ onMounted(async () => {
         }
         .actions{
           position: absolute;
-          right: 20px;
+          right: 48px;
           bottom: 26px;
           display: flex;
           align-items: center;
-          gap: 20px;
+          gap: 32px;
           opacity: 0;
           color: rgba(0,0,0,0.85);
           transition: opacity .2s ease;
@@ -446,7 +448,7 @@ onMounted(async () => {
     background: #FFFFFF;
     .iframe-container{
       width: 100%;
-      height: 80%;
+      height: calc(80% - 48px);
     }
     &.fullscreen{
       position: fixed;
@@ -484,16 +486,11 @@ onMounted(async () => {
     }
   }
 }
-.scene-edit-dialog {
-  :deep(.el-dialog__body) {
-    padding-top: 8px;
-  }
-}
 .dialog-filter-row {
   display: flex;
   align-items: center;
   gap: 16px;
-  margin-bottom: 10px;
+  margin-bottom: 16px;
   .filter-item {
     display: flex;
     align-items: center;
@@ -503,7 +500,6 @@ onMounted(async () => {
     }
   }
 }
-
 /* 已导入的行:选中且置灰不可操作 */
 .is-imported-row {
   opacity: 0.6;
@@ -511,3 +507,25 @@ onMounted(async () => {
 }
 
 </style>
+<style lang="scss">
+.scene-edit-dialog{
+  padding: 0!important;
+}
+.scene-edit-dialog .el-dialog__header{
+  padding: 16px;
+  border-bottom: 1px solid #E4E7ED;
+}
+.scene-edit-dialog .el-dialog__footer{
+  border-top: 1px solid #E4E7ED;
+  padding: 16px;
+}
+.el-dialog__headerbtn{
+  height: 60px!important;
+}
+.scene-edit-dialog .el-dialog__body {
+  padding: 24px 30px;
+}
+.el-table th .cell .el-checkbox {
+  margin-left: 0!important;
+}
+</style>

+ 16 - 15
src/view/newFireCase/newFireDetails/components/otherFiles.vue

@@ -37,24 +37,24 @@
           :key="file.filesId"
         >
           <div class="file-left" @click="handleView(file)">
-            <div class="file-avatar">W</div>
+            <div class="file-avatar"><img src="@/assets/image/otherFile.png" alt="" /></div>
             <span class="file-name" :title="file.filesTitle">{{ file.filesTitle || '未命名' }}</span>
           </div>
           <div class="file-actions" v-if="editOrShow === 'edit'">
             <el-tooltip content="重命名" placement="top">
-              <el-button link @click.stop="handleRename(file)">
-                <i class="iconfont icon-rename" />
-              </el-button>
+              <span link @click.stop="handleRename(file)">
+                <i class="iconfont icon-rename_s" />
+              </span>
             </el-tooltip>
             <el-tooltip content="下载" placement="top">
-              <el-button link @click.stop="handleDownload(file)">
+              <span link @click.stop="handleDownload(file)">
                 <i class="iconfont icon-download" />
-              </el-button>
+              </span>
             </el-tooltip>
             <el-tooltip content="移除" placement="top">
-              <el-button link @click.stop="handleDelete(file)">
+              <span @click.stop="handleDelete(file)">
                 <i class="iconfont icon-CloseCircle" />
-              </el-button>
+              </span>
             </el-tooltip>
           </div>
         </div>
@@ -196,7 +196,7 @@ onMounted(async () => {
   display: flex;
   justify-content: center;
   height: calc(100% - 220px);
-  padding: 0 48px;
+  padding: 0;
 }
 
 .left-panel {
@@ -219,6 +219,7 @@ onMounted(async () => {
     border: 1px solid #D9D9D9;
     border-radius: 6px;
     margin-top: 16px; 
+    background-color: #fff;
   }
   .file-left {
     display: flex;
@@ -228,22 +229,22 @@ onMounted(async () => {
   .file-avatar {
     width: 26px;
     height: 26px;
-    border-radius: 50%;
-    background: #c0c4cc;
-    color: #fff;
-    font-size: 12px;
     display: flex;
     align-items: center;
     justify-content: center;
+    img{
+      width: 100%;
+      height: 100%;
+    }
   }
   .file-name {
-    margin-left: 10px;
+    margin-left: 16px;
     color: #333;
   }
   .file-actions {
     display: flex;
     align-items: center;
-    gap: 6px;
+    gap: 32px;
   }
 }
 

+ 4 - 5
src/view/newFireCase/newFireDetails/components/scene.vue

@@ -466,7 +466,6 @@ onMounted(async () => {
   .let-bar{
     width: 510px;
     height: 100%;
-    border-right: 1px solid #f0f0f0;
     overflow: auto;
     .no-data{
       line-height: 100px;
@@ -525,11 +524,11 @@ onMounted(async () => {
         }
         .actions{
           position: absolute;
-          right: 20px;
+          right: 48px;
           bottom: 26px;
           display: flex;
           align-items: center;
-          gap: 20px;
+          gap: 32px;
           opacity: 0;
           color: rgba(0,0,0,0.85);
           transition: opacity .2s ease;
@@ -554,8 +553,8 @@ onMounted(async () => {
     height: calc(100% - 30px);
     background: #FFFFFF;
     .iframe-container{
-      width: 98%;
-      height: 80%;
+      width: calc(100% - 48px);
+      height: calc(80% - 48px);
     }
     &.fullscreen{
       position: fixed;

+ 28 - 15
src/view/newFireCase/newFireDetails/components/screenShot.vue

@@ -12,7 +12,7 @@
     />
     <div class="left-panel">
       <div class="screen-shot" v-if="editOrShow === 'edit'" @click="startShot">
-        <i class="iconfont icon-record" />开始录制
+        <i class="iconfont record-luzhi icon-record" />开始录制
       </div>
       <!-- 文件列表 -->
       <div class="file-list">
@@ -24,31 +24,33 @@
           <div class="file-left" @click="handleView(file)">
             <div class="file-avatar">
               <img v-if="file.videoFolderCover" :src="file.videoFolderCover" alt="">
-              <i class="iconfont icon-play play-icon" />
+              <div class="zhezhao">
+                <i class="iconfont icon-play play-icon" />
+              </div>
             </div>
             <span class="file-name" :title="file.filesTitle">{{ file.videoFolderName }}</span>
             <span class="file-tip" v-if="isProcessing(file)">(后台正在处理...)</span>
           </div>
           <div class="file-actions" v-if="editOrShow === 'edit' && !isProcessing(file)">
             <el-tooltip content="重命名" placement="top">
-              <el-button link @click.stop="handleRename(file)">
-                <i class="iconfont icon-rename" />
-              </el-button>
+              <span link @click.stop="handleRename(file)">
+                <i class="iconfont icon-rename_s" />
+              </span>
             </el-tooltip>
             <el-tooltip content="继续录制" placement="top">
-              <el-button link @click.stop="handleContinue(file)">
+              <span link @click.stop="handleContinue(file)">
                 <i class="iconfont icon-record" />
-              </el-button>
+              </span>
             </el-tooltip>
             <el-tooltip content="下载" placement="top">
-              <el-button link @click.stop="handleDownload(file)">
+              <span link @click.stop="handleDownload(file)">
                 <i class="iconfont icon-download" />
-              </el-button>
+              </span>
             </el-tooltip>
             <el-tooltip content="移除" placement="top">
-              <el-button link @click.stop="handleDelete(file)">
+              <span link @click.stop="handleDelete(file)">
                 <i class="iconfont icon-CloseCircle" />
-              </el-button>
+              </span>
             </el-tooltip>
           </div>
         </div>
@@ -251,7 +253,7 @@ const handleDelete = async (file: any) => {
   display: flex;
   justify-content: center;
   height: calc(100% - 220px);
-  padding: 0 48px;
+  padding: 0;
   .left-panel {
     display: flex;
     flex-direction: column;
@@ -272,6 +274,7 @@ const handleDelete = async (file: any) => {
       border: 1px solid #D9D9D9;
       border-radius: 6px;
       margin-top: 16px; 
+      background: #fff;
     }
     .file-left {
       display: flex;
@@ -282,7 +285,6 @@ const handleDelete = async (file: any) => {
       position: relative;
       width: 32px;
       height: 32px;
-      background: #c0c4cc;
       color: #fff;
       font-size: 12px;
       display: flex;
@@ -292,6 +294,14 @@ const handleDelete = async (file: any) => {
         width: 100%;
         height: 100%;
       }
+      .zhezhao{
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background: rgba(0, 0, 0, 0.3);
+      }
       .play-icon{
         position: absolute;
         top: 50%;
@@ -306,7 +316,7 @@ const handleDelete = async (file: any) => {
     .file-actions {
       display: flex;
       align-items: center;
-      gap: 6px;
+      gap: 32px;
     }
     .empty-tip {
       text-align: center;
@@ -322,9 +332,12 @@ const handleDelete = async (file: any) => {
     gap: 10px;
     width: 80%;
     height: 100px;
-    background: #FAFAFA;
+    background: #fff;
     border-radius: 2px 2px 2px 2px;
     cursor: pointer;
+    .record-luzhi{
+      font-size: 24px;
+    }
   }
 }
 </style>

+ 9 - 10
src/view/newFireCase/newFireDetails/components/siteInspection.vue

@@ -270,7 +270,7 @@
           />
         </template>
 
-        <!-- 勘验笔录右侧:文本详情或上传文件预览 -->
+        <!-- 勘验笔录右侧:文本详情或上传文件预览 to: line-height不对 -->
         <template v-else-if="activeTab === 'inspection'">
           <div v-if="currentInspection" class="record-content">
             <!-- 上传类型:判断文件名是否为图片,图片则右侧直接展示并可放大;非图片则显示文件名 -->
@@ -289,7 +289,7 @@
                 @close="showViewer = false"
               />
             </template>
-            <!-- 非上传类型:显示勘验笔录文本详情 -->
+            <!-- 非上传类型:显示勘验笔录文本详情 to: line-height不对 -->
             <template v-else>
               <div class="records-preview">
                 <h3 class="title">基本信息</h3>
@@ -939,7 +939,7 @@ watch(selectedExtractId, () => {
 const albumList = ref<{ id?: number; title: string; images: PicItem[] }[]>([]);
 const selectedAlbumIndex = ref(0);
 const currentAlbum = computed(() => albumList.value[selectedAlbumIndex.value]);
-console.log(currentAlbum.value, 999)
+
 const selectAlbum = (idx: number) => (selectedAlbumIndex.value = idx);
 const openAlbumViewer = (index: number) => {
   const list = currentAlbum.value?.images || [];
@@ -1322,7 +1322,7 @@ watch(caseId, () => { loadListsFromTree(); loadAlbum(); });
     .actions{
       display: inline-flex;
       height: 48px;
-      gap: 33px;
+      gap: 32px;
       .action-item {
         display: flex;
         align-items: center;
@@ -1336,16 +1336,15 @@ watch(caseId, () => { loadListsFromTree(); loadAlbum(); });
       color: rgba(0, 0, 0, 0.85);
       font-weight: bold;
       font-family: Microsoft YaHei, Microsoft YaHei;
-      margin-bottom: 12px;
-      padding: 12px 0;
+      margin-bottom: 0px;
+      padding: 24px 0 12px;
     }
     .plus-btn {
       display: inline-flex;
       align-items: center;
-      justify-content: center;
+      justify-content: flex-end;
       width: 28px;
       height: 28px;
-      border: 1px solid #d9d9d9;
       border-radius: 4px;
       cursor: pointer;
       color: rgba(0,0,0,.85);
@@ -1397,9 +1396,9 @@ watch(caseId, () => { loadListsFromTree(); loadAlbum(); });
       }
       .header-actions {
         display: flex;
-        gap: 20px;
+        gap: 32px;
         display: none;
-        padding: 8px 0 0 320px;
+        padding: 8px 0 0 292px;
         transition: opacity .2s ease-in-out;
       }
       &.active .header-actions {

+ 4 - 2
src/view/newFireCase/newFireDetails/editFilePage.vue

@@ -389,7 +389,7 @@ defineExpose({ handleSave });
 .page-body {
   max-width: 1280px;
   margin: 0 auto;
-  padding: 20px 28px;
+  padding: 20px 48px;
   background: #fff;
   overflow: auto;
 
@@ -429,7 +429,9 @@ defineExpose({ handleSave });
 
 .title {
   text-align: center;
-  margin-bottom: 20px;
+  font-size: 24px;
+  font-weight: bold;
+  margin: 44px 0 48px;
 }
 
 .sub-tit {

+ 6 - 0
src/view/newFireCase/newFireDetails/editIndex.vue

@@ -193,3 +193,9 @@ onUnmounted(() => {
   window.removeEventListener('fireDetails:renameTitle', renameHandler as any);
 });
 </script>
+
+<style scoped lang="scss">
+.menu-vertical{
+  height: 88px;
+}
+</style>

+ 5 - 5
src/view/newFireCase/newFireDetails/editInspection.vue

@@ -200,7 +200,7 @@ defineExpose({ handleSave });
 .records {
   max-width: 1280px;
   margin: 0 auto;
-  padding: 20px 28px;
+  padding: 24px 48px;
   background: #fff;
   overflow: auto;
 
@@ -224,20 +224,20 @@ defineExpose({ handleSave });
   }
 }
 
-.title { text-align: center;margin-bottom: 20px; }
+.title { text-align: center;margin: 44px 0 48px; font-size: 24px; font-weight: bold; }
 .sub-tit { display: inline-block; padding-bottom: 20px; }
 
 .detail {
   .con { padding: 20px; background-color: #f5f5f5; }
   .info {
-    .inner { margin-bottom: 20px; display: flex; flex-direction: row; width: 100%; }
-    .sec { flex: 1; display: inline-flex; align-items: center; justify-content: center; }
+    .inner { margin-bottom: 20px; display: flex; flex-direction: row; width: 100%; gap: 40px; }
+    .sec { flex: 1; display: inline-flex; align-items: center; justify-content: center; span{min-width: 60px;} }
   }
 }
 
 .extractUser {
   margin-right: 0px;
-  .line { background-color: #f5f5f5; padding: 15px; width: calc(100% - 30px); display: inline-flex; margin-bottom: 15px; }
+  .line { background-color: #f5f5f5; padding: 15px; width: calc(100% - 30px); display: inline-flex; margin-bottom: 15px;gap: 40px; }
 }
 
 .witnessInfo { background: #f5f5f5; padding: 15px; margin-top: 20px; }

+ 10 - 63
src/view/newFireCase/newFireDetails/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="new-fire-details" v-if="allowEnter">
+  <div class="new-fire-details">
     <!-- 顶部标题栏 -->
     <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 && allowEnter" @close="closeHandler" @append="appendFragment" @playVideo="playVideo"
+  <shot v-if="isShot" @close="closeHandler" @append="appendFragment" @playVideo="playVideo"
     @updateCover="(cover: string) => $emit('updateCover', cover)" @deleteRecord="$emit('delete')" :record="record" />
   <el-dialog
       v-model="previewVisible"
@@ -29,27 +29,13 @@
         <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, updateCaseInfo } from "@/store/case";
@@ -87,18 +73,7 @@ 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 () => {
@@ -114,47 +89,19 @@ const loadCaseInfo = async () => {
   } catch (error) {
     console.error(error);
     const msg = typeof error === 'string' ? error : (error?.msg || error?.message || '');
-    if (msg.includes('未登录') || msg.includes('失效')) {
+    if (!msg || msg.includes('未登录') || msg.includes('失效') || msg.includes('token')) {
+      // 未登录或登录失效:跳转到登录页,并携带当前页作为重定向目标
+      vueRouter.replace({ name: RouteName.login, query: { redirect: route.fullPath } });
       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(() => {
-  // 分享访问控制:带 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);
   
@@ -206,7 +153,6 @@ const appendFragment = (blobs: Blob[]) => {
       createRecordFragment({ url: blob, recordId: current.id })
     )
   );
-  console.log(recordFragments.value, 8888)
   // 触发后端保存与合并
   const files = getRecordFragmentBlobs(current);
   if (!files.length) return;
@@ -391,6 +337,7 @@ const backEditSub = () => {
     .el-menu-item{
       padding: 0;
       margin: 0 24px;
+      font-size: 16px;
     }
     .el-menu-item:not(.is-disabled):hover, .el-menu-item:not(.is-disabled):focus{
       background-color: transparent;

+ 7 - 1
src/view/newFireCase/newFireDetails/showIndex.vue

@@ -71,7 +71,7 @@ const startShot = (payload?: any) => {
   emit("start", payload);
 }
 // 从路由查询参数中获取当前菜单项,如果没有则默认为 'scene'
-let currentMenuKey = ref(route.query.tab as string || 'scene');
+let currentMenuKey = ref(route.query.tab as string || 'info');
 
 // 是否存在“实景三维”数据,用于控制菜单项显示
 const hasScene3D = ref(false);
@@ -226,3 +226,9 @@ const menusAbstract = computed(() => {
   return list.filter(item => item.key !== 'scene' || hasScene3D.value);
 });
 </script>
+
+<style scoped lang="scss">
+.menu-vertical{
+  height: 88px;
+}
+</style>

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

@@ -141,12 +141,60 @@ const submitClick = async () => {
     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);
-      window.localStorage.setItem('token', user.value.token)
-      console.log(url, 888);
-      window.location.replace(url);
+      const rawRedirect = String(params.redirect);
+      let decoded = rawRedirect;
+      try {
+        decoded = decodeURIComponent(rawRedirect);
+      } catch {
+        try {
+          decoded = decodeURI(rawRedirect);
+        } catch {}
+      }
+      let url: URL;
+      try {
+        url = decoded.startsWith("http://") || decoded.startsWith("https://")
+          ? new URL(decoded)
+          : new URL(decoded, window.location.origin);
+      } catch (e) {
+        console.warn("Invalid redirect URL, fallback to dispatch:", decoded, e);
+        router.replace({ name: RouteName.dispatch });
+        return;
+      }
+      const isAbsolute = /^https?:\/\//.test(decoded);
+      const isInternal = !isAbsolute || url.origin === window.location.origin;
+
+      // remove token from appropriate place
+      window.localStorage.setItem('token', user.value.token);
+      if (isInternal) {
+        let targetPath = '';
+        if (url.hash) {
+          const hashStr = url.hash.slice(1); // remove leading '#'
+          const qIndex = hashStr.indexOf('?');
+          if (qIndex > -1) {
+            const basePath = hashStr.slice(0, qIndex);
+            const queryStr = hashStr.slice(qIndex + 1);
+            const usp = new URLSearchParams(queryStr);
+            usp.delete('token');
+            const qs = usp.toString();
+            targetPath = qs ? `${basePath}?${qs}` : basePath;
+          } else {
+            targetPath = hashStr;
+          }
+        } else {
+          const usp = new URLSearchParams(url.search);
+          usp.delete('token');
+          const qs = usp.toString();
+          const pathName = url.pathname;
+          targetPath = qs ? `${pathName}?${qs}` : pathName;
+        }
+        if (!targetPath.startsWith('/')) targetPath = '/' + targetPath;
+        console.log('Internal redirect to', targetPath);
+        router.replace(targetPath);
+      } else {
+        url.searchParams.delete("token");
+        console.log('External redirect to', url.toString());
+        window.location.replace(url.toString());
+      }
     } else {
       // router.replace({ name: RouteName.scene });
       router.replace({ name: RouteName.dispatch });