gaoluyang
5 天以前 c7566bba45e6f90594d36ec4b80ac4ff89f577ee
src/views/login.vue
@@ -1,71 +1,126 @@
<template>
  <div class="login">
    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">{{ title }}</h3>
      <el-form-item prop="username">
        <el-input
          v-model="loginForm.username"
          type="text"
          size="large"
          auto-complete="off"
          placeholder="账号"
        >
          <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          v-model="loginForm.password"
          type="password"
          size="large"
          auto-complete="off"
          placeholder="密码"
          @keyup.enter="handleLogin"
        >
          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="code" v-if="captchaEnabled">
        <el-input
          v-model="loginForm.code"
          size="large"
          auto-complete="off"
          placeholder="验证码"
          style="width: 63%"
          @keyup.enter="handleLogin"
        >
          <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
        </el-input>
        <div class="login-code">
          <img :src="codeUrl" @click="getCode" class="login-code-img"/>
    <!-- 左侧:仪器控制台面板 -->
    <aside class="login-brand">
      <header class="brand-head">
        <span class="brand-id">
          <span class="brand-mark" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
                 stroke-linecap="round" stroke-linejoin="round">
              <path d="M9 3h6" />
              <path d="M10 3v5.2L5.4 17A2.6 2.6 0 0 0 7.8 21h8.4a2.6 2.6 0 0 0 2.4-4L14 8.2V3" />
              <path d="M7.2 14h9.6" />
            </svg>
          </span>
          <span class="brand-name">{{ title }}</span>
        </span>
        <span class="brand-console">
          <i class="led" aria-hidden="true"></i>
          <span>INSTRUMENT CONSOLE</span>
        </span>
      </header>
      <!-- 量程刻度尺:纯装饰,给面板仪表感 -->
      <div class="gauge" aria-hidden="true">
        <div class="gauge-ticks"></div>
        <div class="gauge-scale">
          <span v-for="n in gaugeScale" :key="n">{{ n }}</span>
        </div>
      </el-form-item>
      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
      <el-form-item style="width:100%;">
      </div>
      <!-- 刻度尺下方的静态读数轨迹:纯装饰 -->
      <div class="trace" aria-hidden="true">
        <svg viewBox="0 0 560 56" preserveAspectRatio="none" fill="none">
          <polyline
            points="0,50 20,50 30,48 40,50 60,50 70,44 80,50 110,50 120,20 130,8 140,20 150,50 170,50 180,30 190,50 210,50 220,46 230,50 260,50 270,38 280,50 300,50 310,50 320,14 330,6 340,16 350,50 380,50 390,34 400,50 430,50 440,49 450,50 480,50 490,26 500,50 520,50 540,50 560,50"
            stroke="rgba(94, 234, 212, 0.5)"
            stroke-width="1.5"
            stroke-linejoin="round"
            stroke-linecap="round"
            vector-effect="non-scaling-stroke"
          />
        </svg>
      </div>
      <div class="brand-copy">
        <h1 class="brand-title">实验室信息管理系统</h1>
        <p class="brand-sub">Laboratory Information Management System</p>
      </div>
      <ul class="brand-points">
        <li v-for="(item, i) in capabilities" :key="item">
          <span class="pt-idx">{{ i < 9 ? '0' + (i + 1) : i + 1 }}</span>
          <span class="pt-txt">{{ item }}</span>
          <span class="pt-line" aria-hidden="true"></span>
        </li>
      </ul>
      <p class="brand-foot">{{ footerContent }}</p>
    </aside>
    <!-- 右侧:身份验证控制卡 -->
    <main class="login-main">
      <el-form
        ref="loginRef"
        :model="loginForm"
        :rules="loginRules"
        label-position="top"
        class="login-form"
      >
        <div class="form-head">
          <h2 class="form-title">
            <i class="led" aria-hidden="true"></i>
            身份验证
          </h2>
          <span class="form-tag">AUTH</span>
        </div>
        <el-form-item label="账号" prop="username">
          <el-input
            v-model="loginForm.username"
            type="text"
            size="large"
            autocomplete="username"
            placeholder="请输入账号"
          >
            <template #prefix><svg-icon icon-class="user" class="input-icon" /></template>
          </el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            size="large"
            autocomplete="current-password"
            placeholder="请输入密码"
            @keyup.enter="handleLogin"
          >
            <template #prefix><svg-icon icon-class="password" class="input-icon" /></template>
          </el-input>
        </el-form-item>
        <div class="form-actions">
          <el-checkbox v-model="loginForm.rememberMe" class="remember">记住密码</el-checkbox>
          <router-link v-if="register" class="link-type" :to="'/register'">立即注册</router-link>
        </div>
        <el-button
          :loading="loading"
          size="large"
          type="primary"
          style="width:100%;"
          class="submit-btn"
          @click.prevent="handleLogin"
        >
          <span v-if="!loading">登 录</span>
          <span v-else>登 录 中...</span>
          {{ loading ? '登 录 中' : '登 录' }}
        </el-button>
        <div style="float: right;" v-if="register">
          <router-link class="link-type" :to="'/register'">立即注册</router-link>
        </div>
      </el-form-item>
    </el-form>
    <!--  底部  -->
    <div class="el-login-footer">
      <span>{{ footerContent }}</span>
    </div>
      </el-form>
    </main>
  </div>
</template>
<script setup>
import { getCodeImg } from "@/api/login"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"
import useUserStore from '@/store/modules/user'
@@ -73,6 +128,14 @@
const title = import.meta.env.VITE_APP_TITLE
const footerContent = defaultSettings.footerContent
// 装饰性刻度值
const gaugeScale = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
const capabilities = [
  '样品全流程追溯',
  '检验数据集中管理',
  '体系文件与记录归档',
  '设备与人员能力管控'
]
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
@@ -81,21 +144,15 @@
const loginForm = ref({
  username: "admin",
  password: "admin123",
  rememberMe: false,
  code: "",
  uuid: ""
  rememberMe: false
})
const loginRules = {
  username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
  password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
  code: [{ required: true, trigger: "change", message: "请输入验证码" }]
  password: [{ required: true, trigger: "blur", message: "请输入您的密码" }]
}
const codeUrl = ref("")
const loading = ref(false)
// 验证码开关
const captchaEnabled = ref(true)
// 注册开关
const register = ref(false)
const redirect = ref(undefined)
@@ -131,21 +188,7 @@
        router.push({ path: redirect.value || "/", query: otherQueryParams })
      }).catch(() => {
        loading.value = false
        // 重新获取验证码
        if (captchaEnabled.value) {
          getCode()
        }
      })
    }
  })
}
function getCode() {
  getCodeImg().then(res => {
    captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
    if (captchaEnabled.value) {
      codeUrl.value = "data:image/gif;base64," + res.img
      loginForm.value.uuid = res.uuid
    }
  })
}
@@ -161,79 +204,448 @@
  }
}
getCode()
getCookie()
</script>
<style lang='scss' scoped>
$console-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
.login {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/login-background.jpg");
  background-size: cover;
  background: var(--lims-bg);
}
.title {
  margin: 0px auto 30px auto;
  text-align: center;
  color: #707070;
/* ============ 左侧:仪器控制台面板 ============ */
.login-brand {
  position: relative;
  overflow: hidden;
  flex: 0 0 46%;
  min-width: 400px;
  padding: 40px 48px 32px;
  display: flex;
  flex-direction: column;
  color: #fff;
  /* 实验室笔记本细网格 + 青绿渐变。
     底色压到 teal-700 一档:白色正文在 #0d9488 上只有 3.3:1,达不到 AA 4.5:1。 */
  background-color: #0f766e;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px),
    linear-gradient(158deg, #0f766e 0%, #0d6a62 50%, #0a5750 100%);
  background-size: 32px 32px, 32px 32px, 100% 100%;
}
/* --- 顶栏:标识 + 控制台铭牌 --- */
.brand-head {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 14px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.22);
  .brand-id {
    display: flex;
    align-items: center;
    gap: 10px;
  }
  .brand-mark {
    width: 30px;
    height: 30px;
    flex-shrink: 0;
    border-radius: 7px;
    background: rgba(255, 255, 255, 0.14);
    border: 1px solid rgba(255, 255, 255, 0.26);
    display: flex;
    align-items: center;
    justify-content: center;
    svg {
      width: 17px;
      height: 17px;
    }
  }
  .brand-name {
    font-family: $console-mono;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: 4px;
    color: rgba(255, 255, 255, 0.97);
  }
  .brand-console {
    display: flex;
    align-items: center;
    gap: 7px;
    font-family: $console-mono;
    font-size: 10px;
    letter-spacing: 1.6px;
    /* 纯装饰铭牌,不承载信息 */
    color: rgba(255, 255, 255, 0.78);
    white-space: nowrap;
    .led {
      color: #5eead4;
    }
  }
}
/* LED 指示灯:颜色跟随上下文,纯装饰 */
.led {
  width: 6px;
  height: 6px;
  flex-shrink: 0;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 22%, transparent);
  animation: led-pulse 2.6s ease-in-out infinite;
}
@keyframes led-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}
/* --- 量程刻度尺(装饰) --- */
.gauge {
  flex-shrink: 0;
  margin-top: 18px;
}
.gauge-ticks {
  height: 14px;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, 0.5) 0 1px, transparent 1px),
    linear-gradient(to right, rgba(255, 255, 255, 0.2) 0 1px, transparent 1px);
  background-size: 28px 14px, 7px 7px;
  background-position: 0 100%, 0 100%;
  background-repeat: repeat-x, repeat-x;
}
.gauge-scale {
  display: flex;
  justify-content: space-between;
  font-family: $console-mono;
  font-size: 10px;
  letter-spacing: 0.5px;
  color: rgba(255, 255, 255, 0.6);
  font-variant-numeric: tabular-nums;
}
/* --- 读数轨迹:刻度尺下方的静态波形(装饰) --- */
.trace {
  flex-shrink: 0;
  height: 56px;
  margin-top: 20px;
  svg {
    display: block;
    width: 100%;
    height: 100%;
  }
}
/* 品牌主体:上下各留一个 auto 外边距,让中段在头/脚之间居中 */
.brand-copy {
  flex: 0 0 auto;
  margin-top: auto;
  .brand-title {
    margin: 0;
    font-size: 30px;
    font-weight: 600;
    line-height: 1.34;
    letter-spacing: 1px;
  }
  .brand-sub {
    margin: 10px 0 0;
    font-size: 11px;
    line-height: 1.6;
    letter-spacing: 0.9px;
    color: rgba(255, 255, 255, 0.88);
  }
}
/* 能力清单:细线分隔 + 序号 + 点状引导线 */
.brand-points {
  flex-shrink: 0;
  margin: 26px 0 0;
  padding: 0;
  list-style: none;
  li {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    font-size: 13.5px;
    line-height: 1.3;
    color: rgba(255, 255, 255, 0.95);
    border-top: 1px solid rgba(255, 255, 255, 0.13);
    &:last-child {
      border-bottom: 1px solid rgba(255, 255, 255, 0.13);
    }
  }
  .pt-idx {
    flex-shrink: 0;
    font-family: $console-mono;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.5px;
    /* 纯装饰序号,不承载信息,允许低于正文对比度 */
    color: rgba(255, 255, 255, 0.62);
    font-variant-numeric: tabular-nums;
  }
  .pt-txt {
    flex-shrink: 0;
  }
  /* 点状引导线,把行右侧填满,像仪表面板上的通道标尺 */
  .pt-line {
    flex: 1 1 auto;
    min-width: 12px;
    height: 1px;
    background-image: repeating-linear-gradient(to right,
        rgba(255, 255, 255, 0.22) 0 2px,
        transparent 2px 7px);
  }
}
.brand-foot {
  flex-shrink: 0;
  margin: auto 0 0;
  font-size: 11px;
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.85);
}
/* ============ 右侧:身份验证控制卡 ============ */
.login-main {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 32px;
  overflow-y: auto;
  background: var(--lims-bg);
}
.login-form {
  border-radius: 6px;
  background: #ffffff;
  width: 400px;
  padding: 25px 25px 5px 25px;
  z-index: 1;
  .el-input {
    height: 40px;
    input {
      height: 40px;
  position: relative;
  width: 392px;
  max-width: 100%;
  padding: 0 32px 28px;
  background: var(--lims-surface);
  border: 1px solid var(--lims-border);
  border-radius: var(--lims-radius);
  box-shadow:
    0 18px 42px -22px rgba(16, 40, 38, 0.22),
    0 2px 8px rgba(16, 40, 38, 0.05);
  /* 卡头做成仪表盘标题条 */
  .form-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin: 0 -32px 24px;
    padding: 13px 20px;
    background: var(--lims-surface-2);
    border-bottom: 1px solid var(--lims-border);
    border-radius: var(--lims-radius) var(--lims-radius) 0 0;
  }
  .form-title {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0;
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 1px;
    color: var(--lims-text);
    .led {
      color: var(--el-color-primary);
    }
  }
  .form-tag {
    font-family: $console-mono;
    font-size: 10px;
    letter-spacing: 2px;
    color: var(--lims-text-3);
  }
  :deep(.el-form-item) {
    margin-bottom: 16px;
  }
  :deep(.el-form-item__label) {
    padding-bottom: 6px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.14em;
    line-height: 1.4;
    color: var(--lims-text-2);
  }
  :deep(.el-input__wrapper) {
    border-radius: var(--lims-radius-sm);
    padding: 1px 12px;
    box-shadow: 0 0 0 1px var(--lims-border) inset;
    transition: box-shadow 0.16s ease;
    &:hover {
      box-shadow: 0 0 0 1px var(--el-color-primary-light-5) inset;
    }
    &.is-focus {
      box-shadow:
        0 0 0 1px var(--el-color-primary) inset,
        0 0 0 3px var(--el-color-primary-light-9);
    }
  }
  /* 等宽输入,贴合仪表读数 */
  :deep(.el-input__inner) {
    font-family: $console-mono;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.05em;
  }
  .input-icon {
    height: 39px;
    width: 14px;
    margin-left: 0px;
    color: var(--lims-text-3);
  }
}
.login-tip {
  font-size: 13px;
  text-align: center;
  color: #bfbfbf;
}
.login-code {
  width: 33%;
  height: 40px;
  float: right;
  img {
    cursor: pointer;
    vertical-align: middle;
  :deep(.el-button--primary) {
    width: 100%;
    height: 46px;
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 4px;
    border: none;
    border-radius: var(--lims-radius-sm);
    background-image: linear-gradient(135deg, #12a594, #0d9488 58%, #0b6b61);
    transition: filter 0.16s ease, box-shadow 0.16s ease;
    &:hover:not(.is-disabled) {
      filter: brightness(1.06);
      box-shadow: 0 8px 18px -10px rgba(13, 148, 136, 0.7);
    }
    &:focus-visible {
      outline: 2px solid var(--el-color-primary-dark-2);
      outline-offset: 2px;
    }
  }
}
.el-login-footer {
  height: 40px;
  line-height: 40px;
  position: fixed;
  bottom: 0;
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial;
  font-size: 12px;
  letter-spacing: 1px;
}
.login-code-img {
  height: 40px;
  padding-left: 12px;
}
html.dark .login {
  background-image: linear-gradient(rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55)), url("../assets/images/login-background.jpg");
  .login-form {
    background: var(--el-bg-color-overlay) !important;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
/* 仪表裁切角标:对角两个 L 形细线,纯装饰 */
.login-form::before,
.login-form::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  border: 0 solid var(--el-color-primary);
  opacity: 0.55;
  pointer-events: none;
}
.login-form::before {
  top: -1px;
  left: -1px;
  border-top-width: 1px;
  border-left-width: 1px;
  border-top-left-radius: var(--lims-radius);
}
.login-form::after {
  right: -1px;
  bottom: -1px;
  border-right-width: 1px;
  border-bottom-width: 1px;
  border-bottom-right-radius: var(--lims-radius);
}
/* 记住密码 + 立即注册 */
.form-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 0 18px;
  .remember {
    color: var(--lims-text-2);
  }
  .link-type {
    font-size: 13px;
  }
  .link-type:focus-visible {
    outline: 2px solid var(--el-color-primary);
    outline-offset: 2px;
    border-radius: 3px;
  }
}
/* 窄屏:收窄品牌面板,铭牌让位给标识 */
@media (max-width: 1024px) {
  .login-brand {
    flex-basis: 40%;
    min-width: 320px;
    padding: 30px 32px 26px;
    .brand-head .brand-console {
      display: none;
    }
    .brand-copy .brand-title {
      font-size: 25px;
    }
  }
}
@media (max-width: 900px) {
  .login-brand {
    display: none;
  }
  .login-main {
    flex: 1 1 100%;
  }
}
html.dark .login-brand {
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
    linear-gradient(158deg, #0f766e 0%, #0d5f57 55%, #10403c 100%);
}
html.dark .login-form {
  background: var(--el-bg-color-overlay);
  border-color: var(--lims-border);
  box-shadow:
    0 18px 42px -22px rgba(0, 0, 0, 0.6),
    0 2px 8px rgba(0, 0, 0, 0.28);
}
</style>