1
0
tangning 4 ماه پیش
والد
کامیت
3662f6924e

+ 2 - 0
src/request/config.ts

@@ -23,6 +23,7 @@ import {
   userLogin,
   userReg,
   newFileupload,
+  getByTree,
 } from "./urls";
 
 // 不需要登录就能请求的接口
@@ -35,6 +36,7 @@ export const notLoginUrls = [
   getCompanyList,
   fireDetailByPsw,
   getAttachListByPsw,
+  getByTree,
 ];
 // 需要用表单提交的数据
 export const fromUrls: string[] = [];

+ 19 - 11
src/request/index.ts

@@ -15,6 +15,8 @@ import {
 } from "./config";
 import { router } from "@/router";
 import { RouteName } from "@/router/config";
+// import { loginShow, setLoginShow } from "@/store/system";
+import { loginShow, setLoginShow } from "@/store/system";
 
 export * from "./urls";
 export * from "./config";
@@ -26,6 +28,8 @@ export type AuthHook = () => {
   userId: string;
   clear: () => void;
 };
+export const setLoginHook = (hook: AuthHook) => (getLogin = hook);
+let getLogin = () => ({ loginShow: "", userId: "0", setLoginShow: () => {} });
 export const setAuthHook = (hook: AuthHook) => (getAuth = hook);
 let getAuth: AuthHook = () => ({ token: "", userId: "0", clear: () => {} });
 
@@ -47,7 +51,6 @@ axios.interceptors.request.use(async (config) => {
 
   const { token, userId } = getAuth();
   let caseId = router.currentRoute.value?.params?.caseId
-  console.log("token",localStorage.getItem('token'), token, "userId", userId, config.url);
   if (!token && !~notLoginUrls.indexOf(config.url)) {
     // router.replace({ name: RouteName.login });
     let redirect = encodeURIComponent(`${window.location.href}`);
@@ -95,17 +98,22 @@ axios.interceptors.request.use(async (config) => {
 
 const responseInterceptor = (res: AxiosResponse<any, any>) => {
   closeLoading();
-  if (res.data.code === 4010){
-    ElMessageBox.alert("您没有访问权限", "提示", {
-      confirmButtonText: "我知道了",
-      type: "warning",
-      showClose: false
-    }).then(async () => {
-      window.open(window.location.origin + "/admin/#/statistics/scene");
-    });
+  if (res.data.code === 4010 || res.data.code === 7012){
+    const { setLoginShow, loginShow } = getLogin();
+    console.log("4010",loginShow,'loginShow', res);
+    setLoginShow(true)
+    throw res.data.msg;
+    // console.log("4010",loginShow,'loginShow', res);
+    // setLoginShow(true);
+    // ElMessageBox.alert("您没有访问权限", "提示", {
+    //   confirmButtonText: "我知道了",
+    //   type: "warning",
+    //   showClose: false
+    // }).then(async () => {
+    //   window.open(window.location.origin + "/admin/#/statistics/scene");
+    // });
   };
-  console.log("responseInterceptor", res);
-  if (!successCode.includes(res.data.code) && res.config.responseType != "blob") {
+  if (!successCode.includes(res.data.code) && res.config?.responseType != "blob") {
     let errMsg = res.data.msg || res.data.message;
     openErrorMsg(errMsg);
 

+ 1 - 1
src/request/urls.ts

@@ -8,7 +8,7 @@ export const getListByDeptId = "/fusion/web/role/getAllRoleList";
 
 /**  ----------------用户接口----------------   */
 // 登录
-export const userLogin = "/fusion/fdLogin";
+export const userLogin = "/service/manage/login";
 // 权限
 export const userperInfo = "/fusion/web/user/getPerInfo";
 // export const userInfo = "/fusion/web/user/getUserInfo";

+ 122 - 5
src/store/system.ts

@@ -2,13 +2,109 @@ import {
   axios,
   userLogin,
   uploadFile as uploadFileUrl,
+  setLoginHook,
   userInfo,
 } from "@/request";
 import { encodePwd } from "@/util";
 import { user } from "./user";
 import { refreshRole } from "./role";
+import { changSaveLocal } from "@/util/localUtil";
 import { appConstant } from "@/app";
 import { ref, watchEffect } from "vue";
+function randomWord(randomFlag, min, max?) {
+  let str = '';
+  let range = min;
+  const arr = [
+    '0',
+    '1',
+    '2',
+    '3',
+    '4',
+    '5',
+    '6',
+    '7',
+    '8',
+    '9',
+    'a',
+    'b',
+    'c',
+    'd',
+    'e',
+    'f',
+    'g',
+    'h',
+    'i',
+    'j',
+    'k',
+    'l',
+    'm',
+    'n',
+    'o',
+    'p',
+    'q',
+    'r',
+    's',
+    't',
+    'u',
+    'v',
+    'w',
+    'x',
+    'y',
+    'z',
+    'A',
+    'B',
+    'C',
+    'D',
+    'E',
+    'F',
+    'G',
+    'H',
+    'I',
+    'J',
+    'K',
+    'L',
+    'M',
+    'N',
+    'O',
+    'P',
+    'Q',
+    'R',
+    'S',
+    'T',
+    'U',
+    'V',
+    'W',
+    'X',
+    'Y',
+    'Z',
+  ];
+  // 随机产生
+  if (randomFlag) {
+    range = Math.round(Math.random() * (max - min)) + min;
+  }
+  for (let i = 0; i < range; i++) {
+    const pos = Math.round(Math.random() * (arr.length - 1));
+    str += arr[pos];
+  }
+  return str;
+}
+function encodeStr(str, strv = ''): string {
+  const NUM = 2;
+  const front = randomWord(false, 8);
+  const middle = randomWord(false, 8);
+  const end = randomWord(false, 8);
+
+  const str1 = str.substring(0, NUM);
+  const str2 = str.substring(NUM);
+
+  if (strv) {
+    const strv1 = strv.substring(0, NUM);
+    const strv2 = strv.substring(NUM);
+    return [front + str2 + middle + str1 + end, front + strv2 + middle + strv1 + end];
+  }
+
+  return front + str2 + middle + str1 + end;
+}
 
 export type LoginProps = {
   phoneNum?: string;
@@ -18,6 +114,7 @@ export type LoginProps = {
 };
 
 export const title = ref(appConstant.title);
+export const loginShow = ref(false);
 export const desc = ref(appConstant.desc);
 
 watchEffect(
@@ -29,14 +126,23 @@ const refreshUserInfo = async (data: any) => {
   await refreshRole();
 };
 
-export const login = async (props: LoginProps) => {
+export const setLoginShow = (show: boolean) => {
+  loginShow.value = show;
+}
+
+
+export const login = async (props) => {
   const res = await axios.post(userLogin, {
-    ...props,
-    deptId: appConstant.deptId,
-    password: encodePwd(props.password),
+    userName: props.username,
+    username: props.username,
+    password: encodeStr(window.btoa(props.password))
   });
   user.value.token = res.data.token;
-  refreshUserInfo(res.data.tmUser);
+  changSaveLocal("token", () => res.data.token);
+  // localStorage.setItem("token", JSON.stringfy(user.value.token));
+  debugger
+  loginShow.value = false;
+  // refreshUserInfo(res.data);
 };
 
 if (user.value.token) {
@@ -48,3 +154,14 @@ if (user.value.token) {
 export const uploadFile = async (file: File) => {
   return (await axios.post<string>(uploadFileUrl, { file })).data;
 };
+
+
+// 设置全局请求hook
+setLoginHook(() => {
+  return {
+    loginShow: loginShow.value,
+    setLoginShow: (value) => {
+      loginShow.value = value;
+    },
+  };
+});

+ 1 - 1
src/store/user.ts

@@ -50,7 +50,7 @@ export const getUsers = async (deptId?: string) =>
 // 当前用户的信息
 
 export const user = ref({
-  token: localStorage.getItem("token") || "",
+  token: getLocal("token", {}) || "",
   info: getLocal("info", {} as UserInfo),
 });
 

+ 3 - 0
src/view/case/draw/board/editCAD/Service/FloorplanService.js

@@ -519,6 +519,9 @@ export class FloorplanService {
             img.onload = function () {
                 resolve(img)
             }.bind(this)
+            img.onerror = (e) => {
+                reject(e)
+            }
         })
         return imageData
     }

+ 7 - 2
src/view/case/draw/board/useBoard.ts

@@ -67,8 +67,9 @@ const getStore = async (caseId: number, fileId: number, type: BoardType) => {
       throw "案件不存在";
     }
   } else {
-    const fileInfo = await getCaseFileImageInfo(fileId);
-    if (fileInfo) {
+    try {
+      const fileInfo = await getCaseFileImageInfo(fileId);
+      if (fileInfo) {
       data = {
         ...fileInfo.content,
         ognFilesUrl: fileInfo.ognFilesUrl,
@@ -79,6 +80,10 @@ const getStore = async (caseId: number, fileId: number, type: BoardType) => {
       router.replace({ name: RouteName.caseFile, params: { caseId } });
       throw "该图不存在!";
     }
+    } catch (error) {
+      console.error(error);
+      throw "接口异常";
+    }
   }
   return data;
 };

+ 39 - 10
src/view/case/draw/index.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="df-layout">
+    <login />
     <Header
       class="df-header"
       :type="props.type"
@@ -42,14 +43,15 @@
 import Header from "./header.vue";
 import Slider from "./slider.vue";
 import Eshape from "./eshape.vue";
-import { computed, nextTick, ref } from "vue";
+import login from "./login.vue";
+import { computed, nextTick, ref, watch, onMounted } from "vue";
 import { RouteName, router } from "@/router";
 import { useBoard, title } from "./board/useBoard";
 import { selectFuseImage, selectMapImage } from "@/view/case/quisk";
 import { CaseTagging } from "@/store/caseTagging";
 import saveAs from "@/util/file-serve";
 import { BoardTypeDesc } from "@/constant/caseFile";
-import { addByMediaLiBrary, updateByTreeFileLists, uploadNewFile } from "@/store/case";
+import { addByMediaLiBrary, updateByTreeFileLists, uploadNewFile, getUrlSrc } from "@/store/case";
 import { imageCropper } from "@/view/system/quisk";
 import {
   BoardType,
@@ -65,6 +67,8 @@ const list = ref({
 });
 const fmtId = ref(0);
 const pmtId = ref(0);
+const board = ref(null);
+const state = ref({});
 const ognFilesUrl = ref('')
 const dom = ref<HTMLCanvasElement>();
 const props = computed(() => {
@@ -83,7 +87,7 @@ const props = computed(() => {
     };
   }
 });
-function getList() {
+async function getList() {
   updateByTreeFileLists(props.caseId).then(res => {
     let newlist =  res.find(ele => ele.filesTypeName == '三录材料')?.childrenList || [];
     list.value.xct = newlist.find(ele => ele.filesTypeName == '现场图')?.childrenList || [];
@@ -92,19 +96,26 @@ function getList() {
     console.log('list.value', list.value)
   })
 }
-getList()
+if(pmtId.value || fmtId.value) {
+  const boardData = useBoard(props);
+  board.value = boardData.board;
+  state.value = boardData.state;
+}
 const backPageHandler = () => {
   board.value && board.value.clear();
-  router.back();
+  router.replace({ name: RouteName.material, params: { caseId: props.caseId } });
+  // router.back();
 };
 
 const setBackImage = (blob: Blob) => {
+  console.log('setBackImage', blob, board.value);
   board.value!.setImage(URL.createObjectURL(blob));
 };
 
 const updateAddShape = async (s, d) => {
   if (d) {
-    state.value.addData = await uploadFile(d);
+    state.value.addData = getUrlSrc({type: 102}) + '/' + await uploadFile(d);
+    console.log('state.value', state.value.addData);
   }
   state.value.addShape = s;
 };
@@ -130,9 +141,17 @@ const trackImage = async () => {
     }
   }
 };
+// watch(props, (newValue) => {
+//   if(!newValue) return;
+//   const BoardData = useBoard(props)
+//   board.value = BoardData.board;
+//   state.value = BoardData.state;
+//   console.log('watchEffect111', newValue, props, board.value)
+// // const board = ref(null);
+// // const state = ref(null);
+// })
+console.log('useBoard', board, state)
 
-const { board, state } = useBoard(props);
-console.log('board', board, state, props);
 // 获取通用数据
 const getStore = async () => {
   const store = await board.value!.getStore();
@@ -142,7 +161,14 @@ const getStore = async () => {
   ) as TitleShapeData;
   return { store, titleShape, ognFilesUrl: store.ognFilesUrl };
 };
-
+const isUrl = (string) => {
+        try {
+          new URL(string);
+          return true;
+        } catch (err) {
+          return false;
+        }
+      }
 //裁剪
 const handleCropping = async (data) => {
   const appStore = await getStore();
@@ -197,6 +223,9 @@ const exportHandler = async () => {
   const blob = await board.value!.export();
   saveAs(blob, `${titleShape.text}.jpg`);
 };
+onMounted(() => {
+  getList()
+})
 </script>
 
 <style lang="scss" scoped>
@@ -253,4 +282,4 @@ const exportHandler = async () => {
     height: 100%;
   }
 }
-</style>
+</style>

+ 265 - 0
src/view/case/draw/login.vue

@@ -0,0 +1,265 @@
+<template>
+  <div class="login-layer">
+    <el-dialog
+      v-model="loginShow"
+      width="560"
+      :show-close="false"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :before-close="handleClose"
+    >
+      <div style="text-align: center">
+        <el-form
+          class="panel dialoglogin"
+          ref="formEl"
+          :model="form"
+          :rules="rules"
+          @submit.stop
+        >
+          <h2 class="title1">现场勘查平面图</h2>
+          <div class="title2">支持户型图、平面图绘制</div>
+          <el-form-item class="panel-form-item" prop="account">
+            <p class="err-info">{{ verification.phone }}</p>
+            <el-input
+              :maxlength="11"
+              v-model.trim="form.account"
+              placeholder="账号"
+              @keydown.enter="submitClick"
+            ></el-input>
+          </el-form-item>
+          <el-form-item class="panel-form-item" prop="password">
+            <p class="err-info">{{ verification.psw }}</p>
+            <el-input
+              v-model="form.password"
+              :maxlength="16"
+              placeholder="密码"
+              :type="flag ? 'password' : 'text'"
+              @keydown.enter="submitClick"
+            >
+              <template v-slot:suffix>
+                <img
+                  v-if="flag"
+                  @click="flag = !flag"
+                  style="width: 20px"
+                  src="@/assets/image/pasword.png"
+                  alt=""
+                />
+                <el-icon
+                  :size="20"
+                  @click="flag = !flag"
+                  class="icon-style"
+                  v-else
+                >
+                  <View />
+                </el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+          <el-form-item class="panel-form-item">
+            <el-checkbox v-model="rememberMe" label="记住密码" size="large" />
+          </el-form-item>
+          <!-- <el-form-item class="panel-form-item code-form-item">
+          <p class="err-info">{{ verification.code }}</p>
+          <el-input
+            v-model="form.code"
+            placeholder="验证码"
+            @keydown.enter="submitClick"
+            class="code-input"
+          >
+            <template v-slot:append>
+              <img :src="codeImg" class="code-img" @click="refer" />
+            </template>
+          </el-input>
+        </el-form-item> -->
+
+          <el-form-item class="panel-form-item">
+            <el-button type="primary" class="fill" @click="submitClick"
+              >登录</el-button
+            >
+            <el-button style="margin: 24px 0" class="fill" @click="submitClick"
+              >免费注册</el-button
+            >
+            <el-button style="margin: 0" class="fill" @click="submitClick"
+              >案事件三维重建及数字化建档系统</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <div class="tips">
+            公安部鉴定中心 | 江门市公安局 | 四维时代
+            | 公安部科技强警基础工作计划(2022JC13)
+          </div>
+          <!-- <el-button @click="loginShow = false">Cancel</el-button>
+        <el-button type="primary" @click="loginShow = false">
+          Confirm
+        </el-button> -->
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { reactive, watch, ref, computed, onMounted } from "vue";
+import { openErrorMsg, baseURL, getCode } from "@/request";
+import { PHONE } from "@/constant/REG";
+import { guid, strToParams } from "@/util";
+import { RouteName, router } from "@/router";
+import { login } from "@/store/system";
+import { appConstant } from "@/app";
+import { user } from "@/store/user";
+import { loginShow } from "@/store/system";
+
+// 是否显示明文密码
+const flag = ref(true);
+// 表单
+const form = reactive({
+  account: localStorage.getItem("userName") || "",
+  password: localStorage.getItem("password") || "",
+  code: "",
+  remember: import.meta.env.DEV || localStorage.getItem("remember") === "1",
+});
+const formEl = ref(null);
+const rememberMe = ref(false);
+const verification = reactive({ phone: "", psw: "", code: "" });
+const rules = reactive<FormRules<RuleForm>>({
+  account: [
+    { required: true, message: "请输入账号", trigger: "blur" },
+    { max: 16, message: "最大长度16", trigger: "blur" },
+  ],
+  password: [
+    {
+      required: true,
+      message: "请输入密码",
+      trigger: "change",
+    },
+  ],
+});
+onMounted(() => {
+  let localePass = localStorage.getItem("password");
+  if (localePass) {
+    let password = decodeURIComponent(escape(window.atob(localePass)));
+    form.account = localStorage.getItem("username") || "";
+    form.password = password;
+    rememberMe.value = true;
+  }
+});
+// 验证
+// watch(
+//   form,
+//   () => {
+//     console.log("form", form);
+//     if (!form.account) {
+//       verification.phone = "请输入手机号";
+//     } else if (form.account == "88888888888") {
+//       verification.phone = "";
+//     } else {
+//       verification.phone = PHONE.REG.test(form.account) ? "" : PHONE.tip;
+//     }
+//     if (!form.password) {
+//       verification.psw = "请输入密码";
+//     } else {
+//       verification.psw = "";
+//     }
+//     if (!form.code.trim()) {
+//       verification.code = "请输入验证码";
+//     } else {
+//       verification.code = "";
+//     }
+//   },
+//   { immediate: true }
+// );
+
+// 图片验证码
+const imgKey = ref(guid());
+const refer = () => (imgKey.value = guid());
+const codeImg = computed(() => baseURL + getCode + "?key=" + imgKey.value);
+
+// 表单提交
+const submitClick = async () => {
+  if (!formEl.value) return;
+  await formEl.value.validate(async (valid, fields) => {
+    if (valid) {
+      console.log("submit!");
+      try {
+        await login({
+          username: form.account,
+          password: form.password,
+        });
+
+        if (rememberMe.value) {
+          let password: string = window.btoa(
+            unescape(encodeURIComponent(form.password))
+          );
+          localStorage.setItem("password", password);
+          localStorage.setItem("username", form.account);
+        } else {
+          localStorage.removeItem("password");
+          localStorage.removeItem("username");
+        }
+
+        // const params = strToParams(window.location.search);
+        // if ("redirect" in params) {
+        //   const url = new URL(unescape(params.redirect));
+        //   url.searchParams.delete("token");
+        //   url.searchParams.append("token", user.value.token);
+        //   window.location.replace(url);
+        // } else {
+          location.reload()
+          // router.replace({ name: RouteName.scene, params: { caseId: 360 } });
+        // }
+      } catch (e) {
+        console.error(e);
+        return refer();
+      }
+    } else {
+      console.log("error submit!", fields);
+    }
+  });
+  // if (verification.phone && verification.phone !== "88888888888") {
+  //   return openErrorMsg(verification.phone);
+  // }
+  // if (verification.psw) return openErrorMsg(verification.psw);
+  // if (verification.code) return openErrorMsg(verification.code);
+};
+</script>
+
+<style lang="scss" scoped>
+.login-layer {
+  text-align: center;
+  .dialoglogin {
+    padding: 0 80px;
+  }
+  .title1 {
+    font-weight: bold;
+    font-size: 40px;
+    color: #000000;
+    line-height: 60px;
+    letter-spacing: 8px;
+    font-style: normal;
+  }
+  .title2 {
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    line-height: 30px;
+    font-style: normal;
+    margin-bottom: 32px;
+  }
+}
+.tips {
+  font-weight: 400;
+  font-size: 12px;
+  color: #999999;
+  line-height: 30px;
+  letter-spacing: 1px;
+  text-align: center;
+}
+.content {
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+}
+</style>

+ 6 - 8
src/view/case/records/index.vue

@@ -501,9 +501,9 @@ border: 1px solid #D9D9D9;">
         </div>
       </div>
       <div class="flex-1 text-center content-start" style="border-radius: 0px 0px 0px 0px;
-border: 1px solid #D9D9D9;">
-        <span v-if="aiImgData.loading">识别中</span>
-        <div class="text-left" style="height: 450px;padding: 10px" v-else v-html="aiImgData.result"></div>
+border: 1px solid #D9D9D9;min-height: 450px">
+        <span style="line-height: 450px" v-if="aiImgData.loading">识别中</span>
+        <div class="text-left" style="height: 450px;padding: 10px; overflow: auto;" v-else v-html="aiImgData.result"></div>
       </div>
       </div>
       <template #footer>
@@ -512,7 +512,7 @@ border: 1px solid #D9D9D9;">
             <el-button type="primary" @click="handleAI"> 识别 </el-button>
           </div>
           <div style="width: 50%">
-            <el-button :disabled="aiImgData.result" @click="handleCopy"> 复制 </el-button>
+            <el-button :disabled="!aiImgData.result" @click="handleCopy"> 复制 </el-button>
           </div>
         </div>
       </template>
@@ -634,10 +634,8 @@ const handleAI = async () => {
   try {
   const res = await getAiByImage({imageUrl: 'http://192.168.0.25/oss/scene_view_data/YZL-jm-Z7xsq0T8502/user/floor-cad-0.png'})
   console.log("handleAI", res)
-  aiImgData.value = {
-    result: res,
-    loading: false
-  }
+  aiImgData.value.result = res
+  aiImgData.value.loading = false
   } catch (error) {
     aiImgData.value.loading = false;
   }

+ 2 - 0
src/view/layout/index.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="layer">
+    <!-- <login /> -->
     <ly-top class="top" />
     <div class="content">
       <router-view v-slot="{ Component }" v-if="isSystem">
@@ -56,6 +57,7 @@
 
 <script lang="ts" setup>
 import lyTop from "./top/index.vue";
+import login from "./login.vue";
 import lySlide from "./slide/index.vue";
 import { routeIsSystem, router } from "@/router";
 import { computed, ref, onMounted, watch } from "vue";

+ 283 - 0
src/view/layout/login.vue

@@ -0,0 +1,283 @@
+<template>
+  <div class="login-layer">
+    <el-dialog
+      v-model="loginShow"
+      title="Tips"
+      width="500"
+      :before-close="handleClose"
+    >
+    <span>This is a message</span>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="loginShow = false">Cancel</el-button>
+        <el-button type="primary" @click="loginShow = false">
+          Confirm
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+    <el-form class="panel login" :model="form" @submit.stop>
+        <h2>欢迎登录</h2>
+        <el-form-item class="panel-form-item">
+          <p class="err-info">{{ verification.phone }}</p>
+          <el-input
+            :maxlength="11"
+            v-model.trim="form.phone"
+            placeholder="手机号"
+            @keydown.enter="submitClick"
+          ></el-input>
+        </el-form-item>
+        <el-form-item class="panel-form-item">
+          <p class="err-info">{{ verification.psw }}</p>
+          <el-input
+            v-model="form.psw"
+            :maxlength="16"
+            placeholder="密码"
+            :type="flag ? 'password' : 'text'"
+            @keydown.enter="submitClick"
+          >
+            <template v-slot:suffix>
+              <img
+                v-if="flag"
+                @click="flag = !flag"
+                style="width: 20px; margin: 15px"
+                src="@/assets/image/pasword.png"
+                alt=""
+              />
+              <el-icon :size="20" @click="flag = !flag" class="icon-style" v-else>
+                <View />
+              </el-icon>
+            </template>
+          </el-input>
+        </el-form-item>
+
+        <el-form-item class="panel-form-item code-form-item">
+          <p class="err-info">{{ verification.code }}</p>
+          <el-input
+            v-model="form.code"
+            placeholder="验证码"
+            @keydown.enter="submitClick"
+            class="code-input"
+          >
+            <template v-slot:append>
+              <img :src="codeImg" class="code-img" @click="refer" />
+            </template>
+          </el-input>
+        </el-form-item>
+
+        <el-form-item class="panel-form-item">
+          <el-button type="primary" class="fill" @click="submitClick">登录</el-button>
+        </el-form-item>
+
+        <div class="more">
+          <a @click="$router.push({ name: 'forget' })">忘记密码</a>
+        </div>
+      </el-form>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { reactive, watch, ref, computed } from "vue";
+import { openErrorMsg, baseURL, getCode } from "@/request";
+import { PHONE } from "@/constant/REG";
+import { guid, strToParams } from "@/util";
+import { RouteName, router } from "@/router";
+import { login } from "@/store/system";
+import { appConstant } from "@/app";
+import { user } from "@/store/user";
+import { loginShow } from "@/store/system";
+
+// 是否显示明文密码
+const flag = ref(true);
+// 表单
+const form = reactive({
+  phone: localStorage.getItem("userName") || "",
+  psw: localStorage.getItem("password") || "",
+  code: "",
+  remember: import.meta.env.DEV || localStorage.getItem("remember") === "1",
+});
+const verification = reactive({ phone: "", psw: "", code: "" });
+// 验证
+watch(
+  form,
+  () => {
+    console.log("form", form);
+    if (!form.phone) {
+      verification.phone = "请输入手机号";
+    } else if (form.phone == "88888888888") {
+      verification.phone = "";
+    } else {
+      verification.phone = PHONE.REG.test(form.phone) ? "" : PHONE.tip;
+    }
+    if (!form.psw) {
+      verification.psw = "请输入密码";
+    } else {
+      verification.psw = "";
+    }
+    if (!form.code.trim()) {
+      verification.code = "请输入验证码";
+    } else {
+      verification.code = "";
+    }
+  },
+  { immediate: true }
+);
+
+// 图片验证码
+const imgKey = ref(guid());
+const refer = () => (imgKey.value = guid());
+const codeImg = computed(() => baseURL + getCode + "?key=" + imgKey.value);
+
+// 表单提交
+const submitClick = async () => {
+  if (verification.phone && verification.phone !== "88888888888") {
+    return openErrorMsg(verification.phone);
+  }
+  if (verification.psw) return openErrorMsg(verification.psw);
+  if (verification.code) return openErrorMsg(verification.code);
+
+  try {
+    await login({ phoneNum: form.phone, code: form.code, password: form.psw });
+
+    if (form.remember) {
+      localStorage.setItem("userName", form.phone);
+      localStorage.setItem("password", form.psw);
+      localStorage.setItem("remember", "1");
+    } else {
+      localStorage.setItem("userName", "");
+      localStorage.setItem("password", "");
+      localStorage.setItem("remember", "0");
+    }
+
+    const params = strToParams(window.location.search);
+    if ("redirect" in params) {
+      const url = new URL(unescape(params.redirect));
+      url.searchParams.delete("token");
+      url.searchParams.append("token", user.value.token);
+      window.location.replace(url);
+    } else {
+      router.replace({ name: RouteName.scene, params: { caseId: 360 } });
+    }
+  } catch (e) {
+    console.error(e);
+    return refer();
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.login-layer {
+  text-align: right;
+}
+.content {
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+}
+.info {
+  color: #fff;
+  margin-right: 143px;
+  padding-top: 80px;
+  padding-bottom: 80px;
+  flex: none;
+  text-align: left;
+  img {
+    width: 76px;
+    height: 76px;
+  }
+  h1 {
+    font-size: 2.8rem;
+    line-height: 3.7rem;
+    margin-bottom: 0.7rem;
+  }
+  p {
+    font-size: 2rem;
+    line-height: 2.2rem;
+  }
+}
+
+.top-text {
+  margin-bottom: 50px;
+  pointer-events: none;
+  height: 153px;
+  min-width: 1200px;
+  img {
+    position: absolute;
+    right: 0;
+  }
+}
+.login {
+  width: 320px;
+  padding: 40px 40px 30px;
+  position: relative;
+  display: inline-block;
+
+  h2 {
+    padding-left: 0;
+    padding-bottom: 0;
+    border-bottom: none;
+    margin-bottom: 2.14rem;
+
+    span {
+      color: #646566;
+      font-size: 1.33rem;
+      margin-top: 0.71rem;
+      display: block;
+    }
+  }
+
+  .panel-form-item {
+    padding-left: 0;
+    padding-right: 0;
+    .icon-style {
+      margin-right: 14px;
+      font-size: 20px;
+      line-height: 50px;
+    }
+  }
+
+  .more a:first-child::after {
+    content: "";
+    position: absolute;
+    right: -5px;
+    width: 1px;
+    height: 8px;
+    background: #dcdee0;
+    top: 50%;
+    transform: translateY(-50%);
+  }
+}
+
+.code-img {
+  width: 100%;
+  height: 100%;
+  // object-fit: cover;
+}
+</style>
+
+<style>
+.login .code-form-item .el-input {
+  display: flex;
+}
+
+.login .code-form-item .el-input-group__append {
+  flex: none;
+  margin-left: 10px;
+  width: 95px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+}
+
+.login .code-form-item .el-input__inner {
+  flex: 1;
+}
+.login .code-form-item .el-input-group__append,
+.login .code-form-item .el-input__inner {
+  border-radius: 4px;
+}
+input[type="password"]::-ms-reveal {
+  display: none;
+}
+</style>