zhangwencui
2 天以前 acd6bbae394c997523b5051d019e584db1845c4c
样式修改
已添加1个文件
已修改17个文件
5025 ■■■■ 文件已修改
script_backup.txt 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 581 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/variables.module.scss 74 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Screenfull/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppMain.vue 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 844 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/NotificationCenter/index.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Settings/index.vue 437 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Logo.vue 89 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/SidebarItem.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 97 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/index.vue 246 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/settings.js 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/theme.js 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/components/viewDia.vue 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/formDia.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 1796 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
script_backup.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,177 @@
<script setup>
import { ElMessage } from "element-plus"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"
import useUserStore from "@/store/modules/user"
import defaultBrandLogo from "@/assets/logo/logo.png"
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const appTitle = String(import.meta.env.VITE_APP_TITLE || "数字工厂 MOM ç³»ç»Ÿ").trim()
const companySubtitle = String(import.meta.env.VITE_LOGIN_SUBTITLE || "Digital Factory Operation Center").trim()
const configuredLogo = String(import.meta.env.VITE_APP_LOGO || "").trim()
const logoModules = import.meta.glob("/src/assets/logo/*.png", { eager: true })
const brandIconUrl = `${import.meta.env.BASE_URL}favicon.ico`
const redirect = ref("")
const loading = ref(false)
const now = ref(new Date())
const brandLogoUrl = ref(defaultBrandLogo)
const loginForm = ref({
  username: "",
  password: "",
  rememberMe: false,
})
const companyName = computed(() => {
  const currentFactoryName = String(userStore.currentFactoryName || "").trim()
  return currentFactoryName || appTitle
})
const todayLabel = computed(() => {
  const date = now.value
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
})
const clockLabel = computed(() => {
  const date = now.value
  return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
})
watch(
  route,
  (newRoute) => {
    redirect.value = String(newRoute.query?.redirect || "")
  },
  { immediate: true }
)
watch(
  () => userStore.currentFactoryName,
  () => updateBrandLogo(),
  { immediate: true }
)
let timer = 0
onMounted(() => {
  timer = window.setInterval(() => {
    now.value = new Date()
  }, 1000)
})
onBeforeUnmount(() => {
  if (timer) {
    window.clearInterval(timer)
    timer = 0
  }
})
function pad(value) {
  return String(value).padStart(2, "0")
}
function resolveConfiguredLogo() {
  if (!configuredLogo) {
    return ""
  }
  if (/^(https?:)?\/\//.test(configuredLogo) || configuredLogo.startsWith("data:")) {
    return configuredLogo
  }
  const cleanPath = configuredLogo.replace(/^\/+/, "")
  const fullPath = cleanPath.startsWith("src/") ? `/${cleanPath}` : `/src/${cleanPath}`
  const localLogo = logoModules[fullPath]
  if (localLogo && localLogo.default) {
    return localLogo.default
  }
  if (configuredLogo.startsWith("/")) {
    return configuredLogo
  }
  return `${import.meta.env.BASE_URL}${cleanPath}`
}
function updateBrandLogo() {
  const logoFromConfig = resolveConfiguredLogo()
  if (logoFromConfig) {
    brandLogoUrl.value = logoFromConfig
    return
  }
  const currentFactoryName = String(userStore.currentFactoryName || "").trim()
  if (!currentFactoryName) {
    brandLogoUrl.value = defaultBrandLogo
    return
  }
  const factoryLogoPath = `/src/assets/logo/${currentFactoryName}.png`
  const matched = logoModules[factoryLogoPath]
  brandLogoUrl.value = matched && matched.default ? matched.default : defaultBrandLogo
}
function handleLogoError() {
  brandLogoUrl.value = defaultBrandLogo
}
function handleRememberCookie() {
  if (!loginForm.value.rememberMe) {
    Cookies.remove("username")
    Cookies.remove("password")
    Cookies.remove("rememberMe")
    return
  }
  Cookies.set("username", loginForm.value.username, { expires: 30 })
  Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })
  Cookies.set("rememberMe", "true", { expires: 30 })
}
function getCookie() {
  const username = Cookies.get("username")
  const password = Cookies.get("password")
  const rememberMe = Cookies.get("rememberMe")
  loginForm.value.username = username || ""
  loginForm.value.password = password ? decrypt(password) : ""
  loginForm.value.rememberMe = rememberMe === "true"
}
function handleLogin() {
  if (!loginForm.value.username) {
    ElMessage.error("请输入账号")
    return
  }
  if (!loginForm.value.password) {
    ElMessage.error("请输入密码")
    return
  }
  loading.value = true
  handleRememberCookie()
    userStore
      .loginCheckFactory(loginForm.value)
      .then(() => {
      const query = route.query
      const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
        if (cur !== "redirect") {
          acc[cur] = query[cur]
        }
        return acc
        }, {})
        router.push({ path: redirect.value || "/", query: otherQueryParams })
      })
      .catch(() => {})
      .finally(() => {
        loading.value = false
      })
}
getCookie()
</script>
src/assets/styles/sidebar.scss
@@ -23,70 +23,8 @@
    padding: 0;
    font-size: 0;
    background: var(--sidebar-bg);
    border-right: 1px solid rgba(255, 255, 255, 0.08);
    box-shadow: 8px 0 24px rgba(15, 23, 42, 0.08);
    isolation: isolate;
    &::before {
      content: "";
      position: absolute;
      inset: -28% -52% -18% -38%;
      z-index: 0;
      pointer-events: none;
      background:
        radial-gradient(circle at 9% 12%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.62), transparent 44%),
        radial-gradient(circle at 87% 18%, rgba(56, 189, 248, 0.4), transparent 48%),
        radial-gradient(circle at 20% 82%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3), transparent 43%),
        radial-gradient(circle at 66% 62%, rgba(125, 211, 252, 0.24), transparent 50%),
        conic-gradient(
          from 210deg at 58% 38%,
          rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14) 0deg,
          rgba(56, 189, 248, 0.05) 76deg,
          rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.16) 180deg,
          rgba(125, 211, 252, 0.04) 290deg,
          rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14) 360deg
        );
      filter: blur(7px) saturate(1.24) contrast(1.05);
      opacity: 0.96;
      transform: translate3d(0, 0, 0);
      transform-origin: 44% 58%;
      animation:
        sidebarAuroraDrift 17.9s cubic-bezier(0.31, 0.03, 0.18, 0.99) infinite,
        sidebarAuroraBreath 9.7s ease-in-out infinite,
        sidebarAuroraSkew 6.9s steps(23, end) infinite;
    }
    &::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: 0;
      pointer-events: none;
      background:
        linear-gradient(
          108deg,
          transparent 10%,
          rgba(255, 255, 255, 0.17) 34%,
          rgba(255, 255, 255, 0.04) 48%,
          transparent 72%
        ),
        linear-gradient(
          202deg,
          rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24) 0%,
          transparent 34%,
          rgba(56, 189, 248, 0.18) 66%,
          transparent 100%
        ),
        radial-gradient(circle at 74% 12%, rgba(125, 211, 252, 0.25), transparent 50%),
        radial-gradient(circle at 22% 84%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14), transparent 56%);
      background-size: 236% 100%, 186% 186%, 164% 164%, 180% 180%;
      background-position: 224% 0, 14% 16%, 78% 10%, 18% 82%;
      opacity: 0.52;
      transform: translate3d(0, 0, 0);
      animation:
        sidebarSheenSweep 13.1s linear infinite,
        sidebarSheenJitter 4.7s steps(31, end) infinite;
    }
    border-right: 1px solid var(--surface-border);
    box-shadow: var(--shadow-md);
    > * {
      position: relative;
@@ -112,7 +50,7 @@
    &.has-logo {
      .el-scrollbar {
        height: calc(100% - 78px);
        height: calc(100% - 64px);
      }
    }
@@ -130,117 +68,111 @@
      border: none !important;
      height: 100%;
      width: 100% !important;
      padding: 10px 0 16px;
      border-radius: 0;
      padding: 12px 0;
      background: transparent !important;
      box-shadow: none;
      backdrop-filter: none;
    }
    .el-menu-item,
    .el-sub-menu__title,
    .menu-title {
      overflow: hidden !important;
      text-overflow: ellipsis !important;
      white-space: nowrap !important;
    }
    .el-menu-item .el-menu-tooltip__trigger {
      display: inline-flex !important;
      width: 100%;
    .submenu-title-noDropdown,
    .el-menu-item {
      display: flex;
      align-items: center;
    }
    .submenu-title-noDropdown,
    .el-sub-menu__title,
    .el-menu-item {
      min-width: 0 !important;
      width: calc(100% - 24px) !important;
      margin: 0 12px 8px !important;
      height: 50px;
      line-height: 50px;
      border-radius: 14px;
      width: calc(100% - 16px) !important;
      margin: 4px 8px !important;
      height: 44px;
      line-height: 44px;
      border-radius: var(--radius-md);
      padding-left: 16px !important;
      padding-right: 36px !important;
      padding-right: 36px !important; // é¢„留箭头位置
      box-sizing: border-box;
      transition: all 0.28s ease;
      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      color: var(--sidebar-text);
      background: linear-gradient(128deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.01));
      border: 1px solid rgba(255, 255, 255, 0.06) !important;
      background: transparent;
      border: none !important;
      position: relative;
      overflow: hidden;
      .svg-icon {
        margin-right: 12px;
        width: 18px;
        height: 18px;
        vertical-align: middle;
        flex-shrink: 0;
        transition: transform 0.3s ease;
      }
    }
    .submenu-title-noDropdown::after,
    .el-sub-menu__title::after,
    .el-menu-item::after {
      content: "";
      position: absolute;
      inset: 0;
      background: linear-gradient(115deg, transparent 12%, rgba(255, 255, 255, 0.16), transparent 78%);
      transform: translateX(-100%);
      opacity: 0;
      transition: transform 0.45s ease, opacity 0.26s ease;
      pointer-events: none;
    .el-sub-menu {
      &.is-opened {
        > .el-sub-menu__title {
          color: var(--menu-active-text) !important;
          .el-sub-menu__icon-arrow {
            transform: rotate(180deg) !important;
            color: var(--menu-active-text) !important;
          }
        }
      }
      .el-sub-menu__icon-arrow {
        position: absolute !important;
        right: 16px !important;
        top: 50% !important;
        margin-top: -6px !important;
        width: 12px !important;
        height: 12px !important;
        font-size: 12px !important;
        display: inline-block !important;
        transition: transform 0.3s ease !important;
        color: var(--sidebar-text) !important;
        z-index: 2;
      }
    }
    .submenu-title-noDropdown:hover,
    .el-sub-menu__title:hover,
    .el-menu-item:hover {
      background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.28), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.08)) !important;
      border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.32) !important;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 8px 18px rgba(8, 36, 76, 0.24);
      transform: translateX(3px);
      background: var(--menu-hover) !important;
      color: var(--menu-active-text) !important;
      .svg-icon {
        transform: scale(1.1);
      }
    }
    .submenu-title-noDropdown:hover::after,
    .el-sub-menu__title:hover::after,
    .el-menu-item:hover::after,
    .el-menu-item.is-active::after,
    .el-sub-menu.is-active > .el-sub-menu__title::after {
      transform: translateX(100%);
      opacity: 1;
    }
    .el-menu-item.is-active,
    .el-sub-menu.is-active > .el-sub-menu__title {
      color: var(--menu-active-text) !important;
      background: var(--menu-active-bg) !important;
      box-shadow: var(--menu-active-glow);
      font-weight: 600;
    & .theme-light .is-active > .el-sub-menu__title,
    & .theme-dark .is-active > .el-sub-menu__title,
    & .el-menu-item.is-active {
      color: #fff !important;
      background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important;
      background-size: 180% 180%;
      box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34));
      border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.5) !important;
      animation: sidebarActiveFlow 4.6s ease infinite;
      .svg-icon {
        color: var(--menu-active-text) !important;
      }
    }
    & .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .el-sub-menu .el-menu-item {
      min-width: 0 !important;
      width: calc(100% - 24px) !important;
      margin: 0 12px 8px !important;
      height: 46px;
      line-height: 46px;
      padding-left: 14px !important;
      padding-right: 14px !important;
      border-radius: 12px;
      transition: all 0.24s ease;
      color: var(--sidebar-text);
      border: 1px solid rgba(255, 255, 255, 0.06) !important;
      background: linear-gradient(128deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01));
      &:hover {
        background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.07)) !important;
        border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3) !important;
        transform: translateX(2px);
      }
      height: 40px;
      line-height: 40px;
      margin: 2px 8px !important;
      padding-left: 44px !important; // å¢žåŠ å­èœå•ç¼©è¿›
      border-radius: var(--radius-sm);
      font-size: 13px;
      &.is-active {
        background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important;
        background-size: 180% 180%;
        color: #fff !important;
        font-weight: 500;
        box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34));
        animation: sidebarActiveFlow 4.6s ease infinite;
        background: var(--menu-active-bg) !important;
      }
    }
  }
@@ -248,99 +180,29 @@
  .hideSidebar {
    .sidebar-container {
      width: var(--sidebar-collapsed-width) !important;
      .el-menu-item,
      .el-sub-menu__title {
        padding: 0 !important;
        display: flex;
        align-items: center;
        justify-content: center;
        margin: 4px 8px !important;
        width: calc(var(--sidebar-collapsed-width) - 16px) !important;
        .svg-icon {
          margin-right: 0;
        }
        .menu-title,
        .el-sub-menu__icon-arrow {
          display: none;
        }
      }
    }
    .main-container {
      margin-left: var(--sidebar-collapsed-width);
    }
    .submenu-title-noDropdown {
      padding: 0 !important;
      position: relative;
      display: flex !important;
      align-items: center;
      justify-content: center;
      .svg-icon {
        margin-right: 0;
      }
      .el-tooltip {
        padding: 0 !important;
        display: inline-flex !important;
        align-items: center;
        justify-content: center;
        width: 100%;
        .svg-icon {
          margin-left: 0;
        }
      }
      .el-menu-tooltip__trigger {
        width: 100%;
        display: inline-flex !important;
        align-items: center;
        justify-content: center;
        .svg-icon {
          width: 22px;
          height: 22px;
          margin-right: 0;
          flex-shrink: 0;
        }
      }
    }
    .el-sub-menu {
      overflow: hidden;
      & > .el-sub-menu__title {
        padding: 0 !important;
        display: flex !important;
        align-items: center;
        justify-content: center;
        .svg-icon {
          margin-left: 0;
          margin-right: 0;
        }
      }
    }
    .el-menu--collapse {
      width: 100% !important;
      padding: 12px 0 16px;
      > .el-menu-item,
      .el-sub-menu {
        & > .el-sub-menu__title,
        &.el-menu-item {
          width: calc(100% - 12px) !important;
          margin: 0 6px 8px !important;
          padding-left: 0 !important;
          padding-right: 0 !important;
          box-sizing: border-box;
          display: flex !important;
          align-items: center;
          justify-content: center;
          .svg-icon {
            width: 22px;
            height: 22px;
            margin-right: 0;
            flex-shrink: 0;
          }
          & > span {
            height: 0;
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
          }
        }
      }
    }
  }
@@ -378,139 +240,61 @@
.el-menu--vertical {
  & > .el-menu {
    .svg-icon {
      margin-right: 10px;
      margin-right: 12px;
    }
  }
  .nest-menu .el-sub-menu > .el-sub-menu__title,
  .el-menu-item {
    min-width: 0 !important;
    margin: 0 10px 8px;
    margin: 4px 10px;
    width: calc(100% - 20px);
    height: 46px;
    line-height: 46px;
    padding-left: 12px !important;
    padding-right: 12px !important;
    height: 44px;
    line-height: 44px;
    padding-left: 16px !important;
    padding-right: 16px !important;
    box-sizing: border-box;
    border-radius: 12px;
    border-radius: var(--radius-md);
    color: var(--sidebar-text);
    border: 1px solid rgba(255, 255, 255, 0.06) !important;
    background: linear-gradient(128deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.01));
    transition: all 0.24s ease;
    background: transparent;
    transition: all 0.2s ease;
    &:hover {
      background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.07)) !important;
      border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3) !important;
      transform: translateX(2px);
      background: var(--menu-hover) !important;
      color: #fff !important;
    }
    &.is-active {
      background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important;
      background-size: 180% 180%;
      color: #fff !important;
      border-radius: 12px;
      box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34));
      border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.5) !important;
      animation: sidebarActiveFlow 4.6s ease infinite;
      background: var(--menu-active-bg) !important;
      color: var(--menu-active-text) !important;
      font-weight: 600;
      box-shadow: var(--menu-active-glow);
    }
  }
  > .el-menu--popup {
    max-height: 100vh;
    overflow: hidden;
    padding: 10px;
    border-radius: 14px;
    position: relative;
    isolation: isolate;
    border: 1px solid rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.26);
    box-shadow:
      0 18px 40px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.16),
      var(--shadow-md);
    padding: 8px;
    border-radius: var(--radius-lg);
    border: 1px solid var(--surface-border);
    box-shadow: var(--shadow-menu);
    background: var(--sidebar-bg);
    backdrop-filter: blur(16px);
    &::before {
      content: "";
      position: absolute;
      inset: -28% -52% -18% -38%;
      z-index: 0;
      pointer-events: none;
      background:
        radial-gradient(circle at 9% 12%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.62), transparent 44%),
        radial-gradient(circle at 87% 18%, rgba(56, 189, 248, 0.4), transparent 48%),
        radial-gradient(circle at 20% 82%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3), transparent 43%),
        radial-gradient(circle at 66% 62%, rgba(125, 211, 252, 0.24), transparent 50%),
        conic-gradient(
          from 210deg at 58% 38%,
          rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14) 0deg,
          rgba(56, 189, 248, 0.05) 76deg,
          rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.16) 180deg,
          rgba(125, 211, 252, 0.04) 290deg,
          rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14) 360deg
        );
      filter: blur(7px) saturate(1.24) contrast(1.05);
      opacity: 0.96;
      transform: translate3d(0, 0, 0);
      transform-origin: 44% 58%;
      animation:
        sidebarAuroraDrift 17.9s cubic-bezier(0.31, 0.03, 0.18, 0.99) infinite,
        sidebarAuroraBreath 9.7s ease-in-out infinite,
        sidebarAuroraSkew 6.9s steps(23, end) infinite;
    }
    &::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: 0;
      pointer-events: none;
      background:
        linear-gradient(
          108deg,
          transparent 10%,
          rgba(255, 255, 255, 0.17) 34%,
          rgba(255, 255, 255, 0.04) 48%,
          transparent 72%
        ),
        linear-gradient(
          202deg,
          rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24) 0%,
          transparent 34%,
          rgba(56, 189, 248, 0.18) 66%,
          transparent 100%
        ),
        radial-gradient(circle at 74% 12%, rgba(125, 211, 252, 0.25), transparent 50%),
        radial-gradient(circle at 22% 84%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14), transparent 56%);
      background-size: 236% 100%, 186% 186%, 164% 164%, 180% 180%;
      background-position: 224% 0, 14% 16%, 78% 10%, 18% 82%;
      opacity: 0.52;
      transform: translate3d(0, 0, 0);
      animation:
        sidebarSheenSweep 13.1s linear infinite,
        sidebarSheenJitter 4.7s steps(31, end) infinite;
    }
    > * {
      position: relative;
      z-index: 1;
    }
    > .el-menu {
      max-height: calc(100vh - 20px);
      max-height: calc(100vh - 16px);
      overflow-y: auto;
      overflow-x: hidden;
      &::-webkit-scrollbar-track-piece {
        background: var(--surface-muted);
      }
      background: transparent !important;
      &::-webkit-scrollbar {
        width: 5px;
        width: 4px;
      }
      &::-webkit-scrollbar-thumb {
        background: var(--accent-light);
        border-radius: 10px;
        background: var(--sidebar-muted);
        border-radius: 4px;
      }
    }
  }
@@ -525,140 +309,5 @@
  }
  100% {
    background-position: 0% 50%;
  }
}
@keyframes sidebarAuroraDrift {
  0% {
    transform: translate3d(-6.3%, -1.8%, 0) scale(1.05) rotate(-1.8deg);
  }
  6% {
    transform: translate3d(2.2%, -4.6%, 0) scale(1.08) rotate(0.7deg);
  }
  17% {
    transform: translate3d(-3.7%, 4.4%, 0) scale(1.11) rotate(2deg);
  }
  27% {
    transform: translate3d(5.6%, 1.2%, 0) scale(1.03) rotate(-1deg);
  }
  39% {
    transform: translate3d(-4.8%, -3.1%, 0) scale(1.09) rotate(1.5deg);
  }
  52% {
    transform: translate3d(2.9%, 4.8%, 0) scale(1.04) rotate(-1.4deg);
  }
  64% {
    transform: translate3d(-6.4%, 0.3%, 0) scale(1.08) rotate(0.5deg);
  }
  73% {
    transform: translate3d(4.8%, -3.9%, 0) scale(1.05) rotate(1.6deg);
  }
  81% {
    transform: translate3d(-2.4%, 2.9%, 0) scale(1.1) rotate(-0.8deg);
  }
  92% {
    transform: translate3d(3.7%, -1.7%, 0) scale(1.06) rotate(-1.6deg);
  }
  100% {
    transform: translate3d(-5.9%, 0.8%, 0) scale(1.08) rotate(1.2deg);
  }
}
@keyframes sidebarAuroraBreath {
  0% {
    opacity: 0.76;
    filter: blur(5px) saturate(1.08);
  }
  15% {
    opacity: 1;
    filter: blur(7px) saturate(1.28);
  }
  37% {
    opacity: 0.84;
    filter: blur(8px) saturate(1.12);
  }
  61% {
    opacity: 0.98;
    filter: blur(6px) saturate(1.24);
  }
  83% {
    opacity: 0.86;
    filter: blur(7px) saturate(1.16);
  }
  100% {
    opacity: 0.94;
    filter: blur(6px) saturate(1.2);
  }
}
@keyframes sidebarAuroraSkew {
  0% {
    transform-origin: 44% 58%;
  }
  21% {
    transform-origin: 62% 42%;
  }
  43% {
    transform-origin: 31% 66%;
  }
  66% {
    transform-origin: 68% 74%;
  }
  100% {
    transform-origin: 39% 45%;
  }
}
@keyframes sidebarSheenSweep {
  0% {
    background-position: 232% 0, 10% 18%, 80% 12%, 20% 82%;
  }
  8% {
    background-position: 186% 0, 16% 30%, 74% 18%, 28% 74%;
  }
  21% {
    background-position: 116% 0, 34% 10%, 62% 26%, 18% 64%;
  }
  37% {
    background-position: 52% 0, 50% 24%, 46% 12%, 32% 58%;
  }
  52% {
    background-position: -4% 0, 34% 54%, 22% 22%, 12% 46%;
  }
  69% {
    background-position: -62% 0, 14% 36%, 32% 34%, 24% 56%;
  }
  84% {
    background-position: -106% 0, 20% 20%, 46% 20%, 34% 70%;
  }
  100% {
    background-position: -136% 0, 10% 18%, 80% 12%, 20% 82%;
  }
}
@keyframes sidebarSheenJitter {
  0% {
    opacity: 0.28;
    transform: translate3d(0, 0, 0);
  }
  17% {
    opacity: 0.56;
    transform: translate3d(1.8%, -0.5%, 0);
  }
  38% {
    opacity: 0.34;
    transform: translate3d(-1.2%, 0.8%, 0);
  }
  63% {
    opacity: 0.6;
    transform: translate3d(2.3%, -0.3%, 0);
  }
  81% {
    opacity: 0.3;
    transform: translate3d(-1.6%, 0.7%, 0);
  }
  100% {
    opacity: 0.52;
    transform: translate3d(2%, -0.1%, 0);
  }
}
src/assets/styles/variables.module.scss
@@ -73,51 +73,51 @@
  --content-radius: 16px;
  --layout-header-z: 20;
  --el-color-primary: #2563eb;
  --el-color-primary-rgb: 37, 99, 235;
  --el-color-primary: #374d77;
  --el-color-primary-rgb: 37, 89, 163;
  --el-color-success: #14b8a6;
  --el-color-warning: #f59e0b;
  --el-color-danger: #ef4444;
  --sidebar-bg: linear-gradient(180deg, #0e2a4f 0%, #123e69 55%, #0e2a4f 100%);
  --sidebar-text: rgba(234, 242, 255, 0.82);
  --sidebar-muted: rgba(234, 242, 255, 0.82);
  --menu-hover: rgba(147, 197, 253, 0.2);
  --menu-active-bg: linear-gradient(135deg, #2f80ff 0%, #38bdf8 100%);
  --menu-active-text: #f8fbff;
  --menu-surface: linear-gradient(180deg, rgba(13, 43, 79, 0.97) 0%, rgba(8, 28, 52, 0.94) 100%);
  --menu-active-glow: 0 8px 18px rgba(56, 139, 255, 0.28);
  --sidebar-bg: #1e293b;
  --sidebar-text: #94a3b8;
  --sidebar-muted: #64748b;
  --menu-hover: rgba(255, 255, 255, 0.05);
  --menu-active-bg: #3b82f6;
  --menu-active-text: #ffffff;
  --menu-surface: #1e293b;
  --menu-active-glow: 0 4px 12px rgba(59, 130, 246, 0.3);
  --app-bg: #f3f7fc;
  --app-bg-accent: #eef5ff;
  --surface-base: rgba(255, 255, 255, 0.92);
  --surface-soft: rgba(255, 255, 255, 0.88);
  --surface-muted: #f5f9ff;
  --surface-border: rgba(148, 163, 184, 0.18);
  --surface-border-strong: rgba(96, 165, 250, 0.34);
  --text-primary: #1e293b;
  --app-bg: #f8fafc;
  --app-bg-accent: #f1f5f9;
  --surface-base: #ffffff;
  --surface-soft: rgba(255, 255, 255, 0.9);
  --surface-muted: #f1f5f9;
  --surface-border: #e2e8f0;
  --surface-border-strong: #cbd5e1;
  --text-primary: #0f172a;
  --text-secondary: #334155;
  --text-tertiary: #64748b;
  --shadow-sm: 0 12px 32px rgba(15, 23, 42, 0.06);
  --shadow-md: 0 20px 42px rgba(15, 23, 42, 0.1);
  --shadow-menu: 0 16px 36px rgba(8, 27, 58, 0.26);
  --radius-lg: 20px;
  --radius-md: 16px;
  --radius-sm: 12px;
  --radius-xs: 10px;
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  --shadow-menu: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
  --radius-lg: 12px;
  --radius-md: 8px;
  --radius-sm: 6px;
  --radius-xs: 4px;
  --navbar-bg: rgba(255, 255, 255, 0.86);
  --navbar-text: #1f3658;
  --navbar-hover: rgba(37, 99, 235, 0.08);
  --navbar-bg: rgba(30, 41, 59, 0.85);
  --navbar-text: #f8fafc;
  --navbar-hover: rgba(255, 255, 255, 0.08);
  --tags-bg: transparent;
  --tags-item-bg: rgba(255, 255, 255, 0.9);
  --tags-item-border: rgba(148, 163, 184, 0.22);
  --tags-item-text: #334155;
  --tags-item-hover: #f4f8ff;
  --tags-close-hover: rgba(37, 99, 235, 0.16);
  --tags-bg: #f8fafc;
  --tags-item-bg: #ffffff;
  --tags-item-border: #e2e8f0;
  --tags-item-text: #475569;
  --tags-item-hover: #f1f5f9;
  --tags-close-hover: rgba(239, 68, 68, 0.1);
  --accent-primary: #2563eb;
  --accent-primary: #374d77;
  --accent-light: #3b82f6;
  --accent-lighter: #60a5fa;
@@ -175,7 +175,7 @@
    .menu-title {
      color: var(--sidebar-text);
    }
    .el-menu-item.is-active,
    .el-menu-item.is-active .menu-title {
      color: var(--menu-active-text) !important;
@@ -185,7 +185,7 @@
    & .theme-dark .el-sub-menu .el-menu-item {
      background-color: var(--el-bg-color) !important;
    }
    & .theme-dark .el-sub-menu .el-menu-item.is-active {
      background-color: var(--menu-active-bg) !important;
    }
src/components/Screenfull/index.vue
@@ -14,7 +14,7 @@
.screenfull-svg {
  display: inline-block;
  cursor: pointer;
  fill: #5a5e66;
  fill: currentColor;
  width: 20px;
  height: 20px;
  vertical-align: 10px;
src/layout/components/AppMain.vue
@@ -37,29 +37,29 @@
</script>
<style lang="scss" scoped>
.app-main {
  min-height: calc(100vh - var(--topbar-height));
  width: 100%;
  position: relative;
  overflow: visible;
  background: transparent;
}
.route-view-wrapper {
  width: 100%;
  height: 100%;
  padding: var(--content-gap);
  padding-top: 0;
}
.fixed-header + .app-main {
  padding-top: 0;
}
.hasTagsView {
  .app-main {
    min-height: calc(100vh - var(--topbar-height) - var(--tagsbar-height));
  }
.app-main {
  min-height: calc(100vh - var(--topbar-height));
  width: 100%;
  position: relative;
  overflow: visible;
  background: transparent;
}
.route-view-wrapper {
  width: 100%;
  height: 100%;
  padding: var(--content-gap);
  padding-top: 0;
}
.fixed-header + .app-main {
  padding-top: 0;
}
.hasTagsView {
  .app-main {
    min-height: calc(100vh - var(--topbar-height) - var(--tagsbar-height));
  }
  .fixed-header + .app-main {
    padding-top: 0;
src/layout/components/Navbar.vue
@@ -1,81 +1,101 @@
<template>
  <div class="navbar">
    <div class="left-zone">
      <hamburger
        id="hamburger-container"
        :is-active="appStore.sidebar.opened"
        class="hamburger-container"
        @toggleClick="toggleSideBar"
      />
      <breadcrumb
        v-if="!settingsStore.topNav"
        id="breadcrumb-container"
        class="breadcrumb-container"
      />
    <div class="left-menu">
      <hamburger id="hamburger-container"
                 :is-active="appStore.sidebar.opened"
                 class="hamburger-container"
                 @toggleClick="toggleSideBar" />
      <breadcrumb v-if="!settingsStore.topNav"
                  id="breadcrumb-container"
                  class="breadcrumb-container" />
    </div>
    <div class="center-zone">
      <el-icon class="search-icon" @click="openHeaderSearch"><Search /></el-icon>
      <el-input
        v-model="topSearchKeyword"
        placeholder="搜索菜单 / åŠŸèƒ½ / æ•°æ®"
        clearable
        @keyup.enter="openHeaderSearch"
      />
      <header-search
        ref="headerSearchRef"
        :keyword="topSearchKeyword"
        class="search-popup-trigger"
      />
    </div>
    <div class="right-menu">
      <el-popover
        v-model:visible="notificationVisible"
        :width="500"
        placement="bottom-end"
        trigger="click"
        :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] }"
        popper-class="notification-popover"
      >
        <template #reference>
          <div class="notification-container right-menu-item hover-effect">
            <el-badge :value="unreadCount" :hidden="unreadCount === 0" class="notification-badge">
              <el-icon :size="18">
                <Bell />
              </el-icon>
            </el-badge>
          </div>
        </template>
        <NotificationCenter @unreadCountChange="handleUnreadCountChange" ref="notificationCenterRef" />
      </el-popover>
      <div class="right-menu-item hover-effect screenfull-container">
        <screenfull />
      <div class="search-wrapper">
        <el-icon class="search-icon"
                 @click="openHeaderSearch">
          <Search />
        </el-icon>
        <el-input v-model="topSearchKeyword"
                  placeholder="快速搜索..."
                  clearable
                  @keyup.enter="openHeaderSearch" />
        <header-search ref="headerSearchRef"
                       :keyword="topSearchKeyword"
                       class="search-popup-trigger" />
      </div>
      <div class="avatar-container">
        <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
          <div class="avatar-wrapper">
            <div class="user-summary">
              <div class="user-name">{{ userStore.nickName || userStore.name || "管理员" }}</div>
              <div class="user-role">{{ userStore.roleName || "系统用户" }}</div>
      <div class="action-icons">
        <el-popover v-model:visible="notificationVisible"
                    :width="500"
                    placement="bottom-end"
                    trigger="click"
                    :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] }"
                    popper-class="notification-popover">
          <template #reference>
            <div class="notification-container right-menu-item hover-effect">
              <el-badge :value="unreadCount"
                        :hidden="unreadCount === 0"
                        class="notification-badge">
                <el-icon :size="18">
                  <Bell />
                </el-icon>
              </el-badge>
            </div>
            <img :src="userStore.avatar" class="user-avatar" />
            <el-icon><caret-bottom /></el-icon>
          </template>
          <NotificationCenter @unreadCountChange="handleUnreadCountChange"
                              ref="notificationCenterRef" />
        </el-popover>
        <div class="right-menu-item hover-effect screenfull-container">
          <screenfull />
        </div>
      </div>
      <div class="user-profile">
        <el-dropdown @command="handleCommand"
                     class="profile-dropdown"
                     trigger="click">
          <div class="profile-trigger">
            <div class="avatar-inner">
              <img :src="userStore.avatar"
                   class="user-avatar" />
              <div class="user-status-dot"></div>
            </div>
            <div class="profile-trigger-text">
              <div class="user-name">{{ userStore.nickName || userStore.name || "管理员" }}</div>
            </div>
            <el-icon class="caret-icon"><caret-bottom /></el-icon>
          </div>
          <template #dropdown>
            <el-dropdown-menu>
              <router-link to="/user/profile">
                <el-dropdown-item>个人中心</el-dropdown-item>
              </router-link>
              <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
                <span>布局设置</span>
              </el-dropdown-item>
              <el-dropdown-item divided command="logout">
                <span>退出登录</span>
              </el-dropdown-item>
            </el-dropdown-menu>
            <div class="user-profile-dropdown-panel">
              <div class="user-profile-dropdown-head">
                <img :src="userStore.avatar"
                     class="user-profile-dropdown-avatar" />
                <div class="user-profile-dropdown-meta">
                  <div class="user-profile-dropdown-name">{{ userStore.nickName || userStore.name || "管理员" }}</div>
                  <div class="user-profile-dropdown-role">{{ userStore.roleName || "系统用户" }}</div>
                </div>
              </div>
              <el-dropdown-menu class="user-profile-dropdown">
                <router-link to="/user/profile">
                  <el-dropdown-item>
                    <el-icon>
                      <User />
                    </el-icon>个人中心
                  </el-dropdown-item>
                </router-link>
                <el-dropdown-item command="setLayout"
                                  v-if="settingsStore.showSettings">
                  <el-icon>
                    <Setting />
                  </el-icon>布局设置
                </el-dropdown-item>
                <el-dropdown-item divided
                                  command="logout"
                                  class="logout-item">
                  <el-icon>
                    <SwitchButton />
                  </el-icon>退出登录
                </el-dropdown-item>
              </el-dropdown-menu>
            </div>
          </template>
        </el-dropdown>
      </div>
@@ -84,299 +104,441 @@
</template>
<script setup>
import { ElMessageBox } from "element-plus";
import { Bell, Search } from "@element-plus/icons-vue";
import Breadcrumb from "@/components/Breadcrumb";
import Hamburger from "@/components/Hamburger";
import Screenfull from "@/components/Screenfull";
import HeaderSearch from "@/components/HeaderSearch";
import NotificationCenter from "./NotificationCenter/index.vue";
import useAppStore from "@/store/modules/app";
import useUserStore from "@/store/modules/user";
import useSettingsStore from "@/store/modules/settings";
  import { ElMessageBox } from "element-plus";
  import { Bell, Search } from "@element-plus/icons-vue";
  import Breadcrumb from "@/components/Breadcrumb";
  import Hamburger from "@/components/Hamburger";
  import Screenfull from "@/components/Screenfull";
  import HeaderSearch from "@/components/HeaderSearch";
  import NotificationCenter from "./NotificationCenter/index.vue";
  import useAppStore from "@/store/modules/app";
  import useUserStore from "@/store/modules/user";
  import useSettingsStore from "@/store/modules/settings";
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
  const appStore = useAppStore();
  const userStore = useUserStore();
  const settingsStore = useSettingsStore();
const topSearchKeyword = ref("");
const headerSearchRef = ref(null);
const notificationVisible = ref(false);
const notificationCenterRef = ref(null);
const unreadCount = ref(0);
  const topSearchKeyword = ref("");
  const headerSearchRef = ref(null);
  const notificationVisible = ref(false);
  const notificationCenterRef = ref(null);
  const unreadCount = ref(0);
function toggleSideBar() {
  appStore.toggleSideBar();
}
function openHeaderSearch() {
  headerSearchRef.value?.open(topSearchKeyword.value);
}
function handleCommand(command) {
  switch (command) {
    case "setLayout":
      setLayout();
      break;
    case "logout":
      logout();
      break;
    default:
      break;
  function toggleSideBar() {
    appStore.toggleSideBar();
  }
}
function logout() {
  ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      userStore.logOut().then(() => {
        location.href = "/index";
      });
  function openHeaderSearch() {
    headerSearchRef.value?.open(topSearchKeyword.value);
  }
  function handleCommand(command) {
    switch (command) {
      case "setLayout":
        setLayout();
        break;
      case "logout":
        logout();
        break;
      default:
        break;
    }
  }
  function logout() {
    ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
    .catch(() => {});
}
      .then(() => {
        userStore.logOut().then(() => {
          location.href = "/index";
        });
      })
      .catch(() => {});
  }
const emits = defineEmits(["setLayout"]);
function setLayout() {
  emits("setLayout");
}
  const emits = defineEmits(["setLayout"]);
  function setLayout() {
    emits("setLayout");
  }
function handleUnreadCountChange(count) {
  unreadCount.value = count;
}
  function handleUnreadCountChange(count) {
    unreadCount.value = count;
  }
let unreadCountTimer = null;
onMounted(() => {
  nextTick(() => {
    if (notificationCenterRef.value) {
      notificationCenterRef.value.loadUnreadCount();
  let unreadCountTimer = null;
  onMounted(() => {
    nextTick(() => {
      if (notificationCenterRef.value) {
        notificationCenterRef.value.loadUnreadCount();
      }
    });
    unreadCountTimer = setInterval(() => {
      if (notificationCenterRef.value) {
        notificationCenterRef.value.loadUnreadCount();
      }
    }, 30000);
  });
  watch(notificationVisible, val => {
    if (val && notificationCenterRef.value) {
      nextTick(() => {
        notificationCenterRef.value.loadMessages();
      });
    }
  });
  unreadCountTimer = setInterval(() => {
    if (notificationCenterRef.value) {
      notificationCenterRef.value.loadUnreadCount();
  onUnmounted(() => {
    if (unreadCountTimer) {
      clearInterval(unreadCountTimer);
    }
  }, 30000);
});
watch(notificationVisible, (val) => {
  if (val && notificationCenterRef.value) {
    nextTick(() => {
      notificationCenterRef.value.loadMessages();
    });
  }
});
onUnmounted(() => {
  if (unreadCountTimer) {
    clearInterval(unreadCountTimer);
  }
});
  });
</script>
<style lang="scss" scoped>
.navbar {
  height: var(--topbar-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  background: rgba(255, 255, 255, 0.86);
  border: 1px solid rgba(148, 163, 184, 0.18);
  border-radius: var(--content-radius);
  backdrop-filter: blur(16px);
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.05);
  padding: 0 18px;
}
.left-zone {
  flex: 0 1 420px;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
}
.hamburger-container {
  line-height: 36px;
  height: 36px;
  width: 36px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--navbar-text);
  &:hover {
    background: var(--navbar-hover);
  }
}
.breadcrumb-container {
  min-width: 0;
}
.center-zone {
  width: clamp(360px, 34vw, 560px);
  min-width: 320px;
  height: 38px;
  border-radius: 999px;
  border: 1px solid rgba(148, 163, 184, 0.24);
  background: rgba(248, 251, 255, 0.92);
  display: flex;
  align-items: center;
  padding: 0 12px;
  gap: 8px;
}
.search-icon {
  color: #5b86c9;
  cursor: pointer;
}
.center-zone :deep(.el-input__wrapper) {
  border: 0;
  box-shadow: none !important;
  background: transparent;
  padding: 0;
}
.center-zone :deep(.el-input__inner) {
  color: #334155;
  font-size: 13px;
}
.search-popup-trigger :deep(.search-icon) {
  color: #5b86c9;
  font-size: 16px;
  cursor: pointer;
}
.right-menu {
  height: 100%;
  align-items: center;
  display: flex;
  gap: 14px;
}
.right-menu-item {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--navbar-text);
  border-radius: 8px;
}
.hover-effect {
  cursor: pointer;
  transition: background 0.2s;
  &:hover {
    background: var(--navbar-hover);
  }
}
.notification-container,
.screenfull-container {
  width: 36px;
  height: 36px;
}
.notification-badge :deep(.el-badge__content) {
  border: none;
}
.screenfull-container :deep(.svg-icon) {
  width: 16px;
  height: 16px;
  color: var(--navbar-text);
}
.avatar-container {
  height: 100%;
  display: flex;
  align-items: center;
}
.avatar-container :deep(.el-dropdown) {
  height: 100%;
  display: flex;
  align-items: center;
}
.avatar-wrapper {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 10px 4px 8px;
  height: 44px;
  border-radius: 22px;
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid rgba(148, 163, 184, 0.22);
}
.user-summary {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}
.user-name {
  color: var(--text-primary);
  font-size: 13px;
  line-height: 1;
}
.user-role {
  color: var(--text-tertiary);
  font-size: 11px;
  line-height: 1;
}
.user-avatar {
  cursor: pointer;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid rgba(148, 163, 184, 0.3);
}
@media (max-width: 1200px) {
  .center-zone {
    display: none;
  .navbar {
    height: var(--topbar-height);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 24px;
    background: var(--navbar-bg);
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    backdrop-filter: blur(12px);
    z-index: var(--layout-header-z);
  }
  .user-summary {
    display: none;
  .left-menu {
    display: flex;
    align-items: center;
    gap: 12px;
  }
}
  .hamburger-container {
    height: 32px;
    width: 32px;
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--navbar-text);
    cursor: pointer;
    transition: all 0.2s ease;
    &:hover {
      background: var(--navbar-hover);
      color: #fff;
    }
  }
  .breadcrumb-container {
    min-width: 0;
    :deep(.el-breadcrumb__inner) {
      color: var(--navbar-text) !important;
      opacity: 0.85;
      &:hover {
        color: #fff !important;
        opacity: 1;
      }
      a {
        color: inherit !important;
        font-weight: 500 !important;
      }
    }
    :deep(.no-redirect) {
      color: #fff !important;
      font-weight: 600 !important;
      opacity: 1;
    }
    :deep(.el-breadcrumb__separator) {
      color: var(--navbar-text);
      opacity: 0.5;
    }
  }
  .right-menu {
    display: flex;
    align-items: center;
    gap: 20px; // å¢žåŠ å¤§ç»„ä¹‹é—´çš„é—´è·
    .search-wrapper {
      display: flex;
      align-items: center;
      gap: 8px;
      height: 34px;
      padding: 0 12px;
      background: var(--navbar-hover);
      border: 1px solid var(--surface-border);
      border-radius: 17px;
      width: 240px; // æœç´¢æ¡†æ›´åŠ ç²¾è‡´å°å·§
      transition: all 0.3s ease;
      &:focus-within {
        width: 300px;
        background: rgba(255, 255, 255, 0.1);
        border-color: var(--accent-primary);
        box-shadow: 0 0 0 2px rgba(var(--el-color-primary-rgb), 0.2);
      }
      .search-icon {
        color: var(--sidebar-text);
        font-size: 16px;
        cursor: pointer;
      }
      :deep(.el-input__wrapper) {
        background: transparent;
        box-shadow: none !important;
        padding: 0;
      }
      :deep(.el-input__inner) {
        color: var(--navbar-text);
        font-size: 13px;
        height: 32px;
        &::placeholder {
          color: var(--sidebar-text);
        }
      }
    }
    .action-icons {
      display: flex;
      align-items: center;
      gap: 4px;
      padding-right: 16px;
      border-right: 1px solid var(--surface-border); // å¢žåŠ åž‚ç›´åˆ†å‰²çº¿
      .right-menu-item {
        padding: 0 8px;
        height: 34px;
        display: flex;
        align-items: center;
        color: var(--navbar-text);
        border-radius: var(--radius-sm);
        transition: all 0.2s ease;
        cursor: pointer;
        &:hover {
          background: var(--navbar-hover);
          color: var(--menu-active-text);
        }
      }
    }
    .user-profile {
      .profile-dropdown {
        cursor: pointer;
      }
      .profile-trigger {
        display: flex;
        align-items: center;
        gap: 10px;
        height: 38px;
        padding: 4px 10px 4px 6px;
        border-radius: 999px;
        border: 1px solid rgba(var(--el-color-primary-rgb), 0.32);
        background: rgba(0, 0, 0, 0.16);
        transition: 0.2s ease;
        &:hover {
          background: rgba(0, 0, 0, 0.24);
          border-color: rgba(var(--el-color-primary-rgb), 0.58);
          box-shadow: 0 0 0 2px rgba(var(--el-color-primary-rgb), 0.18);
        }
      }
      .profile-trigger-text {
        display: flex;
        align-items: center;
        max-width: 120px;
        min-width: 0;
      }
      .user-name {
        font-size: 13px;
        font-weight: 700;
        color: var(--navbar-text);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        line-height: 1;
      }
      .avatar-inner {
        position: relative;
        flex-shrink: 0;
        display: flex;
      }
      .user-avatar {
        width: 30px;
        height: 30px;
        border-radius: 999px;
        border: 2px solid rgba(var(--el-color-primary-rgb), 0.28);
        box-shadow: 0 8px 18px rgba(0, 0, 0, 0.18);
        object-fit: cover;
        transition: 0.2s ease;
      }
      .profile-trigger:hover .user-avatar {
        transform: translateY(-1px);
      }
      .user-status-dot {
        position: absolute;
        bottom: -1px;
        right: -1px;
        width: 9px;
        height: 9px;
        background: #10b981;
        border: 2px solid var(--navbar-bg);
        border-radius: 999px;
      }
      .caret-icon {
        color: var(--navbar-text);
        opacity: 0.76;
        font-size: 12px;
        transition: 0.2s ease;
        margin-left: 2px;
      }
      .profile-trigger:hover .caret-icon {
        transform: translateY(1px);
        opacity: 1;
      }
    }
  }
  :deep(.user-profile-dropdown-panel) {
    min-width: 240px;
    color: var(--sidebar-text);
  }
  :deep(.user-profile-dropdown-head) {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 12px 10px;
    margin: 0 6px 6px;
    border-radius: var(--radius-lg);
    // background: rgba(0, 0, 0, 0.18);
    // border: 1px solid rgba(var(--el-color-primary-rgb), 0.22);
  }
  :deep(.user-profile-dropdown-avatar) {
    width: 38px;
    height: 38px;
    border-radius: 999px;
    border: 2px solid rgba(var(--el-color-primary-rgb), 0.26);
    object-fit: cover;
    flex-shrink: 0;
  }
  :deep(.user-profile-dropdown-meta) {
    min-width: 0;
  }
  :deep(.user-profile-dropdown-name) {
    font-size: 13px;
    font-weight: 800;
    color: #000;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  :deep(.user-profile-dropdown-role) {
    margin-top: 2px;
    font-size: 12px;
    // color: var(--sidebar-muted, rgba(255, 255, 255, 0.62));
    color: #64748b;
    line-height: 1.2;
  }
  :deep(.user-profile-dropdown) {
    background: var(--sidebar-bg) !important;
    border: 1px solid var(--surface-border) !important;
    border-radius: var(--radius-lg) !important;
    box-shadow: var(--shadow-menu) !important;
    padding: 6px !important;
    backdrop-filter: blur(16px);
    .el-dropdown-menu__item {
      color: var(--sidebar-text) !important;
      border-radius: var(--radius-md) !important;
      margin: 2px 0 !important;
      padding: 8px 16px !important;
      font-size: 13px !important;
      display: flex;
      align-items: center;
      gap: 10px;
      .el-icon {
        font-size: 16px;
        margin-right: 0;
      }
      &:hover {
        background: var(--menu-hover) !important;
        color: var(--menu-active-text) !important;
      }
      &.el-dropdown-menu__item--divided {
        border-top-color: var(--surface-border) !important;
        margin-top: 6px !important;
        padding-top: 10px !important;
        &::before {
          display: none;
        }
      }
      &.logout-item {
        color: #f87171 !important;
        &:hover {
          background: rgba(239, 68, 68, 0.1) !important;
          color: #ef4444 !important;
        }
      }
    }
  }
</style>
<style lang="scss">
.notification-popover {
  padding: 0 !important;
  border-radius: 16px !important;
  border: 1px solid rgba(148, 163, 184, 0.22) !important;
  box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12) !important;
  background: rgba(255, 255, 255, 0.94) !important;
  backdrop-filter: blur(16px);
  .el-popover__title {
    display: none;
  }
  .el-popover__body {
  .notification-popover {
    padding: 0 !important;
  }
}
    border-radius: var(--radius-lg) !important;
    border: 1px solid rgba(15, 23, 42, 0.1) !important;
    box-shadow: var(--shadow-menu) !important;
    background: rgba(255, 255, 255, 0.96) !important;
    backdrop-filter: blur(16px);
    color: rgba(15, 23, 42, 0.92);
.el-badge__content.is-fixed {
  top: 8px;
}
    .el-popover__title {
      display: none;
    }
    .el-popover__body {
      padding: 0 !important;
    }
  }
  .el-badge__content.is-fixed {
    top: 8px;
  }
</style>
src/layout/components/NotificationCenter/index.vue
@@ -255,7 +255,8 @@
    flex-direction: column;
    width: 500px;
    padding: 16px;
    background: rgba(255, 255, 255, 0.92);
    background: transparent;
    color: rgba(15, 23, 42, 0.92);
  }
  .popover-header {
@@ -265,12 +266,12 @@
    width: 100%;
    margin-bottom: 16px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--surface-border);
    border-bottom: 1px solid rgba(15, 23, 42, 0.08);
    .popover-title {
      font-size: 18px;
      font-weight: 500;
      color: var(--text-primary);
      color: rgba(15, 23, 42, 0.92);
    }
  }
@@ -285,6 +286,18 @@
      flex-direction: column;
      min-height: 0;
      .el-tabs__nav-wrap::after {
        background-color: rgba(15, 23, 42, 0.08);
      }
      .el-tabs__item {
        color: rgba(15, 23, 42, 0.7);
        &.is-active {
          color: var(--accent-primary);
        }
      }
      .el-tabs__header {
        margin-bottom: 0;
        flex-shrink: 0;
@@ -297,90 +310,96 @@
        min-height: 0;
        padding-top: 16px;
      }
    }
  }
      .el-tab-pane {
        height: 100%;
  .notification-list {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .notification-item {
    display: flex;
    gap: 12px;
    padding: 12px;
    border-radius: var(--radius-md);
    background: rgba(15, 23, 42, 0.03);
    border: 1px solid rgba(15, 23, 42, 0.06);
    transition: all 0.2s ease;
    &:hover {
      background: rgba(15, 23, 42, 0.05);
      border-color: rgba(15, 23, 42, 0.1);
    }
    &.read {
      opacity: 0.6;
    }
    .notification-icon {
      flex-shrink: 0;
    }
    .notification-content-wrapper {
      flex: 1;
      min-width: 0;
      .notification-title {
        font-size: 14px;
        font-weight: 500;
        color: rgba(15, 23, 42, 0.92);
        margin-bottom: 4px;
      }
      .notification-detail {
        font-size: 13px;
        color: rgba(15, 23, 42, 0.7);
        line-height: 1.4;
        margin-bottom: 8px;
      }
      .notification-time {
        font-size: 11px;
        color: rgba(15, 23, 42, 0.55);
      }
    }
  }
  .empty-state {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 300px;
    padding: 40px 0;
  }
  .notification-list {
    .notification-item {
      display: flex;
      padding: 12px 0;
      border-bottom: 1px solid rgba(148, 163, 184, 0.18);
      transition: background-color 0.3s;
      &:hover {
        background-color: #f8fbff;
      }
      &.read {
        opacity: 0.7;
      }
      .notification-icon {
        flex-shrink: 0;
        width: 40px;
        height: 40px;
        display: flex;
        align-items: center;
        justify-content: center;
        background-color: rgba(59, 130, 246, 0.12);
        border-radius: 50%;
        margin-right: 12px;
      }
      .notification-content-wrapper {
        flex: 1;
        min-width: 0;
        .notification-title {
          font-size: 14px;
          font-weight: 500;
          color: var(--text-primary);
          margin-bottom: 8px;
        }
        .notification-detail {
          font-size: 13px;
          color: var(--text-secondary);
          line-height: 1.5;
          margin-bottom: 8px;
          word-break: break-all;
        }
        .notification-time {
          font-size: 12px;
          color: var(--text-tertiary);
        }
      }
      .notification-action {
        flex-shrink: 0;
        margin-left: 12px;
        display: flex;
        align-items: center;
      }
    :deep(.el-empty__description p) {
      color: rgba(15, 23, 42, 0.68);
    }
  }
  .pagination-wrapper {
    margin-top: 16px;
    padding-top: 16px;
    border-top: 1px solid var(--surface-border);
    border-top: 1px solid rgba(15, 23, 42, 0.08);
    display: flex;
    justify-content: center;
    padding-left: 0;
    padding-right: 0;
    justify-content: flex-end;
    :deep(.el-pagination) {
      --el-pagination-bg-color: transparent;
      --el-pagination-button-bg-color: transparent;
      --el-pagination-hover-color: var(--accent-primary);
      button,
      .el-pager li {
        color: rgba(15, 23, 42, 0.7);
        background: transparent;
        &:disabled {
          color: rgba(15, 23, 42, 0.3);
        }
      }
      .el-pager li.is-active {
        color: var(--accent-primary);
        font-weight: bold;
      }
    }
  }
</style>
src/layout/components/Settings/index.vue
@@ -1,287 +1,280 @@
<template>
  <el-drawer v-model="showSettings" direction="rtl" size="300px">
<!--    <div class="setting-drawer-title">-->
<!--      <h3 class="drawer-title">主题风格设置</h3>-->
<!--    </div>-->
<!--    <div class="setting-drawer-block-checbox">-->
<!--      <div-->
<!--        class="setting-drawer-block-checbox-item"-->
<!--        @click="handleTheme('theme-dark')"-->
<!--      >-->
<!--        <img src="@/assets/images/dark.svg" alt="dark" />-->
<!--        <div-->
<!--          v-if="sideTheme === 'theme-dark'"-->
<!--          class="setting-drawer-block-checbox-selectIcon"-->
<!--          style="display: block"-->
<!--        >-->
<!--          <i aria-label="图标: check" class="anticon anticon-check">-->
<!--            <svg-->
<!--              viewBox="64 64 896 896"-->
<!--              data-icon="check"-->
<!--              width="1em"-->
<!--              height="1em"-->
<!--              :fill="theme"-->
<!--              aria-hidden="true"-->
<!--              focusable="false"-->
<!--              class-->
<!--            >-->
<!--              <path-->
<!--                d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"-->
<!--              />-->
<!--            </svg>-->
<!--          </i>-->
<!--        </div>-->
<!--      </div>-->
<!--      <div-->
<!--        class="setting-drawer-block-checbox-item"-->
<!--        @click="handleTheme('theme-light')"-->
<!--      >-->
<!--        <img src="@/assets/images/light.svg" alt="light" />-->
<!--        <div-->
<!--          v-if="sideTheme === 'theme-light'"-->
<!--          class="setting-drawer-block-checbox-selectIcon"-->
<!--          style="display: block"-->
<!--        >-->
<!--          <i aria-label="图标: check" class="anticon anticon-check">-->
<!--            <svg-->
<!--              viewBox="64 64 896 896"-->
<!--              data-icon="check"-->
<!--              width="1em"-->
<!--              height="1em"-->
<!--              :fill="theme"-->
<!--              aria-hidden="true"-->
<!--              focusable="false"-->
<!--              class-->
<!--            >-->
<!--              <path-->
<!--                d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"-->
<!--              />-->
<!--            </svg>-->
<!--          </i>-->
<!--        </div>-->
<!--      </div>-->
<!--    </div>-->
  <el-drawer v-model="showSettings"
             direction="rtl"
             size="300px">
    <!--    <div class="setting-drawer-title">-->
    <!--      <h3 class="drawer-title">主题风格设置</h3>-->
    <!--    </div>-->
    <!--    <div class="setting-drawer-block-checbox">-->
    <!--      <div-->
    <!--        class="setting-drawer-block-checbox-item"-->
    <!--        @click="handleTheme('theme-dark')"-->
    <!--      >-->
    <!--        <img src="@/assets/images/dark.svg" alt="dark" />-->
    <!--        <div-->
    <!--          v-if="sideTheme === 'theme-dark'"-->
    <!--          class="setting-drawer-block-checbox-selectIcon"-->
    <!--          style="display: block"-->
    <!--        >-->
    <!--          <i aria-label="图标: check" class="anticon anticon-check">-->
    <!--            <svg-->
    <!--              viewBox="64 64 896 896"-->
    <!--              data-icon="check"-->
    <!--              width="1em"-->
    <!--              height="1em"-->
    <!--              :fill="theme"-->
    <!--              aria-hidden="true"-->
    <!--              focusable="false"-->
    <!--              class-->
    <!--            >-->
    <!--              <path-->
    <!--                d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"-->
    <!--              />-->
    <!--            </svg>-->
    <!--          </i>-->
    <!--        </div>-->
    <!--      </div>-->
    <!--      <div-->
    <!--        class="setting-drawer-block-checbox-item"-->
    <!--        @click="handleTheme('theme-light')"-->
    <!--      >-->
    <!--        <img src="@/assets/images/light.svg" alt="light" />-->
    <!--        <div-->
    <!--          v-if="sideTheme === 'theme-light'"-->
    <!--          class="setting-drawer-block-checbox-selectIcon"-->
    <!--          style="display: block"-->
    <!--        >-->
    <!--          <i aria-label="图标: check" class="anticon anticon-check">-->
    <!--            <svg-->
    <!--              viewBox="64 64 896 896"-->
    <!--              data-icon="check"-->
    <!--              width="1em"-->
    <!--              height="1em"-->
    <!--              :fill="theme"-->
    <!--              aria-hidden="true"-->
    <!--              focusable="false"-->
    <!--              class-->
    <!--            >-->
    <!--              <path-->
    <!--                d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"-->
    <!--              />-->
    <!--            </svg>-->
    <!--          </i>-->
    <!--        </div>-->
    <!--      </div>-->
    <!--    </div>-->
    <div class="drawer-item">
      <span>主题颜色</span>
      <span class="comp-style">
        <el-color-picker
          v-model="theme"
          :predefine="predefineColors"
          @change="themeChange"
        />
        <el-color-picker v-model="theme"
                         :predefine="predefineColors"
                         @change="themeChange" />
      </span>
    </div>
    <div class="drawer-item">
      <span>显示模式</span>
      <span class="comp-style">
        <el-select
          v-model="settingsStore.darkMode"
          placeholder="请选择"
          style="width: 130px"
          @change="darkModeChange"
        >
          <el-option
            v-for="item in darkModeOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        <el-select v-model="settingsStore.darkMode"
                   placeholder="请选择"
                   style="width: 130px"
                   @change="darkModeChange">
          <el-option v-for="item in darkModeOptions"
                     :key="item.value"
                     :label="item.label"
                     :value="item.value" />
        </el-select>
      </span>
    </div>
    <el-divider />
    <h3 class="drawer-title">系统布局配置</h3>
    <div class="drawer-item">
      <span>开启 TopNav</span>
      <span class="comp-style">
        <el-switch
          v-model="settingsStore.topNav"
          @change="topNavChange"
          class="drawer-switch"
        />
        <el-switch v-model="settingsStore.topNav"
                   @change="topNavChange"
                   class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>开启 Tags-Views</span>
      <span class="comp-style">
        <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
        <el-switch v-model="settingsStore.tagsView"
                   class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>固定 Header</span>
      <span class="comp-style">
        <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
        <el-switch v-model="settingsStore.fixedHeader"
                   class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>显示 Logo</span>
      <span class="comp-style">
        <el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
        <el-switch v-model="settingsStore.sidebarLogo"
                   class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>动态标题</span>
      <span class="comp-style">
        <el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" />
        <el-switch v-model="settingsStore.dynamicTitle"
                   class="drawer-switch" />
      </span>
    </div>
    <el-divider />
    <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting"
      >保存配置</el-button
    >
    <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
    <el-button type="primary"
               plain
               icon="DocumentAdd"
               @click="saveSetting">保存配置</el-button>
    <el-button plain
               icon="Refresh"
               @click="resetSetting">重置配置</el-button>
  </el-drawer>
</template>
<script setup>
import variables from "@/assets/styles/variables.module.scss";
import axios from "axios";
import { ElLoading, ElMessage } from "element-plus";
import { useDynamicTitle } from "@/utils/dynamicTitle";
import useAppStore from "@/store/modules/app";
import useSettingsStore from "@/store/modules/settings";
import usePermissionStore from "@/store/modules/permission";
import { handleThemeStyle } from "@/utils/theme";
  import variables from "@/assets/styles/variables.module.scss";
  import axios from "axios";
  import { ElLoading, ElMessage } from "element-plus";
  import { useDynamicTitle } from "@/utils/dynamicTitle";
  import useAppStore from "@/store/modules/app";
  import useSettingsStore from "@/store/modules/settings";
  import usePermissionStore from "@/store/modules/permission";
  import { handleThemeStyle } from "@/utils/theme";
const { proxy } = getCurrentInstance();
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const showSettings = ref(false);
const theme = ref(settingsStore.theme);
const sideTheme = ref(settingsStore.sideTheme);
const storeSettings = computed(() => settingsStore);
const predefineColors = ref([
  "#002fa7",
  "#81D8D0",
  "#E85827",
  "#008C8C",
  "#F9DC24",
  "#B05923",
  "#003153",
  "#8F4B28",
  "#4C0009",
]);
const darkModeOptions = ref([
  { label: "跟随系统", value: "auto" },
  { label: "浅色", value: "light" },
  { label: "深色", value: "dark" },
]);
  const { proxy } = getCurrentInstance();
  const appStore = useAppStore();
  const settingsStore = useSettingsStore();
  const permissionStore = usePermissionStore();
  const showSettings = ref(false);
  const theme = ref(settingsStore.theme);
  const sideTheme = ref(settingsStore.sideTheme);
  const storeSettings = computed(() => settingsStore);
  const predefineColors = ref([
    "#374D77",
    "#81D8D0",
    "#E85827",
    "#008C8C",
    "#F9DC24",
    "#B05923",
    "#003153",
    "#8F4B28",
    "#4C0009",
  ]);
  const darkModeOptions = ref([
    { label: "跟随系统", value: "auto" },
    { label: "浅色", value: "light" },
    { label: "深色", value: "dark" },
  ]);
/** æ˜¯å¦éœ€è¦topnav */
function topNavChange(val) {
  if (!val) {
    appStore.toggleSideBarHide(false);
    permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
  /** æ˜¯å¦éœ€è¦topnav */
  function topNavChange(val) {
    if (!val) {
      appStore.toggleSideBarHide(false);
      permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
    }
  }
}
function themeChange(val) {
  settingsStore.theme = val;
  handleThemeStyle(val);
}
  function themeChange(val) {
    settingsStore.theme = val;
    handleThemeStyle(val);
  }
function darkModeChange(val) {
  settingsStore.setDarkMode(val);
}
  function darkModeChange(val) {
    settingsStore.setDarkMode(val);
  }
function handleTheme(val) {
  settingsStore.sideTheme = val;
  sideTheme.value = val;
}
  function handleTheme(val) {
    settingsStore.sideTheme = val;
    sideTheme.value = val;
  }
function saveSetting() {
  proxy.$modal.loading("正在保存到本地,请稍候...");
  let layoutSetting = {
    topNav: storeSettings.value.topNav,
    tagsView: storeSettings.value.tagsView,
    fixedHeader: storeSettings.value.fixedHeader,
    sidebarLogo: storeSettings.value.sidebarLogo,
    dynamicTitle: storeSettings.value.dynamicTitle,
    sideTheme: storeSettings.value.sideTheme,
    theme: storeSettings.value.theme,
    darkMode: storeSettings.value.darkMode,
  };
  localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
  setTimeout(proxy.$modal.closeLoading(), 1000);
}
  function saveSetting() {
    proxy.$modal.loading("正在保存到本地,请稍候...");
    let layoutSetting = {
      topNav: storeSettings.value.topNav,
      tagsView: storeSettings.value.tagsView,
      fixedHeader: storeSettings.value.fixedHeader,
      sidebarLogo: storeSettings.value.sidebarLogo,
      dynamicTitle: storeSettings.value.dynamicTitle,
      sideTheme: storeSettings.value.sideTheme,
      theme: storeSettings.value.theme,
      darkMode: storeSettings.value.darkMode,
    };
    localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
    setTimeout(proxy.$modal.closeLoading(), 1000);
  }
function resetSetting() {
  proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
  localStorage.removeItem("layout-setting");
  setTimeout("window.location.reload()", 1000);
}
  function resetSetting() {
    proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
    localStorage.removeItem("layout-setting");
    setTimeout("window.location.reload()", 1000);
  }
function openSetting() {
  showSettings.value = true;
}
  function openSetting() {
    showSettings.value = true;
  }
defineExpose({
  openSetting,
});
  defineExpose({
    openSetting,
  });
</script>
<style lang="scss" scoped>
.setting-drawer-title {
  margin-bottom: 12px;
  color: var(--el-text-color-primary, rgba(0, 0, 0, 0.85));
  line-height: 22px;
  font-weight: bold;
  .setting-drawer-title {
    margin-bottom: 12px;
    color: var(--el-text-color-primary, rgba(0, 0, 0, 0.85));
    line-height: 22px;
    font-weight: bold;
  .drawer-title {
    font-size: 14px;
  }
}
.setting-drawer-block-checbox {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  margin-top: 10px;
  margin-bottom: 20px;
  .setting-drawer-block-checbox-item {
    position: relative;
    margin-right: 16px;
    border-radius: 2px;
    cursor: pointer;
    img {
      width: 48px;
      height: 48px;
    }
    .setting-drawer-block-checbox-selectIcon {
      position: absolute;
      top: 0;
      right: 0;
      width: 100%;
      height: 100%;
      padding-top: 15px;
      padding-left: 24px;
      color: #1890ff;
      font-weight: 700;
    .drawer-title {
      font-size: 14px;
    }
  }
}
.drawer-item {
  color: var(--el-text-color-regular, rgba(0, 0, 0, 0.65));
  padding: 12px 0;
  font-size: 14px;
  .setting-drawer-block-checbox {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    margin-top: 10px;
    margin-bottom: 20px;
  .comp-style {
    float: right;
    margin: -3px 8px 0px 0px;
    .setting-drawer-block-checbox-item {
      position: relative;
      margin-right: 16px;
      border-radius: 2px;
      cursor: pointer;
      img {
        width: 48px;
        height: 48px;
      }
      .setting-drawer-block-checbox-selectIcon {
        position: absolute;
        top: 0;
        right: 0;
        width: 100%;
        height: 100%;
        padding-top: 15px;
        padding-left: 24px;
        color: #1890ff;
        font-weight: 700;
        font-size: 14px;
      }
    }
  }
}
  .drawer-item {
    color: var(--el-text-color-regular, rgba(0, 0, 0, 0.65));
    padding: 12px 0;
    font-size: 14px;
    .comp-style {
      float: right;
      margin: -3px 8px 0px 0px;
    }
  }
</style>
src/layout/components/Sidebar/Logo.vue
@@ -1,7 +1,7 @@
<template>
  <div class="sidebar-logo-container" :class="{ collapse }">
    <transition name="sidebarLogoFade">
      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
      <router-link style="display: flex;" v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
        <img :src="faviconUrl" class="sidebar-logo sidebar-favicon" alt="站点图标" />
      </router-link>
      <router-link v-else key="expand" class="sidebar-logo-link" :style="expandLogoLinkStyle" to="/">
@@ -90,108 +90,55 @@
.sidebar-logo-container {
  position: relative;
  width: 100% !important;
  height: 78px !important;
  line-height: 78px;
  height: 64px !important;
  line-height: 64px;
  background: transparent;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  text-align: left;
  overflow: hidden;
  box-shadow: none;
  backdrop-filter: none;
  transition: all 0.3s ease;
  .sidebar-logo-link {
    position: relative;
    isolation: isolate;
    height: 100%;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    &::before {
      content: "";
      position: absolute;
      inset: 0;
      background-image: var(--logo-bg-image);
      background-size: cover;
      background-position: center;
      opacity: 0.26;
      filter: blur(8px) saturate(0.9);
      transform: scale(1.06);
      pointer-events: none;
      z-index: 0;
    }
    &::after {
      content: "";
      position: absolute;
      inset: 0;
      background: linear-gradient(180deg, rgba(16, 49, 89, 0.18), rgba(16, 49, 89, 0.08));
      pointer-events: none;
      z-index: 0;
    }
    justify-content: flex-start; // é»˜è®¤å±•开时靠左
    padding: 0 16px;
  }
  .sidebar-logo {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    padding: 6px 10px;
   height:100%;
   width:100%;
    width: auto;
    max-width: 100%;
    vertical-align: middle;
    object-fit: contain;
    object-position: center;
    filter: none;
    display: block;
    position: relative;
    z-index: 1;
  }
  .sidebar-title {
    display: inline-block;
    margin: 0;
    color: var(--text-primary);
    color: #fff;
    font-weight: 600;
    line-height: 1.2;
    font-size: 14px;
    font-family: "Segoe UI", "PingFang SC", sans-serif;
    line-height: 64px;
    font-size: 16px;
    vertical-align: middle;
    position: relative;
    z-index: 1;
    margin-left: 12px;
    white-space: nowrap;
  }
  &.collapse {
    .sidebar-logo-link {
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      &::before,
      &::after {
        display: none;
      }
      justify-content: center; // æŠ˜å æ—¶ç»å¯¹å±…中
    }
    .sidebar-logo {
      width: calc(100% - 8px);
      height: calc(100% - 8px);
      max-width: none;
      max-height: none;
      padding: 4px;
      margin: 0 auto;
      filter: none;
      object-fit: contain;
      object-position: center;
    }
    .sidebar-favicon {
      width: calc(100% - 8px);
      height: calc(100% - 8px);
      max-width: none;
      max-height: none;
      width: 32px;
      height: 32px;
      margin: 0;
    }
  }
}
src/layout/components/Sidebar/SidebarItem.vue
@@ -101,58 +101,40 @@
}
</script>
<style lang="scss" scoped>
<style lang="scss" scoped>
.sidebar-item-wrapper {
  :deep(.menu-icon) {
    width: 26px;
    height: 26px;
    width: 18px;
    height: 18px;
    margin-right: 12px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: color 0.2s ease;
    color: var(--sidebar-text);
    opacity: 0.88;
    transition: all 0.3s ease;
    color: inherit;
  }
  
  :deep(.el-menu-item:hover .menu-icon),
  :deep(.el-sub-menu__title:hover .menu-icon) {
    color: #ffffff;
    opacity: 1;
  }
  :deep(.el-menu-item.is-active .menu-icon) {
    color: var(--menu-active-text) !important;
    opacity: 1;
  }
  :deep(.menu-title) {
    font-weight: 500;
    transition: color 0.2s ease;
    color: var(--sidebar-text);
    opacity: 0.82;
  }
  :deep(.el-menu-item:hover .menu-title),
  :deep(.el-sub-menu__title:hover .menu-title) {
    color: #ffffff;
    opacity: 1;
  }
  :deep(.el-menu-item.is-active .menu-title) {
    color: var(--menu-active-text) !important;
    opacity: 1;
    color: inherit;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex: 1;
  }
  
  :deep(.nest-menu) {
    .menu-icon {
      width: 22px;
      height: 22px;
      width: 16px;
      height: 16px;
      margin-right: 10px;
    }
    
    .menu-title { font-size: 13px; }
    .menu-title {
      font-size: 13px;
    }
  }
}
</style>
src/layout/components/Sidebar/index.vue
@@ -67,100 +67,35 @@
    border: none !important;
    height: 100%;
    width: 100% !important;
    border-radius: 0;
    background: transparent !important;
    .el-menu-item,
    .el-sub-menu__title {
      margin-bottom: 8px;
      border-radius: 14px;
      color: v-bind(getMenuTextColor);
    :deep(.el-menu-item),
    :deep(.el-sub-menu__title) {
      margin-bottom: 4px;
      border-radius: var(--radius-md);
      color: var(--sidebar-text);
      font-size: 14px;
      letter-spacing: 0;
      transition:
        transform 0.18s ease,
        background 0.2s ease,
        box-shadow 0.2s ease,
        color 0.2s ease;
      border: none !important;
      transition: all 0.2s ease;
      display: flex;
      align-items: center;
      &:hover {
        background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.28), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.08)) !important;
        transform: translate3d(2px, 0, 0);
        background: var(--menu-hover) !important;
        color: #fff !important;
      }
    }
    .el-menu-item {
      color: var(--sidebar-text);
      &.is-active {
        background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important;
        color: var(--menu-active-text) !important;
        font-weight: 500;
        border-radius: 14px;
        box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34));
        .svg-icon {
          color: var(--menu-active-text) !important;
        }
      }
    }
    .el-sub-menu__title {
      color: v-bind(getMenuTextColor);
    }
    :deep(.el-sub-menu__icon-arrow) {
      display: inline-flex !important;
      align-items: center;
      justify-content: center;
      width: 14px;
      height: 14px;
      margin-top: -7px;
      right: 14px;
      font-size: 14px !important;
      color: currentColor !important;
      opacity: 0.7;
      transition: transform 0.2s ease, opacity 0.2s ease;
    }
    :deep(.el-sub-menu.is-opened .el-sub-menu__icon-arrow) {
      transform: rotate(180deg);
    }
    :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
      color: var(--menu-active-text) !important;
      font-weight: 500;
      border-radius: 12px;
      margin: 0 12px 6px !important;
      padding-left: 14px !important;
      padding-right: 34px !important;
      box-sizing: border-box;
      overflow: hidden;
      background-clip: padding-box;
      background: var(--menu-active-bg) !important;
      box-shadow: var(--menu-active-glow);
      border: none !important;
    }
    :deep(.el-menu-item.is-active) {
      margin: 0 12px 6px !important;
      width: calc(100% - 24px) !important;
      padding-left: 14px !important;
      padding-right: 34px !important;
      box-sizing: border-box;
      overflow: hidden;
      background-clip: padding-box;
      border-radius: 12px;
      background: var(--menu-active-bg) !important;
      color: var(--menu-active-text) !important;
      font-weight: 600;
      box-shadow: var(--menu-active-glow);
      .svg-icon {
        color: var(--menu-active-text) !important;
      }
    }
    :deep(.el-sub-menu.is-active > .el-sub-menu__title .menu-title),
    :deep(.el-sub-menu.is-active > .el-sub-menu__title .svg-icon),
    :deep(.el-menu-item.is-active .menu-title) {
      color: var(--menu-active-text) !important;
    }
  }
}
</style>
src/layout/components/TagsView/index.vue
@@ -13,9 +13,9 @@
        @contextmenu.prevent="openMenu(tag, $event)"
      >
        {{ tag.title }}
        <span v-if="!isAffix(tag)" class="tags-view-close" @click.prevent.stop="closeSelectedTag(tag)">
          <close class="el-icon-close" />
        </span>
        <span v-if="!isAffix(tag)" class="tags-view-close" @click.prevent.stop="closeSelectedTag(tag)">
          <close class="el-icon-close" />
        </span>
      </router-link>
    </scroll-pane>
    <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
@@ -258,76 +258,96 @@
</script>
<style lang="scss" scoped>
.tags-view-container {
  height: var(--tagsbar-height);
  width: 100%;
  margin-top: 0;
  padding: 0 2px;
  background: transparent;
  border: none;
  border-radius: 0;
  backdrop-filter: none;
  box-shadow: none;
  .tags-view-wrapper {
    display: flex;
    align-items: center;
    min-height: var(--tagsbar-height);
    .tags-view-item {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      position: relative;
      cursor: pointer;
      height: 30px;
      line-height: 1;
      color: var(--tags-item-text, #4E5463);
      background: var(--tags-item-bg, #E5E7EA);
      border: 1px solid var(--tags-item-border, #d8dce5);
      border-radius: 999px;
      padding: 0 14px;
      font-size: 12px;
      margin-right: 6px;
      flex-shrink: 0;
      gap: 6px;
      transition: all 0.24s ease;
      &:hover {
        background: var(--tags-item-hover, #eee);
        border-color: rgba(96, 165, 250, 0.36);
      }
      &.active {
        background-image: linear-gradient(132deg, rgba(47, 128, 255, 0.95), rgba(56, 189, 248, 0.9));
        color: #fff;
        box-shadow: 0 10px 20px rgba(37, 99, 235, 0.22);
        border-color: rgba(147, 197, 253, 0.72) !important;
      }
    }
  }
.tags-view-container {
  height: 42px; // å¢žåŠ å®¹å™¨é«˜åº¦ï¼Œæä¾›æ›´å¤šå‘¼å¸æ„Ÿ
  width: 100%;
  background: var(--tags-bg);
  border-bottom: 1px solid var(--surface-border);
  padding: 0 16px;
  display: flex;
  align-items: center;
  .tags-view-wrapper {
    flex: 1;
    overflow: hidden;
    white-space: nowrap;
    .tags-view-item {
      display: inline-flex;
      align-items: center;
      position: relative;
      cursor: pointer;
      height: 30px; // å¢žåŠ é¡µç­¾é«˜åº¦
      line-height: 30px;
      color: var(--tags-item-text);
      background: var(--tags-item-bg);
      border: 1px solid var(--tags-item-border);
      border-radius: 6px; // ç¨å¾®åœ†æ¶¦ä¸€ç‚¹
      padding: 0 12px;
      font-size: 12px;
      margin-right: 6px;
      transition: all 0.2s ease;
      gap: 6px;
      text-decoration: none;
      &:hover {
        background: var(--tags-item-hover);
        color: var(--accent-primary);
        border-color: var(--accent-primary);
      }
      &.active {
        background: var(--accent-primary);
        color: #fff;
        border-color: var(--accent-primary);
        box-shadow: 0 2px 8px rgba(37, 99, 235, 0.15);
        font-weight: 500;
        &::before {
          content: "";
          background: #fff;
          display: inline-block;
          width: 6px;
          height: 6px;
          border-radius: 50%;
          margin-right: 2px;
        }
      }
    }
  }
  .contextmenu {
    margin: 0;
    background: var(--el-bg-color-overlay, #fff);
    background: #fff;
    z-index: 3000;
    position: absolute;
    list-style-type: none;
    padding: 5px 0;
    border-radius: 16px;
    padding: 4px 0;
    border-radius: var(--radius-md);
    font-size: 12px;
    font-weight: 400;
    color: var(--tags-item-text, #333);
    box-shadow: var(--shadow-md);
    border: 1px solid var(--surface-border, #e4e7ed);
    color: var(--text-secondary);
    box-shadow: var(--shadow-md);
    border: 1px solid var(--surface-border);
    li {
      margin: 0;
      padding: 7px 16px;
      padding: 8px 16px;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 8px;
      transition: all 0.2s ease;
      white-space: nowrap;
      svg {
        width: 14px;
        height: 14px;
        color: inherit;
      }
      &:hover {
        background: var(--tags-item-hover, #eee);
        background: #f1f5f9;
        color: var(--accent-primary);
      }
    }
  }
@@ -336,56 +356,56 @@
<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
  .el-scrollbar__view {
    display: flex;
    align-items: center;
  }
  .tags-view-item {
    .tags-view-close {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 12px;
      height: 12px;
      line-height: 1;
      align-self: center;
      transform: translateY(1px);
    }
    .el-icon-close {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 12px;
      height: 12px;
      line-height: 1;
      vertical-align: initial !important;
      border-radius: 50%;
      text-align: center;
      transition: all .3s cubic-bezier(.645, .045, .355, 1);
      transform-origin: 100% 50%;
      align-self: center;
      &:before {
        transform: scale(.6);
        display: inline-flex;
        align-items: center;
        justify-content: center;
      }
      svg {
        display: block;
        width: 10px;
        height: 10px;
      }
      &:hover {
        background-color: var(--tags-close-hover, #b4bccc);
        color: #fff;
      }
    }
  }
}
</style>
.tags-view-wrapper {
  .el-scrollbar__view {
    display: flex;
    align-items: center;
  }
  .tags-view-item {
    .tags-view-close {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 12px;
      height: 12px;
      line-height: 1;
      align-self: center;
      transform: translateY(1px);
    }
    .el-icon-close {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 12px;
      height: 12px;
      line-height: 1;
      vertical-align: initial !important;
      border-radius: 50%;
      text-align: center;
      transition: all .3s cubic-bezier(.645, .045, .355, 1);
      transform-origin: 100% 50%;
      align-self: center;
      &:before {
        transform: scale(.6);
        display: inline-flex;
        align-items: center;
        justify-content: center;
      }
      svg {
        display: block;
        width: 10px;
        height: 10px;
      }
      &:hover {
        background-color: var(--tags-close-hover, #b4bccc);
        color: #fff;
      }
    }
  }
}
</style>
src/layout/index.vue
@@ -100,17 +100,7 @@
    position: relative;
    min-height: 100%;
    width: 100%;
    background: radial-gradient(
        circle at 14% -8%,
        rgba(59, 130, 246, 0.14),
        transparent 36%
      ),
      radial-gradient(
        circle at 88% -12%,
        rgba(56, 189, 248, 0.1),
        transparent 30%
      ),
      linear-gradient(165deg, #f3f7fc 0%, #eef5ff 56%, #f8fbff 100%);
    background: var(--app-bg);
    &.mobile.openSidebar {
      position: fixed;
@@ -119,7 +109,7 @@
  }
  .drawer-bg {
    background: rgba(15, 23, 42, 0.22);
    background: rgba(0, 0, 0, 0.3);
    width: 100%;
    top: 0;
    height: 100%;
@@ -133,6 +123,7 @@
    transition: margin-left 0.25s ease;
    display: flex;
    flex-direction: column;
    padding: 0;
  }
  .fixed-header {
@@ -140,15 +131,15 @@
    top: 0;
    z-index: var(--layout-header-z);
    width: 100%;
    padding: 8px var(--content-gap) 8px;
    padding: 0;
    background: #fff;
    display: flex;
    flex-direction: column;
    gap: 6px;
    background: var(--app-bg, #f3f7fc);
    gap: 2px; // åœ¨ Navbar å’Œ TagsView ä¹‹é—´å¢žåŠ æžå°çš„ç©ºéš™
  }
  .fixed-header.with-tags {
    padding-bottom: 6px;
    padding-bottom: 4px; // åº•部留出一点距离,不要直接贴着主体内容
  }
  .hideSidebar .fixed-header {
src/store/modules/settings.js
@@ -7,43 +7,24 @@
  emitAuto: true,
});
const {
  sideTheme,
  showSettings,
  topNav,
  tagsView,
  fixedHeader,
  sidebarLogo,
  dynamicTitle,
  darkMode,
} = defaultSettings;
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle, darkMode } = defaultSettings;
const storageSetting = JSON.parse(localStorage.getItem("layout-setting") || "{}");
const defaultDarkMode = darkMode || "auto";
const initialDarkMode = storageSetting.darkMode || defaultDarkMode;
colorMode.value = initialDarkMode;
const getIsDark = (mode) => mode === "dark" || (mode === "auto" && preferredDark.value);
const getIsDark = mode => mode === "dark" || (mode === "auto" && preferredDark.value);
const useSettingsStore = defineStore("settings", () => {
  const title = ref("");
  const theme = ref(storageSetting.theme || "#002fa7");
  const theme = ref(storageSetting.theme || "#374D77");
  const sideThemeValue = ref(storageSetting.sideTheme || sideTheme);
  const showSettingsValue = ref(showSettings);
  const topNavValue = ref(
    storageSetting.topNav === undefined ? topNav : storageSetting.topNav
  );
  const tagsViewValue = ref(
    storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView
  );
  const fixedHeaderValue = ref(
    storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader
  );
  const sidebarLogoValue = ref(
    storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo
  );
  const dynamicTitleValue = ref(
    storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle
  );
  const topNavValue = ref(storageSetting.topNav === undefined ? topNav : storageSetting.topNav);
  const tagsViewValue = ref(storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView);
  const fixedHeaderValue = ref(storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader);
  const sidebarLogoValue = ref(storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo);
  const dynamicTitleValue = ref(storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle);
  const darkModeValue = ref(initialDarkMode);
  const isDark = computed(() => getIsDark(darkModeValue.value));
src/utils/theme.js
@@ -1,74 +1,110 @@
// å¤„理主题样式
export function handleThemeStyle(theme) {
    const primary = normalizeHex(theme)
    const [r, g, b] = hexToRgb(primary)
    const light2 = getLightColor(primary, 0.2)
    const light3 = getLightColor(primary, 0.3)
    const light5 = getLightColor(primary, 0.5)
  const primary = normalizeHex(theme);
  const [r, g, b] = hexToRgb(primary);
  const light2 = getLightColor(primary, 0.2);
  const light3 = getLightColor(primary, 0.3);
  const light5 = getLightColor(primary, 0.5);
    document.documentElement.style.setProperty('--el-color-primary', primary)
    document.documentElement.style.setProperty('--el-color-primary-rgb', `${r}, ${g}, ${b}`)
    for (let i = 1; i <= 9; i++) {
        document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(primary, i / 10)}`)
    }
    for (let i = 1; i <= 9; i++) {
        document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(primary, i / 10)}`)
    }
  document.documentElement.style.setProperty("--el-color-primary", primary);
  document.documentElement.style.setProperty("--el-color-primary-rgb", `${r}, ${g}, ${b}`);
  for (let i = 1; i <= 9; i++) {
    document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(primary, i / 10)}`);
  }
  for (let i = 1; i <= 9; i++) {
    document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(primary, i / 10)}`);
  }
    // ç³»ç»Ÿä¸»é¢˜è”动到侧边栏选中态与高亮
    document.documentElement.style.setProperty('--menu-active-bg', `linear-gradient(135deg, ${primary} 0%, ${light3} 100%)`)
    document.documentElement.style.setProperty('--menu-active-glow', `0 10px 24px rgba(${r}, ${g}, ${b}, 0.32)`)
    document.documentElement.style.setProperty('--menu-hover', `rgba(${r}, ${g}, ${b}, 0.2)`)
    document.documentElement.style.setProperty('--accent-primary', primary)
    document.documentElement.style.setProperty('--accent-light', light2)
    document.documentElement.style.setProperty('--accent-lighter', light5)
  // ç³»ç»Ÿä¸»é¢˜è”动到侧边栏选中态与高亮
  document.documentElement.style.setProperty("--menu-active-bg", `linear-gradient(135deg, ${primary} 0%, ${light3} 100%)`);
  document.documentElement.style.setProperty("--menu-active-glow", `0 10px 24px rgba(${r}, ${g}, ${b}, 0.32)`);
  document.documentElement.style.setProperty("--menu-hover", `rgba(${r}, ${g}, ${b}, 0.2)`);
  document.documentElement.style.setProperty("--accent-primary", primary);
  document.documentElement.style.setProperty("--accent-light", light2);
  document.documentElement.style.setProperty("--accent-lighter", light5);
  // æ–°å¢žï¼šä¾§è¾¹æ ä¸Žå¯¼èˆªæ åŠ¨æ€è‰²å½©è”åŠ¨
  // ä¾§è¾¹æ èƒŒæ™¯ï¼šæžæ·±çš„主题色变体,增加一点深度感
  const sidebarBg = getDarkColor(primary, 0.15);
  document.documentElement.style.setProperty("--sidebar-bg", sidebarBg);
  // å¯¼èˆªæ èƒŒæ™¯ï¼šå¸¦é€æ˜Žåº¦çš„主题色变体,实现玻璃拟态
  document.documentElement.style.setProperty("--navbar-bg", `rgba(${r}, ${g}, ${b}, 0.85)`);
  // é¡µç­¾æ èƒŒæ™¯ï¼šæžæµ…的主题色变体,增加呼吸感
  const tagsBg = getLightColor(primary, 0.96);
  document.documentElement.style.setProperty("--tags-bg", tagsBg);
  // æ–‡æœ¬é¢œè‰²é€‚配逻辑
  const brightness = (r * 299 + g * 587 + b * 114) / 1000;
  const isDarkTheme = brightness < 128;
  // å¯¼èˆªæ æ–‡å­—颜色:根据主题色亮度自动切换深浅
  document.documentElement.style.setProperty("--navbar-text", isDarkTheme ? "#f8fafc" : "#1e293b");
  // ä¾§è¾¹æ æ–‡å­—与图标颜色
  document.documentElement.style.setProperty("--sidebar-text", isDarkTheme ? "rgba(255, 255, 255, 0.75)" : "rgba(15, 23, 42, 0.75)");
  document.documentElement.style.setProperty("--sidebar-muted", isDarkTheme ? "rgba(255, 255, 255, 0.45)" : "rgba(15, 23, 42, 0.45)");
  // æ¿€æ´»é¡¹æ–‡å­—颜色:始终保持高对比度
  document.documentElement.style.setProperty("--menu-active-text", isDarkTheme ? "#ffffff" : "#ffffff"); // é€šå¸¸ä¸»è‰²è°ƒæ¿€æ´»æ€ç”¨ç™½è‰²
  // æ‚¬åœä¸Žæ¿€æ´»çŠ¶æ€é¢œè‰²
  document.documentElement.style.setProperty("--menu-hover", `rgba(${r}, ${g}, ${b}, ${isDarkTheme ? 0.12 : 0.08})`);
  document.documentElement.style.setProperty("--navbar-hover", `rgba(${r}, ${g}, ${b}, 0.1)`);
  // è¾¹æ¡†é¢œè‰²è”动
  document.documentElement.style.setProperty("--surface-border", `rgba(${r}, ${g}, ${b}, 0.12)`);
}
// hex颜色转rgb颜色
export function hexToRgb(str) {
    str = normalizeHex(str).replace('#', '')
    const hexs = str.match(/../g) || ['40', '9e', 'ff']
    for (let i = 0; i < 3; i++) {
        hexs[i] = parseInt(hexs[i], 16)
    }
    return hexs
  str = normalizeHex(str).replace("#", "");
  const hexs = str.match(/../g) || ["40", "9e", "ff"];
  for (let i = 0; i < 3; i++) {
    hexs[i] = parseInt(hexs[i], 16);
  }
  return hexs;
}
// rgb颜色转hex颜色
export function rgbToHex(r, g, b) {
    const hexs = [r.toString(16), g.toString(16), b.toString(16)]
    for (let i = 0; i < 3; i++) {
        if (hexs[i].length === 1) {
            hexs[i] = `0${hexs[i]}`
        }
    }
    return `#${hexs.join('')}`
  const hexs = [r.toString(16), g.toString(16), b.toString(16)];
  for (let i = 0; i < 3; i++) {
    if (hexs[i].length === 1) {
      hexs[i] = `0${hexs[i]}`;
    }
  }
  return `#${hexs.join("")}`;
}
// å˜æµ…颜色值
export function getLightColor(color, level) {
    const rgb = hexToRgb(color)
    for (let i = 0; i < 3; i++) {
        rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
    }
    return rgbToHex(rgb[0], rgb[1], rgb[2])
  const rgb = hexToRgb(color);
  for (let i = 0; i < 3; i++) {
    rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);
  }
  return rgbToHex(rgb[0], rgb[1], rgb[2]);
}
// å˜æ·±é¢œè‰²å€¼
export function getDarkColor(color, level) {
    const rgb = hexToRgb(color)
    for (let i = 0; i < 3; i++) {
        rgb[i] = Math.floor(rgb[i] * (1 - level))
    }
    return rgbToHex(rgb[0], rgb[1], rgb[2])
  const rgb = hexToRgb(color);
  for (let i = 0; i < 3; i++) {
    rgb[i] = Math.floor(rgb[i] * (1 - level));
  }
  return rgbToHex(rgb[0], rgb[1], rgb[2]);
}
function normalizeHex(color) {
    if (!color || typeof color !== 'string') return '#409eff'
    let value = color.trim().replace('#', '')
    if (value.length === 3) {
        value = value.split('').map((s) => s + s).join('')
    }
    if (!/^[0-9a-fA-F]{6}$/.test(value)) return '#409eff'
    return `#${value.toLowerCase()}`
  if (!color || typeof color !== "string") return "#409eff";
  let value = color.trim().replace("#", "");
  if (value.length === 3) {
    value = value
      .split("")
      .map(s => s + s)
      .join("");
  }
  if (!/^[0-9a-fA-F]{6}$/.test(value)) return "#409eff";
  return `#${value.toLowerCase()}`;
}
src/views/customerService/components/viewDia.vue
@@ -1,44 +1,73 @@
<template>
  <el-dialog v-model="dialogVisible" title="售后单详情" width="80%" @close="closeDia">
  <el-dialog v-model="dialogVisible"
             title="售后单详情"
             width="80%"
             @close="closeDia">
    <div v-loading="loading">
      <span class="descriptions">基础资料</span>
      <el-descriptions :column="4" border style="margin-top: 10px;">
      <el-descriptions :column="4"
                       border
                       style="margin-top: 10px;">
        <el-descriptions-item label="客户名称">{{ detail.customerName || '-' }}</el-descriptions-item>
        <el-descriptions-item label="售后类型">{{ getDictLabel(classificationOptions, detail.serviceType) }}</el-descriptions-item>
        <el-descriptions-item label="关联销售单号">{{ detail.salesContractNo || '-' }}</el-descriptions-item>
        <el-descriptions-item label="紧急程度">{{ getDictLabel(degreeOfUrgencyOptions, detail.urgency) }}</el-descriptions-item>
        <el-descriptions-item label="工单编号">{{ detail.afterSalesServiceNo || '-' }}</el-descriptions-item>
        <el-descriptions-item label="处理状态">
          <el-tag :type="detail.status === 1 ? 'danger' : 'success'" size="small">
          <el-tag :type="detail.status === 1 ? 'danger' : 'success'"
                  size="small">
            {{ detail.status === 1 ? '待处理' : '已处理' }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="登记人">{{ detail.checkNickName || '-' }}</el-descriptions-item>
        <el-descriptions-item label="反馈日期">{{ detail.feedbackDate || '-' }}</el-descriptions-item>
        <el-descriptions-item label="客户诉求" :span="4">{{ detail.proDesc || '-' }}</el-descriptions-item>
        <el-descriptions-item label="客户诉求"
                              :span="4">{{ detail.proDesc || '-' }}</el-descriptions-item>
      </el-descriptions>
      <div style="margin-top: 20px;">
        <span class="descriptions">处理信息</span>
        <el-descriptions :column="3" border style="margin-top: 10px;">
        <el-descriptions :column="3"
                         border
                         style="margin-top: 10px;">
          <el-descriptions-item label="处理人">{{ detail.disposeNickName || '-' }}</el-descriptions-item>
          <el-descriptions-item label="处理日期">{{ detail.disDate || '-' }}</el-descriptions-item>
          <el-descriptions-item label="处理结果" :span="3">{{ detail.disRes || '-' }}</el-descriptions-item>
          <el-descriptions-item label="处理结果"
                                :span="3">{{ detail.disRes || '-' }}</el-descriptions-item>
        </el-descriptions>
      </div>
      <div style="margin-top: 20px;">
        <span class="descriptions">关联产品</span>
        <el-table :data="tableData" border style="width: 100%; margin-top: 10px;">
          <el-table-column type="index" label="序号" width="60" align="center" />
          <el-table-column prop="productCategory" label="产品大类" align="center" />
          <el-table-column prop="specificationModel" label="规格型号" align="center" />
          <el-table-column prop="unit" label="单位" align="center" />
          <el-table-column prop="expressCompany" label="快递公司" align="center" />
          <el-table-column prop="expressNumber" label="快递单号" align="center" />
          <el-table-column prop="shippingCarNumber" label="发货车牌" align="center" />
          <el-table-column prop="shippingDate" label="发货日期" align="center" />
          <el-table-column prop="quantity" label="售后数量" align="center" />
        <el-table :data="tableData"
                  border
                  style="width: 100%; margin-top: 10px;">
          <el-table-column type="index"
                           label="序号"
                           width="60"
                           align="center" />
          <el-table-column prop="productCategory"
                           label="产品大类"
                           align="center" />
          <el-table-column prop="specificationModel"
                           label="规格型号"
                           align="center" />
          <el-table-column prop="unit"
                           label="单位"
                           align="center" />
          <el-table-column prop="expressCompany"
                           label="快递公司"
                           align="center" />
          <el-table-column prop="expressNumber"
                           label="快递单号"
                           align="center" />
          <el-table-column prop="shippingCarNumber"
                           label="发货车牌"
                           align="center" />
          <el-table-column prop="shippingDate"
                           label="发货日期"
                           align="center" />
          <el-table-column prop="quantity"
                           label="售后数量"
                           align="center" />
        </el-table>
      </div>
    </div>
@@ -51,73 +80,81 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue';
import { getAfterSalesServiceById } from '@/api/customerService/index.js';
  import { ref, computed, getCurrentInstance } from "vue";
  import { getAfterSalesServiceById } from "@/api/customerService/index.js";
const { proxy } = getCurrentInstance();
const dialogVisible = ref(false);
const loading = ref(false);
const detail = ref({});
const tableData = ref([]);
  const { proxy } = getCurrentInstance();
  const dialogVisible = ref(false);
  const loading = ref(false);
  const detail = ref({});
  const tableData = ref([]);
const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict(
  "post_sale_waiting_list",
  "degree_of_urgency"
);
const classificationOptions = computed(() => post_sale_waiting_list?.value || []);
const degreeOfUrgencyOptions = computed(() => degree_of_urgency?.value || []);
  const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict(
    "post_sale_waiting_list",
    "degree_of_urgency"
  );
  const classificationOptions = computed(
    () => post_sale_waiting_list?.value || []
  );
  const degreeOfUrgencyOptions = computed(() => degree_of_urgency?.value || []);
const getDictLabel = (options, value) => {
  if (!value) return '-';
  const item = options.find(i => String(i.value) === String(value));
  return item ? item.label : value;
};
  const getDictLabel = (options, value) => {
    if (!value) return "-";
    const item = options.find(i => String(i.value) === String(value));
    return item ? item.label : value;
  };
const openDialog = (row) => {
  dialogVisible.value = true;
  loading.value = true;
  detail.value = {};
  tableData.value = [];
  getAfterSalesServiceById(row.id).then(res => {
    loading.value = false;
    if (res.code === 200) {
      detail.value = res.data || {};
      let productData = res.data?.salesLedgerDto?.productData || [];
      const selectedIds = res.data.productModelIds ? String(res.data.productModelIds).split(",") : [];
      tableData.value = productData.filter(item => selectedIds.includes(String(item.id)));
    }
  }).catch(() => {
    loading.value = false;
  const openDialog = row => {
    dialogVisible.value = true;
    loading.value = true;
    detail.value = {};
    tableData.value = [];
    getAfterSalesServiceById(row.id)
      .then(res => {
        loading.value = false;
        if (res.code === 200) {
          detail.value = res.data || {};
          let productData = res.data?.salesLedgerDto?.productData || [];
          const selectedIds = res.data.productModelIds
            ? String(res.data.productModelIds).split(",")
            : [];
          tableData.value = productData.filter(item =>
            selectedIds.includes(String(item.id))
          );
        }
      })
      .catch(() => {
        loading.value = false;
      });
  };
  const closeDia = () => {
    dialogVisible.value = false;
  };
  defineExpose({
    openDialog,
  });
};
const closeDia = () => {
  dialogVisible.value = false;
};
defineExpose({
  openDialog
});
</script>
<style scoped>
.descriptions {
  display: inline-block;
  font-size: 1rem;
  font-weight: 600;
  padding-left: 12px;
  position: relative;
}
.descriptions::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 4px;
  height: 1rem;
  background-color: #002fa7;
  border-radius: 2px;
}
  .descriptions {
    display: inline-block;
    font-size: 1rem;
    font-weight: 600;
    padding-left: 12px;
    position: relative;
  }
  .descriptions::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 4px;
    height: 1rem;
    background-color: #374d77;
    border-radius: 2px;
  }
</style>
src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -98,7 +98,9 @@
              </el-tag>
            </template>
            <template #quantity="{ row }">
              <el-input-number v-model="row.quantity" :min="0" size="small" />
              <el-input-number v-model="row.quantity"
                               :min="0"
                               size="small" />
            </template>
          </PIMTable>
        </div>
@@ -442,15 +444,25 @@
          );
          if (opt) {
            let restoredData = (opt.productData || []).map(normalizeProductRow);
            const selectedIds = form.value.productModelIds ? String(form.value.productModelIds).split(",") : [];
            const quantities = form.value.productModelQuantities ? String(form.value.productModelQuantities).split(",") : [];
            tableData.value = restoredData.filter(item => selectedIds.includes(String(item.id))).map(item => {
              let qIndex = selectedIds.indexOf(String(item.id));
              if (qIndex !== -1 && qIndex < quantities.length && quantities[qIndex] !== "") {
                 item.quantity = Number(quantities[qIndex]);
              }
              return item;
            });
            const selectedIds = form.value.productModelIds
              ? String(form.value.productModelIds).split(",")
              : [];
            const quantities = form.value.productModelQuantities
              ? String(form.value.productModelQuantities).split(",")
              : [];
            tableData.value = restoredData
              .filter(item => selectedIds.includes(String(item.id)))
              .map(item => {
                let qIndex = selectedIds.indexOf(String(item.id));
                if (
                  qIndex !== -1 &&
                  qIndex < quantities.length &&
                  quantities[qIndex] !== ""
                ) {
                  item.quantity = Number(quantities[qIndex]);
                }
                return item;
              });
          }
        }
      }
@@ -510,7 +522,7 @@
    transform: translateY(-50%);
    width: 4px;
    height: 1rem;
    background-color: #002fa7; /* Element é»˜è®¤çº¢è‰² */
    background-color: #374d77; /* Element é»˜è®¤çº¢è‰² */
    border-radius: 2px;
  }
</style>
src/views/login.vue
@@ -4,15 +4,12 @@
      <section class="factory">
        <div class="brand hero-brand">
          <div class="logo hero-logo">
            <img
              :src="brandLogoUrl"
              :alt="`${companyName} logo`"
              class="logo-image hero-logo-image"
              @error="handleLogoError"
            />
            <img :src="brandLogoUrl"
                 :alt="`${companyName} logo`"
                 class="logo-image hero-logo-image"
                 @error="handleLogoError" />
          </div>
        </div>
        <div class="hero">
          <div class="chip">数字工厂 Â· æ™ºèƒ½æŽ’产 Â· è®¾å¤‡äº’联 Â· è´¨é‡è¿½æº¯</div>
          <h1>数字工厂<br />MOM æ™ºé€ å¹³å°</h1>
@@ -20,132 +17,251 @@
            ä»¥å®žæ—¶æ•°æ®é©±åŠ¨ç”Ÿäº§çŽ°åœºï¼ŒæŠŠå·¥å•ã€è®¾å¤‡ã€ç‰©æ–™ã€è´¨é‡ã€èƒ½è€—ä¸Žä»“å‚¨è¿žæŽ¥æˆä¸€å¼ é€æ˜Žçš„åˆ¶é€ è¿è¥ç½‘ç»œã€‚
          </p>
        </div>
        <div class="scene" aria-hidden="true">
        <div class="scene"
             aria-hidden="true">
          <div class="floor"></div>
          <svg class="factory-svg" viewBox="0 0 920 360" preserveAspectRatio="xMidYMid meet">
          <svg class="factory-svg"
               viewBox="0 0 920 360"
               preserveAspectRatio="xMidYMid meet">
            <defs>
              <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1">
                <stop offset="0" stop-color="#40e4ff" stop-opacity=".9" />
                <stop offset="1" stop-color="#1f78ff" stop-opacity=".68" />
              <linearGradient id="g1"
                              x1="0"
                              y1="0"
                              x2="1"
                              y2="1">
                <stop offset="0"
                      stop-color="#40e4ff"
                      stop-opacity=".9" />
                <stop offset="1"
                      stop-color="#1f78ff"
                      stop-opacity=".68" />
              </linearGradient>
              <linearGradient id="g2" x1="0" y1="0" x2="1" y2="1">
                <stop offset="0" stop-color="#ffffff" stop-opacity=".28" />
                <stop offset="1" stop-color="#ffffff" stop-opacity=".06" />
              <linearGradient id="g2"
                              x1="0"
                              y1="0"
                              x2="1"
                              y2="1">
                <stop offset="0"
                      stop-color="#ffffff"
                      stop-opacity=".28" />
                <stop offset="1"
                      stop-color="#ffffff"
                      stop-opacity=".06" />
              </linearGradient>
            </defs>
            <path d="M45 255H830" stroke="url(#g1)" stroke-width="16" stroke-linecap="round" opacity=".55" />
            <path class="belt" d="M45 255H830" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity=".75" />
            <path d="M45 255H830"
                  stroke="url(#g1)"
                  stroke-width="16"
                  stroke-linecap="round"
                  opacity=".55" />
            <path class="belt"
                  d="M45 255H830"
                  stroke="#fff"
                  stroke-width="3"
                  stroke-linecap="round"
                  opacity=".75" />
            <g class="box">
              <rect x="60" y="212" width="54" height="42" rx="8" fill="#ffb15f" />
              <path d="M60 225h54" stroke="#fff" opacity=".45" />
              <rect x="60"
                    y="212"
                    width="54"
                    height="42"
                    rx="8"
                    fill="#ffb15f" />
              <path d="M60 225h54"
                    stroke="#fff"
                    opacity=".45" />
            </g>
            <g class="box two">
              <rect x="60" y="212" width="54" height="42" rx="8" fill="#28d9cd" />
              <path d="M60 225h54" stroke="#fff" opacity=".45" />
              <rect x="60"
                    y="212"
                    width="54"
                    height="42"
                    rx="8"
                    fill="#28d9cd" />
              <path d="M60 225h54"
                    stroke="#fff"
                    opacity=".45" />
            </g>
            <g class="box three">
              <rect x="60" y="212" width="54" height="42" rx="8" fill="#8b5cf6" />
              <path d="M60 225h54" stroke="#fff" opacity=".45" />
              <rect x="60"
                    y="212"
                    width="54"
                    height="42"
                    rx="8"
                    fill="#8b5cf6" />
              <path d="M60 225h54"
                    stroke="#fff"
                    opacity=".45" />
            </g>
            <g>
              <rect x="120" y="112" width="138" height="128" rx="18" fill="url(#g2)" stroke="rgba(255,255,255,.42)" />
              <path d="M145 185h88M145 210h58" stroke="#40e4ff" stroke-width="6" stroke-linecap="round" />
              <path d="M145 140h88" stroke="#fff" stroke-opacity=".5" stroke-width="4" stroke-linecap="round" />
              <rect x="120"
                    y="112"
                    width="138"
                    height="128"
                    rx="18"
                    fill="url(#g2)"
                    stroke="rgba(255,255,255,.42)" />
              <path d="M145 185h88M145 210h58"
                    stroke="#40e4ff"
                    stroke-width="6"
                    stroke-linecap="round" />
              <path d="M145 140h88"
                    stroke="#fff"
                    stroke-opacity=".5"
                    stroke-width="4"
                    stroke-linecap="round" />
            </g>
            <g>
              <rect x="315" y="76" width="190" height="164" rx="22" fill="url(#g2)" stroke="rgba(255,255,255,.42)" />
              <path d="M350 126h120M350 158h90M350 190h112" stroke="#fff" stroke-opacity=".5" stroke-width="6" stroke-linecap="round" />
              <circle class="signal" cx="472" cy="104" r="10" fill="#20e0d2" />
              <circle class="signal two" cx="448" cy="104" r="10" fill="#1f78ff" />
              <circle class="signal three" cx="424" cy="104" r="10" fill="#ff8a3d" />
              <rect x="315"
                    y="76"
                    width="190"
                    height="164"
                    rx="22"
                    fill="url(#g2)"
                    stroke="rgba(255,255,255,.42)" />
              <path d="M350 126h120M350 158h90M350 190h112"
                    stroke="#fff"
                    stroke-opacity=".5"
                    stroke-width="6"
                    stroke-linecap="round" />
              <circle class="signal"
                      cx="472"
                      cy="104"
                      r="10"
                      fill="#20e0d2" />
              <circle class="signal two"
                      cx="448"
                      cy="104"
                      r="10"
                      fill="#1f78ff" />
              <circle class="signal three"
                      cx="424"
                      cy="104"
                      r="10"
                      fill="#ff8a3d" />
            </g>
            <g class="arm">
              <path d="M612 124h92" stroke="#40e4ff" stroke-width="14" stroke-linecap="round" />
              <path d="M704 124l42 56" stroke="#40e4ff" stroke-width="14" stroke-linecap="round" />
              <circle cx="612" cy="124" r="25" fill="#1f78ff" stroke="#fff" stroke-opacity=".45" />
              <circle cx="704" cy="124" r="18" fill="#20e0d2" />
              <path d="M744 180v34M727 214h34" stroke="#fff" stroke-width="7" stroke-linecap="round" />
              <path d="M612 124h92"
                    stroke="#40e4ff"
                    stroke-width="14"
                    stroke-linecap="round" />
              <path d="M704 124l42 56"
                    stroke="#40e4ff"
                    stroke-width="14"
                    stroke-linecap="round" />
              <circle cx="612"
                      cy="124"
                      r="25"
                      fill="#1f78ff"
                      stroke="#fff"
                      stroke-opacity=".45" />
              <circle cx="704"
                      cy="124"
                      r="18"
                      fill="#20e0d2" />
              <path d="M744 180v34M727 214h34"
                    stroke="#fff"
                    stroke-width="7"
                    stroke-linecap="round" />
            </g>
            <g>
              <rect x="690" y="82" width="148" height="158" rx="20" fill="url(#g2)" stroke="rgba(255,255,255,.42)" />
              <path d="M724 206V134M764 206V112M804 206V154" stroke="#20e0d2" stroke-width="12" stroke-linecap="round" />
              <path d="M720 206h92" stroke="#fff" stroke-opacity=".44" stroke-width="5" stroke-linecap="round" />
              <rect x="690"
                    y="82"
                    width="148"
                    height="158"
                    rx="20"
                    fill="url(#g2)"
                    stroke="rgba(255,255,255,.42)" />
              <path d="M724 206V134M764 206V112M804 206V154"
                    stroke="#20e0d2"
                    stroke-width="12"
                    stroke-linecap="round" />
              <path d="M720 206h92"
                    stroke="#fff"
                    stroke-opacity=".44"
                    stroke-width="5"
                    stroke-linecap="round" />
            </g>
            <path
              d="M190 112C265 42 348 48 410 76C502 118 568 76 612 124C654 170 700 74 764 82"
              fill="none"
              stroke="#20e0d2"
              stroke-width="2"
              stroke-dasharray="8 10"
              opacity=".58"
            >
              <animate attributeName="stroke-dashoffset" from="80" to="0" dur="2s" repeatCount="indefinite" />
            <path d="M190 112C265 42 348 48 410 76C502 118 568 76 612 124C654 170 700 74 764 82"
                  fill="none"
                  stroke="#20e0d2"
                  stroke-width="2"
                  stroke-dasharray="8 10"
                  opacity=".58">
              <animate attributeName="stroke-dashoffset"
                       from="80"
                       to="0"
                       dur="2s"
                       repeatCount="indefinite" />
            </path>
          </svg>
        </div>
      </section>
      <section class="login-wrap">
        <div class="time">
          <span>{{ todayLabel }}</span>
          {{ clockLabel }}
        </div>
        <form class="login-card" @submit.prevent="handleLogin">
        <form class="login-card"
              @submit.prevent="handleLogin">
          <div class="brand card-brand">
            <div class="logo">
              <img :src="brandIconUrl" :alt="`${companyName} icon`" class="logo-image card-logo-image" />
              <img :src="brandIconUrl"
                   :alt="`${companyName} icon`"
                   class="logo-image card-logo-image" />
            </div>
                        <div class="brand-copy card-brand-copy">
                            <div class="brand-title">{{ companyName }}</div>
                            <small>数字工厂统一入口</small>
                        </div>
            <div class="brand-copy card-brand-copy">
              <div class="brand-title">{{ companyName }}</div>
              <small>数字工厂统一入口</small>
            </div>
          </div>
          <h2>欢迎登录</h2>
          <p class="sub">进入 MOM æ•°å­—工厂运营驾驶舱</p>
          <div class="form-row">
            <label>账号</label>
            <div class="input">
              <svg viewBox="0 0 24 24" fill="none">
                <path d="M20 21a8 8 0 0 0-16 0" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
                <path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="currentColor" stroke-width="1.8" />
              <svg viewBox="0 0 24 24"
                   fill="none">
                <path d="M20 21a8 8 0 0 0-16 0"
                      stroke="currentColor"
                      stroke-width="1.8"
                      stroke-linecap="round" />
                <path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"
                      stroke="currentColor"
                      stroke-width="1.8" />
              </svg>
              <input v-model.trim="loginForm.username" type="text" placeholder="请输入管理员账号" />
              <input v-model.trim="loginForm.username"
                     type="text"
                     placeholder="请输入管理员账号" />
            </div>
          </div>
          <div class="form-row">
            <label>密码</label>
            <div class="input">
              <svg viewBox="0 0 24 24" fill="none">
                <path d="M7 10V8a5 5 0 0 1 10 0v2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
                <path d="M6.8 10h10.4A1.8 1.8 0 0 1 19 11.8v6.4A1.8 1.8 0 0 1 17.2 20H6.8A1.8 1.8 0 0 1 5 18.2v-6.4A1.8 1.8 0 0 1 6.8 10Z" stroke="currentColor" stroke-width="1.8" />
              <svg viewBox="0 0 24 24"
                   fill="none">
                <path d="M7 10V8a5 5 0 0 1 10 0v2"
                      stroke="currentColor"
                      stroke-width="1.8"
                      stroke-linecap="round" />
                <path d="M6.8 10h10.4A1.8 1.8 0 0 1 19 11.8v6.4A1.8 1.8 0 0 1 17.2 20H6.8A1.8 1.8 0 0 1 5 18.2v-6.4A1.8 1.8 0 0 1 6.8 10Z"
                      stroke="currentColor"
                      stroke-width="1.8" />
              </svg>
              <input
                v-model="loginForm.password"
                type="password"
                placeholder="请输入登录密码"
                autocomplete="current-password"
                @keyup.enter="handleLogin"
              />
              <input v-model="loginForm.password"
                     type="password"
                     placeholder="请输入登录密码"
                     autocomplete="current-password"
                     @keyup.enter="handleLogin" />
            </div>
          </div>
          <div class="options">
            <label class="check"><input v-model="loginForm.rememberMe" type="checkbox" />记住账号</label>
            <label class="check"><input v-model="loginForm.rememberMe"
                     type="checkbox" />记住账号</label>
          </div>
          <button class="login-btn" type="submit" :disabled="loading">
          <button class="login-btn"
                  type="submit"
                  :disabled="loading">
            {{ loading ? "登录中..." : "登录数字工厂" }}
          </button>
        </form>
@@ -155,739 +271,901 @@
</template>
<script setup>
import { ElMessage } from "element-plus"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"
import useUserStore from "@/store/modules/user"
import defaultBrandLogo from "@/assets/logo/logo.png"
  import { ElMessage } from "element-plus";
  import Cookies from "js-cookie";
  import { encrypt, decrypt } from "@/utils/jsencrypt";
  import useUserStore from "@/store/modules/user";
  import defaultBrandLogo from "@/assets/logo/logo.png";
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
  const userStore = useUserStore();
  const route = useRoute();
  const router = useRouter();
const appTitle = String(import.meta.env.VITE_APP_TITLE || "数字工厂 MOM ç³»ç»Ÿ").trim()
const companySubtitle = String(import.meta.env.VITE_LOGIN_SUBTITLE || "Digital Factory Operation Center").trim()
const configuredLogo = String(import.meta.env.VITE_APP_LOGO || "").trim()
const logoModules = import.meta.glob("/src/assets/logo/*.png", { eager: true })
const brandIconUrl = `${import.meta.env.BASE_URL}favicon.ico`
  const appTitle = String(
    import.meta.env.VITE_APP_TITLE || "数字工厂 MOM ç³»ç»Ÿ"
  ).trim();
  const companySubtitle = String(
    import.meta.env.VITE_LOGIN_SUBTITLE || "Digital Factory Operation Center"
  ).trim();
  const configuredLogo = String(import.meta.env.VITE_APP_LOGO || "").trim();
  const logoModules = import.meta.glob("/src/assets/logo/*.png", { eager: true });
  const brandIconUrl = `${import.meta.env.BASE_URL}favicon.ico`;
const redirect = ref("")
const loading = ref(false)
const now = ref(new Date())
const brandLogoUrl = ref(defaultBrandLogo)
  const redirect = ref("");
  const loading = ref(false);
  const now = ref(new Date());
  const brandLogoUrl = ref(defaultBrandLogo);
const loginForm = ref({
  username: "",
  password: "",
  rememberMe: false,
})
  const loginForm = ref({
    username: "",
    password: "",
    rememberMe: false,
  });
const companyName = computed(() => {
  const currentFactoryName = String(userStore.currentFactoryName || "").trim()
  return currentFactoryName || appTitle
})
  const companyName = computed(() => {
    const currentFactoryName = String(userStore.currentFactoryName || "").trim();
    return currentFactoryName || appTitle;
  });
const todayLabel = computed(() => {
  const date = now.value
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
})
  const todayLabel = computed(() => {
    const date = now.value;
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
      date.getDate()
    )}`;
  });
const clockLabel = computed(() => {
  const date = now.value
  return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
})
  const clockLabel = computed(() => {
    const date = now.value;
    return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(
      date.getSeconds()
    )}`;
  });
watch(
  route,
  (newRoute) => {
    redirect.value = String(newRoute.query?.redirect || "")
  },
  { immediate: true }
)
  watch(
    route,
    newRoute => {
      redirect.value = String(newRoute.query?.redirect || "");
    },
    { immediate: true }
  );
watch(
  () => userStore.currentFactoryName,
  () => updateBrandLogo(),
  { immediate: true }
)
  watch(
    () => userStore.currentFactoryName,
    () => updateBrandLogo(),
    { immediate: true }
  );
let timer = 0
onMounted(() => {
  timer = window.setInterval(() => {
    now.value = new Date()
  }, 1000)
})
  let timer = 0;
  onMounted(() => {
    timer = window.setInterval(() => {
      now.value = new Date();
    }, 1000);
  });
onBeforeUnmount(() => {
  if (timer) {
    window.clearInterval(timer)
    timer = 0
  }
})
  onBeforeUnmount(() => {
    if (timer) {
      window.clearInterval(timer);
      timer = 0;
    }
  });
function pad(value) {
  return String(value).padStart(2, "0")
}
function resolveConfiguredLogo() {
  if (!configuredLogo) {
    return ""
  function pad(value) {
    return String(value).padStart(2, "0");
  }
  if (/^(https?:)?\/\//.test(configuredLogo) || configuredLogo.startsWith("data:")) {
    return configuredLogo
  function resolveConfiguredLogo() {
    if (!configuredLogo) {
      return "";
    }
    if (
      /^(https?:)?\/\//.test(configuredLogo) ||
      configuredLogo.startsWith("data:")
    ) {
      return configuredLogo;
    }
    const cleanPath = configuredLogo.replace(/^\/+/, "");
    const fullPath = cleanPath.startsWith("src/")
      ? `/${cleanPath}`
      : `/src/${cleanPath}`;
    const localLogo = logoModules[fullPath];
    if (localLogo && localLogo.default) {
      return localLogo.default;
    }
    if (configuredLogo.startsWith("/")) {
      return configuredLogo;
    }
    return `${import.meta.env.BASE_URL}${cleanPath}`;
  }
  const cleanPath = configuredLogo.replace(/^\/+/, "")
  const fullPath = cleanPath.startsWith("src/") ? `/${cleanPath}` : `/src/${cleanPath}`
  const localLogo = logoModules[fullPath]
  function updateBrandLogo() {
    const logoFromConfig = resolveConfiguredLogo();
    if (logoFromConfig) {
      brandLogoUrl.value = logoFromConfig;
      return;
    }
  if (localLogo && localLogo.default) {
    return localLogo.default
    const currentFactoryName = String(userStore.currentFactoryName || "").trim();
    if (!currentFactoryName) {
      brandLogoUrl.value = defaultBrandLogo;
      return;
    }
    const factoryLogoPath = `/src/assets/logo/${currentFactoryName}.png`;
    const matched = logoModules[factoryLogoPath];
    brandLogoUrl.value =
      matched && matched.default ? matched.default : defaultBrandLogo;
  }
  if (configuredLogo.startsWith("/")) {
    return configuredLogo
  function handleLogoError() {
    brandLogoUrl.value = defaultBrandLogo;
  }
  return `${import.meta.env.BASE_URL}${cleanPath}`
}
  function handleRememberCookie() {
    if (!loginForm.value.rememberMe) {
      Cookies.remove("username");
      Cookies.remove("password");
      Cookies.remove("rememberMe");
      return;
    }
function updateBrandLogo() {
  const logoFromConfig = resolveConfiguredLogo()
  if (logoFromConfig) {
    brandLogoUrl.value = logoFromConfig
    return
    Cookies.set("username", loginForm.value.username, { expires: 30 });
    Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 });
    Cookies.set("rememberMe", "true", { expires: 30 });
  }
  const currentFactoryName = String(userStore.currentFactoryName || "").trim()
  if (!currentFactoryName) {
    brandLogoUrl.value = defaultBrandLogo
    return
  function getCookie() {
    const username = Cookies.get("username");
    const password = Cookies.get("password");
    const rememberMe = Cookies.get("rememberMe");
    loginForm.value.username = username || "";
    loginForm.value.password = password ? decrypt(password) : "";
    loginForm.value.rememberMe = rememberMe === "true";
  }
  const factoryLogoPath = `/src/assets/logo/${currentFactoryName}.png`
  const matched = logoModules[factoryLogoPath]
  brandLogoUrl.value = matched && matched.default ? matched.default : defaultBrandLogo
}
  function handleLogin() {
    if (!loginForm.value.username) {
      ElMessage.error("请输入账号");
      return;
    }
    if (!loginForm.value.password) {
      ElMessage.error("请输入密码");
      return;
    }
function handleLogoError() {
  brandLogoUrl.value = defaultBrandLogo
}
function handleRememberCookie() {
  if (!loginForm.value.rememberMe) {
    Cookies.remove("username")
    Cookies.remove("password")
    Cookies.remove("rememberMe")
    return
  }
  Cookies.set("username", loginForm.value.username, { expires: 30 })
  Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })
  Cookies.set("rememberMe", "true", { expires: 30 })
}
function getCookie() {
  const username = Cookies.get("username")
  const password = Cookies.get("password")
  const rememberMe = Cookies.get("rememberMe")
  loginForm.value.username = username || ""
  loginForm.value.password = password ? decrypt(password) : ""
  loginForm.value.rememberMe = rememberMe === "true"
}
function handleLogin() {
  if (!loginForm.value.username) {
    ElMessage.error("请输入账号")
    return
  }
  if (!loginForm.value.password) {
    ElMessage.error("请输入密码")
    return
  }
  loading.value = true
  handleRememberCookie()
    loading.value = true;
    handleRememberCookie();
    userStore
      .loginCheckFactory(loginForm.value)
      .then(() => {
      const query = route.query
      const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
        if (cur !== "redirect") {
          acc[cur] = query[cur]
        }
        return acc
        }, {})
        router.push({ path: redirect.value || "/", query: otherQueryParams })
        const query = route.query;
        const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
          if (cur !== "redirect") {
            acc[cur] = query[cur];
          }
          return acc;
        }, {});
        router.push({ path: redirect.value || "/", query: otherQueryParams });
      })
      .catch(() => {})
      .finally(() => {
        loading.value = false
      })
}
        loading.value = false;
      });
  }
getCookie()
  getCookie();
</script>
<style scoped lang="scss">
* {
  box-sizing: border-box;
}
.login-page {
  --blue: #1f78ff;
  --cyan: #20e0d2;
  --violet: #8b5cf6;
  --orange: #ff8a3d;
  --text: #09203f;
  --muted: #7a8da8;
  --line: rgba(113, 154, 214, 0.24);
  --shadow: 0 28px 80px rgba(14, 57, 120, 0.18);
  min-height: 100vh;
  overflow: hidden;
  background:
    radial-gradient(circle at 12% 16%, rgba(31, 120, 255, 0.22), transparent 32%),
    radial-gradient(circle at 84% 14%, rgba(32, 224, 210, 0.24), transparent 28%),
    radial-gradient(circle at 80% 84%, rgba(139, 92, 246, 0.14), transparent 28%),
    linear-gradient(135deg, #eef6ff 0%, #f7fbff 52%, #edf8ff 100%);
}
.login-page::before {
  content: "";
  position: fixed;
  inset: 0;
  background-image:
    linear-gradient(rgba(22, 78, 160, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(22, 78, 160, 0.055) 1px, transparent 1px);
  background-size: 36px 36px;
  mask-image: radial-gradient(circle at center, #000 0%, transparent 78%);
  pointer-events: none;
}
.page {
  position: relative;
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: 28px;
  min-height: 100vh;
  padding: 36px;
}
.factory {
  position: relative;
  overflow: hidden;
  min-height: calc(100vh - 72px);
  border-radius: 30px;
  padding: 32px;
  color: #fff;
  background:
    linear-gradient(135deg, rgba(5, 27, 67, 0.98), rgba(9, 71, 143, 0.92)),
    radial-gradient(circle at 76% 18%, rgba(32, 224, 210, 0.38), transparent 30%);
  box-shadow: var(--shadow);
  border: 1px solid rgba(255, 255, 255, 0.16);
  animation: enter 0.8s ease both;
}
.factory::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(120deg, transparent 0%, rgba(255, 255, 255, 0.11) 46%, transparent 72%),
    linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.055) 1px, transparent 1px);
  background-size: 100% 100%, 44px 44px, 44px 44px;
  opacity: 0.86;
}
.brand {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
}
.logo {
  width: 76px;
  height: 76px;
  border-radius: 14px;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, var(--blue), var(--cyan));
  box-shadow: 0 16px 40px rgba(32, 224, 210, 0.26);
  overflow: hidden;
  position: relative;
}
.logo-image {
  width: 82%;
  height: 82%;
  object-fit: contain;
}
.hero-brand {
  display: block;
}
.hero-logo {
  width: 240px;
  height: 84px;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  overflow: visible;
  place-items: center start;
}
.hero-logo-image {
  width: 100%;
  height: auto;
  max-height: 100%;
  object-fit: contain;
}
.hero {
  position: relative;
  z-index: 2;
  margin-top: 64px;
  max-width: 680px;
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.78);
  font-size: 13px;
  animation: enter 0.75s ease 0.15s both;
}
.hero h1 {
  margin: 24px 0 14px;
  font-size: clamp(42px, 5.6vw, 78px);
  line-height: 1.02;
  font-weight: 900;
  letter-spacing: 0;
  animation: enter 0.8s ease 0.25s both;
}
.hero p {
  margin: 0;
  max-width: 630px;
  font-size: 17px;
  line-height: 1.8;
  color: rgba(255, 255, 255, 0.72);
  animation: enter 0.8s ease 0.35s both;
}
.scene {
  position: absolute;
  left: 42px;
  right: 42px;
  bottom: 24px;
  height: 44%;
  z-index: 1;
}
.floor {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 48%;
  border-radius: 28px;
  background:
    linear-gradient(90deg, rgba(32, 224, 210, 0), rgba(32, 224, 210, 0.22), rgba(31, 120, 255, 0)),
    repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.1) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.1) 0 1px, transparent 1px 34px);
  transform: perspective(620px) rotateX(58deg);
  transform-origin: bottom;
  opacity: 0.82;
}
.factory-svg {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 18%;
  width: 100%;
  height: 78%;
  filter: drop-shadow(0 28px 30px rgba(0, 0, 0, 0.18));
}
.belt {
  stroke-dasharray: 10 12;
  animation: flow 1.2s linear infinite;
}
.arm {
  transform-origin: 612px 124px;
  animation: armMove 3.6s ease-in-out infinite;
}
.box {
  animation: boxMove 5.4s linear infinite;
}
.box.two {
  animation-delay: -1.8s;
}
.box.three {
  animation-delay: -3.6s;
}
.signal {
  opacity: 0.8;
  animation: pulse 2.2s ease-in-out infinite;
}
.signal.two {
  animation-delay: -0.7s;
}
.signal.three {
  animation-delay: -1.4s;
}
.login-wrap {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh - 72px);
  border-radius: 30px;
  border: 1px solid rgba(255, 255, 255, 0.8);
  background: rgba(255, 255, 255, 0.62);
  backdrop-filter: blur(20px);
  box-shadow: var(--shadow);
  animation: enter 0.8s ease 0.12s both;
}
.time {
  position: absolute;
  top: 26px;
  right: 28px;
  display: flex;
  gap: 9px;
  align-items: center;
  font-weight: 900;
  color: #12325e;
}
.time span {
  padding: 8px 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.66);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
}
.login-card {
  width: min(440px, 100%);
  padding: 34px;
  border-radius: 28px;
  border: 1px solid rgba(255, 255, 255, 0.95);
  background: rgba(255, 255, 255, 0.88);
  box-shadow: 0 22px 60px rgba(21, 73, 143, 0.15);
  position: relative;
  overflow: hidden;
}
.login-card::before {
  content: "";
  position: absolute;
  right: -92px;
  top: -92px;
  width: 190px;
  height: 190px;
  border-radius: 999px;
  background: conic-gradient(from 0deg, rgba(31, 120, 255, 0.2), rgba(32, 224, 210, 0.32), rgba(139, 92, 246, 0.18), rgba(31, 120, 255, 0.2));
  animation: rotate 9s linear infinite;
}
.login-card > * {
  position: relative;
  z-index: 1;
}
.card-brand {
  margin-bottom: 18px;
}
.card-brand .logo {
  width: 52px;
  height: 52px;
}
.card-brand-copy {
  margin-left: 12px;
}
.card-logo-image {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.card-brand .logo {
  width: 68px;
  height: 68px;
}
.login-card h2 {
  margin: 8px 0;
  font-size: 31px;
  line-height: 1.15;
  color: #0d2c5e;
  font-weight: 900;
  letter-spacing: 0;
}
.sub {
  margin: 0 0 24px;
  color: var(--muted);
  font-size: 14px;
}
.form-row {
  margin-bottom: 14px;
  animation: enter 0.6s ease both;
}
.form-row:nth-of-type(1) {
  animation-delay: 0.32s;
}
.form-row:nth-of-type(2) {
  animation-delay: 0.4s;
}
.form-row:nth-of-type(3) {
  animation-delay: 0.48s;
}
label {
  display: block;
  margin-bottom: 8px;
  color: #24436b;
  font-size: 13px;
  font-weight: 800;
}
.input {
  position: relative;
}
.input svg {
  position: absolute;
  left: 15px;
  top: 50%;
  width: 19px;
  height: 19px;
  transform: translateY(-50%);
  color: #8197b6;
  pointer-events: none;
}
input[type="text"],
input[type="password"] {
  width: 100%;
  height: 52px;
  padding: 0 15px 0 46px;
  border: 1px solid rgba(108, 143, 190, 0.34);
  border-radius: 16px;
  outline: none;
  background: linear-gradient(180deg, #fff, #f8fbff);
  color: var(--text);
  font-size: 15px;
  transition: 0.2s ease;
}
input[type="text"]:hover,
input[type="password"]:hover {
  border-color: rgba(31, 120, 255, 0.48);
  box-shadow: 0 10px 24px rgba(31, 120, 255, 0.08);
}
input[type="text"]:focus,
input[type="password"]:focus {
  border-color: rgba(31, 120, 255, 0.72);
  box-shadow: 0 0 0 4px rgba(31, 120, 255, 0.1);
}
.options {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  margin: 12px 0 22px;
  font-size: 13px;
  color: var(--muted);
}
.check {
  display: inline-flex;
  gap: 8px;
  align-items: center;
  cursor: pointer;
}
.check input {
  width: 16px;
  height: 16px;
  padding: 0;
  accent-color: var(--blue);
}
.login-btn {
  width: 100%;
  height: 54px;
  border: none;
  border-radius: 17px;
  cursor: pointer;
  color: #fff;
  font-size: 16px;
  font-weight: 900;
  background: linear-gradient(135deg, var(--blue), var(--cyan));
  box-shadow: 0 18px 38px rgba(31, 120, 255, 0.25);
  position: relative;
  overflow: hidden;
  transition: 0.18s ease;
}
.login-btn:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 22px 48px rgba(31, 120, 255, 0.32);
}
.login-btn:disabled {
  opacity: 0.8;
  cursor: not-allowed;
}
@keyframes enter {
  from {
    opacity: 0;
    transform: translateY(26px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
@keyframes rotate {
  to {
    transform: rotate(360deg);
  }
}
@keyframes flow {
  to {
    stroke-dashoffset: -44;
  }
}
@keyframes boxMove {
  0% {
    transform: translateX(-150px);
    opacity: 0;
  }
  12% {
    opacity: 1;
  }
  78% {
    opacity: 1;
  }
  100% {
    transform: translateX(650px);
    opacity: 0;
  }
}
@keyframes armMove {
  0%,
  100% {
    transform: rotate(-4deg);
  }
  50% {
    transform: rotate(9deg);
  }
}
@keyframes pulse {
  0%,
  100% {
    opacity: 0.45;
    transform: scale(1);
  }
  50% {
    opacity: 1;
    transform: scale(1.18);
  }
}
@keyframes glow {
  0%,
  100% {
    box-shadow: 0 0 18px rgba(32, 224, 210, 0.3);
  }
  50% {
    box-shadow: 0 0 34px rgba(32, 224, 210, 0.68);
  }
}
@media (max-width: 1320px) {
  .hero h1 {
    font-size: clamp(52px, 5.2vw, 78px);
  }
}
@media (max-width: 980px) {
  .login-page {
    overflow: auto;
    --accent: var(--accent-primary, var(--el-color-primary, #374d77));
    --accent-rgb: var(--el-color-primary-rgb, 22, 116, 88);
    --text: #0f172a;
    --muted: #64748b;
    --border: rgba(15, 23, 42, 0.1);
    --shadow: 0 24px 70px rgba(2, 6, 23, 0.12);
    position: relative;
    min-height: 100vh;
    overflow: hidden;
    background: radial-gradient(
        circle at 12% 14%,
        rgba(var(--accent-rgb), 0.14),
        transparent 38%
      ),
      radial-gradient(circle at 92% 8%, rgba(2, 132, 199, 0.12), transparent 36%),
      conic-gradient(
        from 220deg at 24% 18%,
        rgba(var(--accent-rgb), 0.1),
        rgba(56, 189, 248, 0.08),
        rgba(99, 102, 241, 0.08),
        rgba(var(--accent-rgb), 0.1)
      ),
      linear-gradient(180deg, #f7f8fb 0%, #f3f5fa 100%);
  }
  .login-page * {
    box-sizing: border-box;
  }
  .login-page::before {
    content: "";
    position: fixed;
    inset: 0;
    background-image: radial-gradient(rgba(15, 23, 42, 0.05) 1px, transparent 1px),
      linear-gradient(rgba(15, 23, 42, 0.045) 1px, transparent 1px),
      linear-gradient(90deg, rgba(15, 23, 42, 0.045) 1px, transparent 1px),
      repeating-linear-gradient(
        118deg,
        transparent 0 84px,
        rgba(var(--accent-rgb), 0.26) 84px 86px,
        transparent 86px 168px
      ),
      repeating-linear-gradient(
        208deg,
        transparent 0 132px,
        rgba(2, 132, 199, 0.22) 132px 134px,
        transparent 134px 264px
      ),
      repeating-linear-gradient(
        156deg,
        transparent 0 112px,
        rgba(99, 102, 241, 0.2) 112px 114px,
        transparent 114px 224px
      ),
      repeating-linear-gradient(
        118deg,
        transparent 0 84px,
        rgba(var(--accent-rgb), 0.12) 83px 89px,
        transparent 89px 168px
      );
    background-size: 22px 22px, 52px 52px, 52px 52px, auto, auto, auto, auto;
    background-position: 0 0, 0 0, 0 0, 0 0, -120px 140px, 120px 0;
    mask-image: radial-gradient(circle at 42% 42%, #000 0%, transparent 80%);
    opacity: 1;
    mix-blend-mode: normal;
    filter: saturate(1.08);
    animation: lineDrift 12s linear infinite;
    pointer-events: none;
    z-index: 0;
  }
  .login-page::after {
    content: "";
    position: fixed;
    inset: -28vmax;
    background: radial-gradient(
        circle at 18% 28%,
        rgba(var(--accent-rgb), 0.22),
        transparent 54%
      ),
      radial-gradient(
        circle at 82% 22%,
        rgba(56, 189, 248, 0.16),
        transparent 50%
      ),
      radial-gradient(
        circle at 72% 82%,
        rgba(99, 102, 241, 0.12),
        transparent 52%
      ),
      radial-gradient(circle at 26% 78%, rgba(244, 63, 94, 0.06), transparent 56%);
    filter: blur(42px) saturate(1.05);
    opacity: 0.9;
    transform: translate3d(0, 0, 0);
    animation: bgFloat 18s ease-in-out infinite;
    pointer-events: none;
    z-index: 0;
  }
  .page {
    grid-template-columns: 1fr;
    padding: 18px;
    min-height: auto;
    position: relative;
    z-index: 1;
    max-width: 1200px;
    margin: 48px auto;
    width: calc(100% - 48px);
    min-height: calc(100vh - 96px);
    display: grid;
    grid-template-columns: 3fr 2fr;
    gap: 0;
    align-items: stretch;
    border-radius: 34px;
    background: rgba(255, 255, 255, 0.72);
    border: 1px solid rgba(255, 255, 255, 0.72);
    box-shadow: var(--shadow);
    backdrop-filter: blur(18px);
    overflow: hidden;
  }
  .factory,
  .login-wrap {
    min-height: auto;
  .page::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(
          circle at 22% 22%,
          rgba(var(--accent-rgb), 0.34),
          transparent 58%
        )
        left center / 60% 100% no-repeat,
      radial-gradient(
          circle at 76% 12%,
          rgba(56, 189, 248, 0.22),
          transparent 50%
        )
        left center / 60% 100% no-repeat,
      linear-gradient(
          135deg,
          rgba(2, 6, 23, 0.96) 0%,
          rgba(15, 35, 57, 0.94) 52%,
          rgba(15, 35, 57, 0.88) 100%
        )
        left center / 60% 100% no-repeat;
    pointer-events: none;
    z-index: 0;
  }
  .page::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 60%;
    width: 1px;
    background: linear-gradient(
      180deg,
      transparent,
      rgba(15, 23, 42, 0.16),
      transparent
    );
    pointer-events: none;
    z-index: 0;
  }
  .factory {
    min-height: 760px;
    position: relative;
    z-index: 1;
    padding: 44px 48px 40px;
    color: rgba(255, 255, 255, 0.92);
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }
  .time {
  .brand {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
  }
  .logo {
    width: 52px;
    height: 52px;
    border-radius: 14px;
    display: grid;
    place-items: center;
    background: rgba(var(--accent-rgb), 0.12);
    border: 1px solid rgba(var(--accent-rgb), 0.22);
    overflow: hidden;
  }
  .logo-image {
    width: 80%;
    height: 80%;
    object-fit: contain;
  }
  .hero-brand {
    display: block;
  }
  .hero-logo {
    width: 260px;
    height: 72px;
    border-radius: 0;
    background: transparent;
    border: none;
    place-items: center start;
  }
  .hero-logo-image {
    width: 100%;
    height: 100%;
    object-fit: contain;
    filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.22));
  }
  .hero {
    position: relative;
    z-index: 1;
    margin: 46px 0 0;
    max-width: 640px;
  }
  .chip {
    display: inline-flex;
    align-items: center;
    padding: 7px 12px;
    border-radius: 999px;
    border: 1px solid rgba(255, 255, 255, 0.16);
    background: rgba(255, 255, 255, 0.08);
    color: rgba(255, 255, 255, 0.78);
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }
  .hero h1 {
    margin: 22px 0 14px;
    font-size: clamp(44px, 5vw, 66px);
    line-height: 1.06;
    font-weight: 860;
    letter-spacing: -0.02em;
    color: #ffffff;
  }
  .hero p {
    margin: 0;
    max-width: 600px;
    font-size: 16px;
    line-height: 1.9;
    color: rgba(255, 255, 255, 0.72);
  }
  .scene {
    display: none;
  }
  .login-wrap {
    padding: 22px;
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 36px 44px;
  }
  .time {
    position: absolute;
    top: 18px;
    right: 18px;
    display: flex;
    gap: 10px;
    align-items: center;
    color: rgba(15, 23, 42, 0.72);
    font-size: 13px;
    font-weight: 700;
  }
  .time span {
    padding: 6px 10px;
    border-radius: 999px;
    border: 1px solid rgba(15, 23, 42, 0.12);
    background: rgba(255, 255, 255, 0.7);
    color: rgba(15, 23, 42, 0.6);
    font-weight: 800;
  }
  .login-card {
    width: min(420px, 100%);
    padding: 38px 34px 34px;
    border-radius: 22px;
    border: 1px solid rgba(15, 23, 42, 0.12);
    background: rgba(255, 255, 255, 0.86);
    backdrop-filter: blur(14px);
    box-shadow: var(--shadow);
    position: relative;
    overflow: hidden;
  }
  .login-card::before {
    content: "";
    position: absolute;
    left: -120px;
    top: -120px;
    width: 240px;
    height: 240px;
    border-radius: 999px;
    background: radial-gradient(
      circle at 30% 30%,
      rgba(var(--accent-rgb), 0.36),
      transparent 60%
    );
    filter: blur(1px);
  }
  .login-card > * {
    position: relative;
    z-index: 1;
  }
  .card-brand {
    margin-bottom: 18px;
    gap: 12px;
  }
  .card-brand .logo {
    width: 46px;
    height: 46px;
    border-radius: 14px;
    background: rgba(var(--accent-rgb), 0.12);
    border: 1px solid rgba(var(--accent-rgb), 0.22);
  }
  .card-brand-copy {
    margin-left: 0;
  }
  .brand-title {
    font-size: 15px;
    font-weight: 900;
    color: rgba(15, 23, 42, 0.92);
    letter-spacing: 0.02em;
  }
  .card-brand-copy small {
    display: block;
    margin-top: 4px;
    font-size: 12px;
    color: rgba(15, 23, 42, 0.55);
    font-weight: 700;
  }
  .login-card h2 {
    margin: 10px 0 8px;
    font-size: 28px;
    line-height: 1.2;
    color: rgba(15, 23, 42, 0.92);
    font-weight: 900;
    letter-spacing: -0.02em;
  }
  .sub {
    margin: 0 0 22px;
    color: var(--muted);
    font-size: 13px;
    font-weight: 700;
  }
  .form-row {
    margin-bottom: 14px;
  }
  label {
    display: block;
    margin-bottom: 8px;
    color: rgba(15, 23, 42, 0.72);
    font-size: 12px;
    font-weight: 800;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }
  .input {
    position: relative;
  }
  .input svg {
    position: absolute;
    left: 14px;
    top: 50%;
    width: 18px;
    height: 18px;
    transform: translateY(-50%);
    color: rgba(15, 23, 42, 0.45);
    pointer-events: none;
  }
  input[type="text"],
  input[type="password"] {
    width: 100%;
    padding: 26px;
    height: 50px;
    padding: 0 14px 0 44px;
    border: 1px solid var(--border);
    border-radius: 14px;
    outline: none;
    background: rgba(255, 255, 255, 0.92);
    color: var(--text);
    font-size: 15px;
    transition: box-shadow 0.16s ease, border-color 0.16s ease,
      transform 0.16s ease;
  }
  input[type="text"]::placeholder,
  input[type="password"]::placeholder {
    color: rgba(100, 116, 139, 0.88);
  }
  input[type="text"]:hover,
  input[type="password"]:hover {
    border-color: rgba(var(--accent-rgb), 0.28);
  }
  input[type="text"]:focus,
  input[type="password"]:focus {
    border-color: rgba(var(--accent-rgb), 0.6);
    box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.14);
  }
  .options {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    margin: 14px 0 22px;
    font-size: 13px;
    color: rgba(15, 23, 42, 0.62);
    font-weight: 700;
  }
  .check {
    display: inline-flex;
    gap: 10px;
    align-items: center;
    cursor: pointer;
    user-select: none;
  }
  .check input {
    width: 16px;
    height: 16px;
    padding: 0;
    accent-color: var(--accent);
  }
  .login-btn {
    font-size: 16px;
    width: 100%;
    height: 52px;
    border: none;
    border-radius: 14px;
    cursor: pointer;
    color: #ffffff;
    font-size: 15px;
    font-weight: 900;
    letter-spacing: 0.02em;
    background: var(--accent);
    box-shadow: 0 18px 44px rgba(var(--accent-rgb), 0.22);
    transition: transform 0.16s ease, box-shadow 0.16s ease, filter 0.16s ease;
  }
}
  .login-btn:hover:not(:disabled) {
    transform: translateY(-1px);
    box-shadow: 0 20px 52px rgba(var(--accent-rgb), 0.28);
    filter: saturate(1.02);
  }
  .login-btn:active:not(:disabled) {
    transform: translateY(0);
  }
  .login-btn:disabled {
    opacity: 0.72;
    cursor: not-allowed;
  }
  .login-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.18),
      0 18px 44px rgba(var(--accent-rgb), 0.22);
  }
  :global(html.dark) .login-page {
    --text: rgba(248, 250, 252, 0.92);
    --muted: rgba(226, 232, 240, 0.62);
    --border: rgba(226, 232, 240, 0.16);
    background: radial-gradient(
        circle at 14% 18%,
        rgba(var(--accent-rgb), 0.22),
        transparent 40%
      ),
      radial-gradient(
        circle at 86% 12%,
        rgba(56, 189, 248, 0.14),
        transparent 42%
      ),
      conic-gradient(
        from 220deg at 22% 16%,
        rgba(var(--accent-rgb), 0.16),
        rgba(56, 189, 248, 0.12),
        rgba(99, 102, 241, 0.12),
        rgba(var(--accent-rgb), 0.16)
      ),
      linear-gradient(180deg, #0b1220 0%, #070b13 100%);
  }
  :global(html.dark) .login-page::before {
    background-image: radial-gradient(
        rgba(226, 232, 240, 0.07) 1px,
        transparent 1px
      ),
      linear-gradient(rgba(226, 232, 240, 0.06) 1px, transparent 1px),
      linear-gradient(90deg, rgba(226, 232, 240, 0.06) 1px, transparent 1px),
      repeating-linear-gradient(
        118deg,
        transparent 0 84px,
        rgba(var(--accent-rgb), 0.32) 84px 86px,
        transparent 86px 168px
      ),
      repeating-linear-gradient(
        208deg,
        transparent 0 132px,
        rgba(56, 189, 248, 0.28) 132px 134px,
        transparent 134px 264px
      ),
      repeating-linear-gradient(
        156deg,
        transparent 0 112px,
        rgba(99, 102, 241, 0.26) 112px 114px,
        transparent 114px 224px
      ),
      repeating-linear-gradient(
        118deg,
        transparent 0 84px,
        rgba(var(--accent-rgb), 0.16) 83px 89px,
        transparent 89px 168px
      );
    background-size: 22px 22px, 52px 52px, 52px 52px, auto, auto, auto, auto;
    opacity: 0.96;
    mix-blend-mode: screen;
  }
  :global(html.dark) .login-page::after {
    opacity: 0.7;
    filter: blur(46px) saturate(1.08);
    background: radial-gradient(
        circle at 18% 28%,
        rgba(var(--accent-rgb), 0.28),
        transparent 56%
      ),
      radial-gradient(
        circle at 84% 18%,
        rgba(56, 189, 248, 0.18),
        transparent 52%
      ),
      radial-gradient(
        circle at 70% 82%,
        rgba(99, 102, 241, 0.14),
        transparent 56%
      ),
      radial-gradient(circle at 30% 84%, rgba(244, 63, 94, 0.06), transparent 60%);
  }
  :global(html.dark) .time {
    color: rgba(226, 232, 240, 0.68);
  }
  :global(html.dark) .time span {
    background: rgba(15, 23, 42, 0.5);
    border-color: rgba(226, 232, 240, 0.16);
    color: rgba(226, 232, 240, 0.72);
  }
  :global(html.dark) .login-card {
    background: rgba(2, 6, 23, 0.56);
    border-color: rgba(226, 232, 240, 0.16);
    box-shadow: 0 24px 70px rgba(0, 0, 0, 0.46);
  }
  :global(html.dark) .page {
    background: rgba(2, 6, 23, 0.46);
    border-color: rgba(226, 232, 240, 0.12);
    box-shadow: 0 38px 96px rgba(0, 0, 0, 0.5);
  }
  :global(html.dark) .page::after {
    background: linear-gradient(
      180deg,
      transparent,
      rgba(226, 232, 240, 0.14),
      transparent
    );
  }
  :global(html.dark) .brand-title {
    color: rgba(248, 250, 252, 0.92);
  }
  :global(html.dark) .card-brand-copy small {
    color: rgba(226, 232, 240, 0.62);
  }
  :global(html.dark) .login-card h2 {
    color: rgba(248, 250, 252, 0.92);
  }
  :global(html.dark) label {
    color: rgba(226, 232, 240, 0.72);
  }
  :global(html.dark) .input svg {
    color: rgba(226, 232, 240, 0.55);
  }
  :global(html.dark) input[type="text"],
  :global(html.dark) input[type="password"] {
    background: rgba(2, 6, 23, 0.38);
    color: rgba(248, 250, 252, 0.92);
  }
  :global(html.dark) input[type="text"]::placeholder,
  :global(html.dark) input[type="password"]::placeholder {
    color: rgba(226, 232, 240, 0.55);
  }
  :global(html.dark) .options {
    color: rgba(226, 232, 240, 0.68);
  }
  @keyframes bgFloat {
    0% {
      transform: translate3d(-1.8%, -1.2%, 0) scale(1.02);
    }
    50% {
      transform: translate3d(1.6%, 1.4%, 0) scale(1.06);
    }
    100% {
      transform: translate3d(-1.8%, -1.2%, 0) scale(1.02);
    }
  }
  @keyframes lineDrift {
    0% {
      background-position: 0 0, 0 0, 0 0, 0 0, -120px 140px, 120px 0, 0 0;
    }
    100% {
      background-position: 0 0, 220px 260px, -240px 210px, 760px -520px,
        -980px 720px, 680px 860px, 760px -520px;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .login-page::before {
      animation: none;
    }
    .login-page::after {
      animation: none;
    }
  }
  @media (max-width: 980px) {
    .login-page {
      overflow: auto;
    }
    .page {
      grid-template-columns: 1fr;
      gap: 18px;
      margin: 18px auto;
      width: calc(100% - 32px);
      min-height: auto;
      max-width: 560px;
    }
    .page::before,
    .page::after {
      display: none;
    }
    .factory {
      display: none;
    }
    .login-wrap {
      padding: 0;
    }
    .time {
      display: none;
    }
    .login-card {
      width: 100%;
      padding: 30px 22px 22px;
    }
  }
</style>