zhangwencui
16 分钟以前 e38c0656952f70552170fc4ee72420b0c741919e
样式修改
已添加1个文件
已修改19个文件
3032 ■■■■ 文件已修改
src/assets/image.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/element-ui.scss 202 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/index.scss 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/ruoyi.scss 578 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/variables.module.scss 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/TopNav/index.vue 433 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppMain.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 261 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Settings/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Logo.vue 240 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/SidebarItem.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 149 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/index.vue 729 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 156 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/settings.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/settings.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/components/viewDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/formDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/image.png
src/assets/styles/element-ui.scss
@@ -1,52 +1,52 @@
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
  font-weight: 400 !important;
}
.el-upload {
  input[type="file"] {
    display: none !important;
  }
}
.el-upload__input {
  display: none;
}
.cell {
  .el-tag {
    margin-right: 0px;
  }
}
.small-padding {
  .cell {
    padding-left: 5px;
    padding-right: 5px;
  }
}
.fixed-width {
  .el-button--mini {
    padding: 7px 10px;
    width: 60px;
  }
}
.status-col {
  .cell {
    padding: 0 10px;
    text-align: center;
    .el-tag {
      margin-right: 0px;
    }
  }
}
// to fixed https://github.com/ElemeFE/element/issues/2461
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
  font-weight: 400 !important;
}
.el-upload {
  input[type="file"] {
    display: none !important;
  }
}
.el-upload__input {
  display: none;
}
.cell {
  .el-tag {
    margin-right: 0px;
  }
}
.small-padding {
  .cell {
    padding-left: 5px;
    padding-right: 5px;
  }
}
.fixed-width {
  .el-button--mini {
    padding: 7px 10px;
    width: 60px;
  }
}
.status-col {
  .cell {
    padding: 0 10px;
    text-align: center;
    .el-tag {
      margin-right: 0px;
    }
  }
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
  transform: none;
  left: 0;
@@ -99,64 +99,64 @@
.el-message-box__content {
  padding: 24px 24px 0;
}
.el-message-box__container {
  justify-content: center;
}
.el-message-box__btns {
  text-align: center;
  padding: 16px;
  display: flex;
  flex-direction: row-reverse;
  justify-content: center;
  align-items: center;
  .el-button--primary {
    margin-right: 12px;
  }
}
.el-message-box__container {
  justify-content: center;
}
.el-message-box__btns {
  text-align: center;
  padding: 16px;
  display: flex;
  flex-direction: row-reverse;
  justify-content: center;
  align-items: center;
  .el-button--primary {
    margin-right: 12px;
  }
}
.el-table__expanded-cell {
  padding: 0 !important;
  .el-table__header-wrapper {
    background-color: var(--surface-soft) !important;
  }
}
// refine element ui upload
.upload-container {
  .el-upload {
    width: 100%;
    .el-upload-dragger {
      width: 100%;
      height: 200px;
    }
  }
}
// dropdown
.el-dropdown-menu {
  a {
    display: block;
  }
}
// fix date-picker ui bug in filter-item
.el-range-editor.el-input__inner {
  display: inline-flex !important;
}
// to fix el-date-picker css style
.el-range-separator {
  box-sizing: content-box;
}
.el-menu--collapse
  > div
  > .el-submenu
  > .el-submenu__title
  .el-submenu__icon-arrow {
  display: none;
}
// refine element ui upload
.upload-container {
  .el-upload {
    width: 100%;
    .el-upload-dragger {
      width: 100%;
      height: 200px;
    }
  }
}
// dropdown
.el-dropdown-menu {
  a {
    display: block;
  }
}
// fix date-picker ui bug in filter-item
.el-range-editor.el-input__inner {
  display: inline-flex !important;
}
// to fix el-date-picker css style
.el-range-separator {
  box-sizing: content-box;
}
.el-menu--collapse
  > div
  > .el-submenu
  > .el-submenu__title
  .el-submenu__icon-arrow {
  display: none;
}
.el-dropdown .el-dropdown-link {
  color: var(--el-color-primary) !important;
}
src/assets/styles/index.scss
@@ -1,24 +1,22 @@
@import './variables.module.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
@import './btn.scss';
@import './ruoyi.scss';
@import "./variables.module.scss";
@import "./mixin.scss";
@import "./transition.scss";
@import "./element-ui.scss";
@import "./sidebar.scss";
@import "./btn.scss";
@import "./ruoyi.scss";
body {
  height: 100%;
  margin: 0;
body {
  height: 100%;
  margin: 0;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  background:
    radial-gradient(circle at 9% -6%, rgba(59, 130, 246, 0.14), transparent 36%),
    radial-gradient(circle at 88% -8%, rgba(56, 189, 248, 0.12), transparent 30%),
    linear-gradient(165deg, #f3f7fc 0%, #eef5ff 54%, #f8fbff 100%);
  color: var(--text-primary);
}
  text-rendering: optimizeLegibility;
  font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  background: radial-gradient(circle at 9% -6%, rgba(59, 130, 246, 0.14), transparent 36%), radial-gradient(circle at 88% -8%, rgba(56, 189, 248, 0.12), transparent 30%),
    linear-gradient(165deg, #f3f7fc 0%, #eef5ff 54%, #f8fbff 100%);
  color: var(--text-primary);
}
label {
  font-weight: 600;
@@ -34,11 +32,11 @@
  height: 100%;
}
html,
body,
#app {
  background-color: var(--app-bg);
}
html,
body,
#app {
  background-color: var(--app-bg);
}
*,
*:before,
@@ -110,17 +108,17 @@
  }
}
aside {
  background: rgba(255, 255, 255, 0.84);
  padding: 8px 24px;
  margin-bottom: 20px;
  border-radius: var(--radius-md);
  border: 1px solid var(--surface-border);
aside {
  background: rgba(255, 255, 255, 0.84);
  padding: 8px 24px;
  margin-bottom: 20px;
  border-radius: var(--radius-md);
  border: 1px solid var(--surface-border);
  display: block;
  line-height: 32px;
  font-size: 16px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
  color: var(--text-secondary);
  color: var(--text-secondary);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
@@ -136,34 +134,57 @@
//main-container全局样式
.app-container {
  padding: 20px 24px 24px;
  --radius-lg: 0px;
  --radius-md: 0px;
  --radius-sm: 0px;
  --radius-xs: 0px;
  --el-border-radius-base: 0px;
  --el-border-radius-small: 0px;
  --el-border-radius-round: 0px;
  --el-border-radius-circle: 0px;
  padding: 20px;
  background-color: var(--surface-base, #ffffff);
  border-radius: 0;
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  border: 1px solid var(--surface-border, #e2e8f0);
  min-height: calc(100vh - var(--topbar-height, 64px) - var(--content-gap, 16px) - 20px);
  margin-bottom: 10px;
  @media (max-width: 768px) {
    padding: 12px;
    border-radius: 0;
  }
}
.search_form {
.search_form {
  display: flex;
  align-items: center;
  justify-content: space-between;
  .search_title {
  margin-bottom: 16px;
  padding-bottom: 16px;
  border-bottom: 1px dashed var(--surface-border, #e2e8f0);
  .search_title {
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--text-secondary);
  }
}
.table_list {
  background: var(--panel-mask);
  border: 1px solid var(--surface-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  backdrop-filter: blur(12px);
  padding: 18px;
}
    color: var(--text-secondary);
  }
}
.table_list {
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  backdrop-filter: none;
  padding: 0;
}
.components-container {
  margin: 30px 50px;
  position: relative;
}
.text-center {
  text-align: center
  text-align: center;
}
.sub-navbar {
@@ -190,15 +211,15 @@
  }
}
.link-type,
.link-type:focus {
  color: var(--accent-light);
  cursor: pointer;
  &:hover {
    color: #2563eb;
  }
}
.link-type,
.link-type:focus {
  color: var(--accent-light);
  cursor: pointer;
  &:hover {
    color: #2563eb;
  }
}
.filter-container {
  padding-bottom: 10px;
src/assets/styles/ruoyi.scss
@@ -1,289 +1,289 @@
/**
 * 通用css样式布局处理
 * Copyright (c) 2019 ruoyi
 */
/** 基础通用 **/
.pt5 {
  padding-top: 5px;
}
.pr5 {
  padding-right: 5px;
}
.pb5 {
  padding-bottom: 5px;
}
.mt5 {
  margin-top: 5px;
}
.mr5 {
  margin-right: 5px;
}
.mb5 {
  margin-bottom: 5px;
}
.mb8 {
  margin-bottom: 8px;
}
.ml5 {
  margin-left: 5px;
}
.mt10 {
  margin-top: 10px;
}
.mr10 {
  margin-right: 10px;
}
.mb10 {
  margin-bottom: 10px;
}
.ml10 {
  margin-left: 10px;
}
.mt20 {
  margin-top: 20px;
}
.mr20 {
  margin-right: 20px;
}
.mb20 {
  margin-bottom: 20px;
}
.ml20 {
  margin-left: 20px;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
  font-family: inherit;
  font-weight: 500;
  line-height: 1.1;
  color: inherit;
}
.el-form .el-form-item__label {
  font-weight: 700;
}
.el-dialog:not(.is-fullscreen) {
  margin-top: 6vh !important;
}
.el-dialog.scrollbar .el-dialog__body {
  overflow: auto;
  overflow-x: hidden;
  max-height: 70vh;
}
.el-table {
  .el-table__header-wrapper, .el-table__fixed-header-wrapper {
    th {
      word-break: break-word;
      background-color: #F0F1F5 !important;
      color: #515a6e;
      height: 40px !important;
      font-size: 13px;
    }
  }
  .el-table__body-wrapper {
    .el-button [class*="el-icon-"] + span {
      margin-left: 1px;
    }
  }
}
/** 表单布局 **/
.form-header {
  font-size:15px;
  color:#6379bb;
  border-bottom:1px solid #ddd;
  margin:8px 10px 25px 10px;
  padding-bottom:5px
}
/** 表格布局 **/
.pagination-container {
  display: flex;
  justify-content: flex-end;
  margin-top: 20px;
  background-color: transparent !important;
}
/* 弹窗中的分页器 */
.el-dialog .pagination-container {
  position: static !important;
  margin: 10px 0 0 0;
  padding: 0 !important;
  .el-pagination {
    position: static;
  }
}
/* 移动端适配 */
@media (max-width: 768px) {
  .pagination-container {
    .el-pagination {
      > .el-pagination__jump {
        display: none !important;
      }
      > .el-pagination__sizes {
        display: none !important;
      }
    }
  }
}
/* tree border */
.tree-border {
  margin-top: 5px;
  border: 1px solid var(--el-border-color-light, #e5e6e7);
  background: var(--el-bg-color, #FFFFFF) none;
  border-radius:4px;
  width: 100%;
}
.el-table .fixed-width .el-button--small {
  padding-left: 0;
  padding-right: 0;
  width: inherit;
}
/** 表格更多操作下拉样式 */
.el-table .el-dropdown-link {
  cursor: pointer;
  color: #2C51D9;
  margin-left: 10px;
}
.el-table .el-dropdown, .el-icon-arrow-down {
  font-size: 12px;
}
.el-tree-node__content > .el-checkbox {
  margin-right: 8px;
}
.list-group-striped > .list-group-item {
  border-left: 0;
  border-right: 0;
  border-radius: 0;
  padding-left: 0;
  padding-right: 0;
}
.list-group {
  padding-left: 0px;
  list-style: none;
}
.list-group-item {
  border-bottom: 1px solid #e7eaec;
  border-top: 1px solid #e7eaec;
  margin-bottom: -1px;
  padding: 11px 0px;
  font-size: 13px;
}
.pull-right {
  float: right !important;
}
.el-card__header {
  padding: 14px 15px 7px !important;
  min-height: 40px;
}
.el-card__body {
  padding: 15px 20px 20px 20px !important;
}
.card-box {
  margin-bottom: 10px;
}
/* button color */
.el-button--cyan.is-active,
.el-button--cyan:active {
  background: #20B2AA;
  border-color: #20B2AA;
  color: #FFFFFF;
}
.el-button--cyan:focus,
.el-button--cyan:hover {
  background: #48D1CC;
  border-color: #48D1CC;
  color: #FFFFFF;
}
.el-button--cyan {
  background-color: #20B2AA;
  border-color: #20B2AA;
  color: #FFFFFF;
}
/* text color */
.text-navy {
  color: #1ab394;
}
.text-primary {
  color: inherit;
}
.text-success {
  color: #1c84c6;
}
.text-info {
  color: #23c6c8;
}
.text-warning {
  color: #f8ac59;
}
.text-danger {
  color: #ed5565;
}
.text-muted {
  color: #888888;
}
/* image */
.img-circle {
  border-radius: 50%;
}
.img-lg {
  width: 120px;
  height: 120px;
}
.avatar-upload-preview {
  position: absolute;
  top: 50%;
  transform: translate(50%, -50%);
  width: 200px;
  height: 200px;
  border-radius: 50%;
  box-shadow: 0 0 4px #ccc;
  overflow: hidden;
}
/* 拖拽列样式 */
.sortable-ghost{
  opacity: .8;
  color: #fff!important;
  background: #42b983!important;
}
/* 表格右侧工具栏样式 */
.top-right-btn {
  margin-left: auto;
}
/* 分割面板样式 */
.splitpanes.default-theme .splitpanes__pane {
  background-color: var(--splitpanes-default-bg) !important;
}
/**
 * 通用css样式布局处理
 * Copyright (c) 2019 ruoyi
 */
/** 基础通用 **/
.pt5 {
  padding-top: 5px;
}
.pr5 {
  padding-right: 5px;
}
.pb5 {
  padding-bottom: 5px;
}
.mt5 {
  margin-top: 5px;
}
.mr5 {
  margin-right: 5px;
}
.mb5 {
  margin-bottom: 5px;
}
.mb8 {
  margin-bottom: 8px;
}
.ml5 {
  margin-left: 5px;
}
.mt10 {
  margin-top: 10px;
}
.mr10 {
  margin-right: 10px;
}
.mb10 {
  margin-bottom: 10px;
}
.ml10 {
  margin-left: 10px;
}
.mt20 {
  margin-top: 20px;
}
.mr20 {
  margin-right: 20px;
}
.mb20 {
  margin-bottom: 20px;
}
.ml20 {
  margin-left: 20px;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
  font-family: inherit;
  font-weight: 500;
  line-height: 1.1;
  color: inherit;
}
.el-form .el-form-item__label {
  font-weight: 700;
}
.el-dialog:not(.is-fullscreen) {
  margin-top: 6vh !important;
}
.el-dialog.scrollbar .el-dialog__body {
  overflow: auto;
  overflow-x: hidden;
  max-height: 70vh;
}
.el-table {
  .el-table__header-wrapper, .el-table__fixed-header-wrapper {
    th {
      word-break: break-word;
      background-color: #F0F1F5 !important;
      color: #515a6e;
      height: 40px !important;
      font-size: 13px;
    }
  }
  .el-table__body-wrapper {
    .el-button [class*="el-icon-"] + span {
      margin-left: 1px;
    }
  }
}
/** 表单布局 **/
.form-header {
  font-size:15px;
  color:#6379bb;
  border-bottom:1px solid #ddd;
  margin:8px 10px 25px 10px;
  padding-bottom:5px
}
/** 表格布局 **/
.pagination-container {
  display: flex;
  justify-content: flex-end;
  margin-top: 20px;
  background-color: transparent !important;
}
/* 弹窗中的分页器 */
.el-dialog .pagination-container {
  position: static !important;
  margin: 10px 0 0 0;
  padding: 0 !important;
  .el-pagination {
    position: static;
  }
}
/* 移动端适配 */
@media (max-width: 768px) {
  .pagination-container {
    .el-pagination {
      > .el-pagination__jump {
        display: none !important;
      }
      > .el-pagination__sizes {
        display: none !important;
      }
    }
  }
}
/* tree border */
.tree-border {
  margin-top: 5px;
  border: 1px solid var(--el-border-color-light, #e5e6e7);
  background: var(--el-bg-color, #FFFFFF) none;
  border-radius:4px;
  width: 100%;
}
.el-table .fixed-width .el-button--small {
  padding-left: 0;
  padding-right: 0;
  width: inherit;
}
/** 表格更多操作下拉样式 */
.el-table .el-dropdown-link {
  cursor: pointer;
  color: #2C51D9;
  margin-left: 10px;
}
.el-table .el-dropdown, .el-icon-arrow-down {
  font-size: 12px;
}
.el-tree-node__content > .el-checkbox {
  margin-right: 8px;
}
.list-group-striped > .list-group-item {
  border-left: 0;
  border-right: 0;
  border-radius: 0;
  padding-left: 0;
  padding-right: 0;
}
.list-group {
  padding-left: 0px;
  list-style: none;
}
.list-group-item {
  border-bottom: 1px solid #e7eaec;
  border-top: 1px solid #e7eaec;
  margin-bottom: -1px;
  padding: 11px 0px;
  font-size: 13px;
}
.pull-right {
  float: right !important;
}
.el-card__header {
  padding: 14px 15px 7px !important;
  min-height: 40px;
}
.el-card__body {
  padding: 15px 20px 20px 20px !important;
}
.card-box {
  margin-bottom: 10px;
}
/* button color */
.el-button--cyan.is-active,
.el-button--cyan:active {
  background: #20B2AA;
  border-color: #20B2AA;
  color: #FFFFFF;
}
.el-button--cyan:focus,
.el-button--cyan:hover {
  background: #48D1CC;
  border-color: #48D1CC;
  color: #FFFFFF;
}
.el-button--cyan {
  background-color: #20B2AA;
  border-color: #20B2AA;
  color: #FFFFFF;
}
/* text color */
.text-navy {
  color: #1ab394;
}
.text-primary {
  color: inherit;
}
.text-success {
  color: #1c84c6;
}
.text-info {
  color: #23c6c8;
}
.text-warning {
  color: #f8ac59;
}
.text-danger {
  color: #ed5565;
}
.text-muted {
  color: #888888;
}
/* image */
.img-circle {
  border-radius: 50%;
}
.img-lg {
  width: 120px;
  height: 120px;
}
.avatar-upload-preview {
  position: absolute;
  top: 50%;
  transform: translate(50%, -50%);
  width: 200px;
  height: 200px;
  border-radius: 50%;
  box-shadow: 0 0 4px #ccc;
  overflow: hidden;
}
/* 拖拽列样式 */
.sortable-ghost{
  opacity: .8;
  color: #fff!important;
  background: #42b983!important;
}
/* 表格右侧工具栏样式 */
.top-right-btn {
  margin-left: auto;
}
/* 分割面板样式 */
.splitpanes.default-theme .splitpanes__pane {
  background-color: var(--splitpanes-default-bg) !important;
}
src/assets/styles/sidebar.scss
@@ -17,14 +17,14 @@
    height: 100vh;
    position: fixed;
    top: 0;
    padding-top: var(--topbar-height);
    left: 0;
    z-index: 1001;
    overflow: hidden;
    padding: 0;
    font-size: 0;
    background: var(--sidebar-bg);
    background: var(--surface-base);
    border-right: 1px solid var(--surface-border);
    box-shadow: var(--shadow-md);
    box-shadow: none;
    > * {
      position: relative;
@@ -68,7 +68,7 @@
      border: none !important;
      height: 100%;
      width: 100% !important;
      padding: 12px 0;
      padding: 8px 0;
      background: transparent !important;
    }
@@ -88,24 +88,26 @@
    .submenu-title-noDropdown,
    .el-sub-menu__title,
    .el-menu-item {
      width: calc(100% - 16px) !important;
      margin: 4px 8px !important;
      width: 100% !important;
      margin: 0 !important;
      height: 44px;
      line-height: 44px;
      border-radius: var(--radius-md);
      padding-left: 16px !important;
      padding-right: 36px !important; // 预留箭头位置
      border-radius: 0;
      padding-left: 18px !important;
      padding-right: 18px !important;
      box-sizing: border-box;
      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      color: var(--sidebar-text);
      color: var(--el-color-primary);
      background: transparent;
      border: none !important;
      position: relative;
      font-size: 14px;
      border-bottom: 1px dashed rgba(var(--el-color-primary-rgb), 0.35) !important;
      .svg-icon {
        margin-right: 12px;
        width: 18px;
        height: 18px;
        width: 16px;
        height: 16px;
        vertical-align: middle;
        flex-shrink: 0;
        transition: transform 0.3s ease;
@@ -115,11 +117,12 @@
    .el-sub-menu {
      &.is-opened {
        > .el-sub-menu__title {
          color: var(--menu-active-text) !important;
          color: var(--el-color-primary) !important;
          font-weight: 600;
          .el-sub-menu__icon-arrow {
            transform: rotate(180deg) !important;
            color: var(--menu-active-text) !important;
            color: var(--el-color-primary) !important;
          }
        }
      }
@@ -134,7 +137,7 @@
        font-size: 12px !important;
        display: inline-block !important;
        transition: transform 0.3s ease !important;
        color: var(--sidebar-text) !important;
        color: var(--el-color-primary) !important;
        z-index: 2;
      }
    }
@@ -142,37 +145,52 @@
    .submenu-title-noDropdown:hover,
    .el-sub-menu__title:hover,
    .el-menu-item:hover {
      background: var(--menu-hover) !important;
      color: var(--menu-active-text) !important;
      background-color: rgba(var(--el-color-primary-rgb), 0.06) !important;
      color: var(--el-color-primary) !important;
      .svg-icon {
        transform: scale(1.1);
        color: var(--el-color-primary) !important;
      }
      .el-sub-menu__icon-arrow {
        color: var(--el-color-primary) !important;
      }
    }
    .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);
      color: var(--el-color-primary) !important;
      background: rgba(var(--el-color-primary-rgb), 0.06) !important;
      box-shadow: none;
      font-weight: 600;
      .svg-icon {
        color: var(--menu-active-text) !important;
        color: var(--el-color-primary) !important;
      }
    }
    & .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .el-sub-menu .el-menu-item {
      height: 40px;
      line-height: 40px;
      margin: 2px 8px !important;
      padding-left: 44px !important; // 增加子菜单缩进
      border-radius: var(--radius-sm);
      font-size: 13px;
      height: 44px;
      line-height: 44px;
      margin: 0 !important;
      padding-left: 38px !important;
      border-radius: 0;
      font-size: 14px;
      background-color: transparent !important;
      border-bottom: 1px dashed rgba(var(--el-color-primary-rgb), 0.3) !important;
      &.is-active {
        background: var(--menu-active-bg) !important;
        background-color: rgba(var(--el-color-primary-rgb), 0.06) !important;
      }
      &:hover {
        background-color: rgba(var(--el-color-primary-rgb), 0.06) !important;
        color: var(--el-color-primary) !important;
        .svg-icon {
          color: var(--el-color-primary) !important;
        }
      }
    }
  }
@@ -189,6 +207,7 @@
        justify-content: center;
        margin: 4px 8px !important;
        width: calc(var(--sidebar-collapsed-width) - 16px) !important;
        border-bottom: none !important;
        .svg-icon {
          margin-right: 0;
@@ -218,6 +237,8 @@
    .sidebar-container {
      transition: transform 0.25s;
      width: var(--sidebar-width) !important;
      padding-top: 0;
      z-index: 1004;
    }
    &.hideSidebar {
@@ -247,40 +268,41 @@
  .nest-menu .el-sub-menu > .el-sub-menu__title,
  .el-menu-item {
    min-width: 0 !important;
    margin: 4px 10px;
    width: calc(100% - 20px);
    margin: 0;
    width: 100%;
    height: 44px;
    line-height: 44px;
    padding-left: 16px !important;
    padding-right: 16px !important;
    padding-left: 18px !important;
    padding-right: 18px !important;
    box-sizing: border-box;
    border-radius: var(--radius-md);
    color: var(--sidebar-text);
    border-radius: 0;
    color: var(--el-color-primary);
    background: transparent;
    transition: all 0.2s ease;
    border-bottom: 1px dashed rgba(var(--el-color-primary-rgb), 0.35);
    &:hover {
      background: var(--menu-hover) !important;
      color: #fff !important;
      background: rgba(var(--el-color-primary-rgb), 0.06) !important;
      color: var(--el-color-primary) !important;
    }
    &.is-active {
      background: var(--menu-active-bg) !important;
      color: var(--menu-active-text) !important;
      background: rgba(var(--el-color-primary-rgb), 0.06) !important;
      color: var(--el-color-primary) !important;
      font-weight: 600;
      box-shadow: var(--menu-active-glow);
      box-shadow: none;
    }
  }
  > .el-menu--popup {
    max-height: 100vh;
    overflow: hidden;
    padding: 8px;
    border-radius: var(--radius-lg);
    padding: 8px 0;
    border-radius: 0;
    border: 1px solid var(--surface-border);
    box-shadow: var(--shadow-menu);
    background: var(--sidebar-bg);
    backdrop-filter: blur(16px);
    box-shadow: var(--shadow-sm);
    background: var(--surface-base);
    backdrop-filter: none;
    > .el-menu {
      max-height: calc(100vh - 16px);
src/assets/styles/variables.module.scss
@@ -73,20 +73,20 @@
  --content-radius: 10px;
  --layout-header-z: 20;
  --el-color-primary: #374d77;
  --el-color-primary-rgb: 37, 89, 163;
  --el-color-primary: #008c8c;
  --el-color-primary-rgb: 0, 140, 140;
  --el-color-success: #14b8a6;
  --el-color-warning: #f59e0b;
  --el-color-danger: #ef4444;
  --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);
  --sidebar-bg: #304156;
  --sidebar-text: #bfcbd9;
  --sidebar-muted: #8b9bb4;
  --menu-hover: #263445;
  --menu-active-bg: transparent;
  --menu-active-text: #409eff;
  --menu-surface: #304156;
  --menu-active-glow: none;
  --app-bg: #f8fafc;
  --app-bg-accent: #f1f5f9;
@@ -122,7 +122,7 @@
  --tags-item-hover: #f1f5f9;
  --tags-close-hover: rgba(239, 68, 68, 0.1);
  --accent-primary: #374d77;
  --accent-primary: #008c8c;
  --accent-light: #3b82f6;
  --accent-lighter: #60a5fa;
src/components/TopNav/index.vue
@@ -1,217 +1,216 @@
<template>
  <el-menu
    :default-active="activeMenu"
    mode="horizontal"
    @select="handleSelect"
    :ellipsis="false"
  >
    <template v-for="(item, index) in topMenus">
      <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
        <svg-icon
        v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
        :icon-class="item.meta.icon"/>
        {{ item.meta.title }}
      </el-menu-item>
    </template>
    <!-- 顶部菜单超出数量折叠 -->
    <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
      <template #title>更多菜单</template>
      <template v-for="(item, index) in topMenus">
        <el-menu-item
          :index="item.path"
          :key="index"
          v-if="index >= visibleNumber">
        <svg-icon
          v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
          :icon-class="item.meta.icon"/>
        {{ item.meta.title }}
        </el-menu-item>
      </template>
    </el-sub-menu>
  </el-menu>
</template>
<script setup>
import { constantRoutes } from "@/router"
import { isHttp } from '@/utils/validate'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
// 顶部栏初始数
const visibleNumber = ref(null)
// 当前激活菜单的 index
const currentIndex = ref(null)
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile']
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const route = useRoute()
const router = useRouter()
// 主题颜色
const theme = computed(() => settingsStore.theme)
// 所有的路由信息
const routers = computed(() => permissionStore.topbarRouters)
// 顶部显示菜单
const topMenus = computed(() => {
  let topMenus = []
  routers.value.map((menu) => {
    if (menu.hidden !== true) {
      // 兼容顶部栏一级菜单内部跳转
      if (menu.path === '/' && menu.children) {
          topMenus.push(menu.children[0])
      } else {
          topMenus.push(menu)
      }
    }
  })
  return topMenus
})
// 设置子路由
const childrenMenus = computed(() => {
  let childrenMenus = []
  routers.value.map((router) => {
    for (let item in router.children) {
      if (router.children[item].parentPath === undefined) {
        if(router.path === "/") {
          router.children[item].path = "/" + router.children[item].path
        } else {
          if(!isHttp(router.children[item].path)) {
            router.children[item].path = router.path + "/" + router.children[item].path
          }
        }
        router.children[item].parentPath = router.path
      }
      childrenMenus.push(router.children[item])
    }
  })
  return constantRoutes.concat(childrenMenus)
})
// 默认激活的菜单
const activeMenu = computed(() => {
  const path = route.path
  let activePath = path
  if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
    const tmpPath = path.substring(1, path.length)
    if (!route.meta.link) {
      activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
      appStore.toggleSideBarHide(false)
    }
  } else if(!route.children) {
    activePath = path
    appStore.toggleSideBarHide(true)
  }
  activeRoutes(activePath)
  return activePath
})
function setVisibleNumber() {
  const width = document.body.getBoundingClientRect().width / 3
  visibleNumber.value = parseInt(width / 85)
}
function handleSelect(key, keyPath) {
  currentIndex.value = key
  const route = routers.value.find(item => item.path === key)
  if (isHttp(key)) {
    // http(s):// 路径新窗口打开
    window.open(key, "_blank")
  } else if (!route || !route.children) {
    // 没有子路由路径内部打开
    const routeMenu = childrenMenus.value.find(item => item.path === key)
    if (routeMenu && routeMenu.query) {
      let query = JSON.parse(routeMenu.query)
      router.push({ path: key, query: query })
    } else {
      router.push({ path: key })
    }
    appStore.toggleSideBarHide(true)
  } else {
    // 显示左侧联动菜单
    activeRoutes(key)
    appStore.toggleSideBarHide(false)
  }
}
function activeRoutes(key) {
  let routes = []
  if (childrenMenus.value && childrenMenus.value.length > 0) {
    childrenMenus.value.map((item) => {
      if (key == item.parentPath || (key == "index" && "" == item.path)) {
        routes.push(item)
      }
    })
  }
  if(routes.length > 0) {
    permissionStore.setSidebarRouters(routes)
  } else {
    appStore.toggleSideBarHide(true)
  }
  return routes
}
onMounted(() => {
  window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
  window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
  setVisibleNumber()
})
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
  float: left;
  height: 50px !important;
  line-height: 50px !important;
  color: #999093 !important;
  padding: 0 5px !important;
  margin: 0 10px !important;
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
  border-bottom: 2px solid #{'var(--theme)'} !important;
  color: #303133;
}
/* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
  float: left;
  height: 50px !important;
  line-height: 50px !important;
  color: #999093 !important;
  padding: 0 5px !important;
  margin: 0 10px !important;
}
/* 背景色隐藏 */
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
  background-color: #ffffff;
}
/* 图标右间距 */
.topmenu-container .svg-icon {
  margin-right: 4px;
}
/* topmenu more arrow */
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
  position: static;
  vertical-align: middle;
  margin-left: 8px;
  margin-top: 0px;
}
</style>
<template>
  <div class="top-nav">
    <button v-show="showArrows"
            class="nav-arrow nav-arrow--left"
            type="button"
            :disabled="!canScrollLeft"
            @click="scrollLeft">
      <el-icon :size="18">
        <ArrowLeft />
      </el-icon>
    </button>
    <div ref="scrollWrapRef"
         class="top-nav__scroll"
         @scroll.passive="updateScrollState">
      <el-menu class="top-nav-menu"
               :default-active="activeMenu"
               mode="horizontal"
               :unique-opened="true"
               :ellipsis="false"
               :active-text-color="theme">
        <sidebar-item v-for="(routeItem, index) in topbarRouters"
                      :key="routeItem.path + index"
                      :item="routeItem"
                  :base-path="routeItem.path" />
      </el-menu>
    </div>
    <button v-show="showArrows"
            class="nav-arrow nav-arrow--right"
            type="button"
            :disabled="!canScrollRight"
            @click="scrollRight">
      <el-icon :size="18">
        <ArrowRight />
      </el-icon>
    </button>
  </div>
</template>
<script setup>
  import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
  import useSettingsStore from "@/store/modules/settings";
  import usePermissionStore from "@/store/modules/permission";
  import SidebarItem from "@/layout/components/Sidebar/SidebarItem.vue";
  const settingsStore = useSettingsStore();
  const permissionStore = usePermissionStore();
  const route = useRoute();
  // 主题颜色
  const theme = computed(() => settingsStore.theme);
  const topbarRouters = computed(() => permissionStore.topbarRouters);
  const scrollWrapRef = ref(null);
  const canScrollLeft = ref(false);
  const canScrollRight = ref(false);
  const showArrows = computed(() => canScrollLeft.value || canScrollRight.value);
  // 默认激活的菜单
  const activeMenu = computed(() => {
    const { meta, path } = route;
    if (meta?.activeMenu) return meta.activeMenu;
    return path;
  });
  function updateScrollState() {
    const el = scrollWrapRef.value;
    if (!el) return;
    const maxScrollLeft = el.scrollWidth - el.clientWidth;
    canScrollLeft.value = el.scrollLeft > 0;
    canScrollRight.value = el.scrollLeft < maxScrollLeft - 1;
  }
  function scrollByStep(direction) {
    const el = scrollWrapRef.value;
    if (!el) return;
    const step = Math.max(240, Math.floor(el.clientWidth * 0.6));
    el.scrollBy({ left: direction * step, behavior: "smooth" });
    requestAnimationFrame(() => updateScrollState());
  }
  function scrollLeft() {
    scrollByStep(-1);
  }
  function scrollRight() {
    scrollByStep(1);
  }
  let resizeRaf = 0;
  function handleResize() {
    if (resizeRaf) cancelAnimationFrame(resizeRaf);
    resizeRaf = requestAnimationFrame(() => {
      updateScrollState();
    });
  }
  onMounted(() => {
    updateScrollState();
    window.addEventListener("resize", handleResize, { passive: true });
  });
  onBeforeUnmount(() => {
    window.removeEventListener("resize", handleResize);
    if (resizeRaf) cancelAnimationFrame(resizeRaf);
  });
</script>
<style lang="scss" scoped>
  .top-nav {
    position: relative;
    height: var(--topbar-height);
    display: flex;
    align-items: center;
    width: 100%;
    min-width: 0;
  }
  .top-nav__scroll {
    flex: 1;
    min-width: 0;
    height: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    &::-webkit-scrollbar {
      display: none;
    }
  }
  .nav-arrow {
    width: 34px;
    height: 34px;
    flex: 0 0 34px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    border: 1px solid var(--surface-border);
    background: var(--surface-base);
    color: var(--text-secondary);
    cursor: pointer;
    transition: 0.2s ease;
    margin: 0 8px;
    &:hover:not(:disabled) {
      color: var(--el-color-primary);
      border-color: rgba(var(--el-color-primary-rgb), 0.35);
      background: rgba(var(--el-color-primary-rgb), 0.06);
    }
    &:disabled {
      opacity: 0.45;
      cursor: not-allowed;
    }
  }
  .top-nav-menu {
    width: max-content;
    min-width: 100%;
    height: 100%;
    border-bottom: none;
    background: transparent;
  }
  :deep(.top-nav-menu.el-menu--horizontal) {
    display: flex;
    align-items: stretch;
    flex-wrap: nowrap;
    height: 100%;
    width: max-content;
    min-width: 100%;
  }
  :deep(.top-nav-menu.el-menu--horizontal > .el-menu-item),
  :deep(.top-nav-menu.el-menu--horizontal > .el-sub-menu > .el-sub-menu__title) {
    height: var(--topbar-height);
    line-height: var(--topbar-height);
    padding: 0 12px;
    color: var(--text-secondary);
    border-bottom: 2px solid transparent;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
  }
  :deep(
      .top-nav-menu.el-menu--horizontal > .el-menu-item:not(.is-disabled):hover
    ),
  :deep(
      .top-nav-menu.el-menu--horizontal > .el-sub-menu > .el-sub-menu__title:hover
    ) {
    background: rgba(var(--el-color-primary-rgb), 0.06);
    color: var(--el-color-primary);
  }
  :deep(.top-nav-menu.el-menu--horizontal > .el-menu-item.is-active),
  :deep(
      .top-nav-menu.el-menu--horizontal
        > .el-sub-menu.is-active
        > .el-sub-menu__title
    ) {
    background: rgba(var(--el-color-primary-rgb), 0.06);
    color: var(--el-color-primary);
    font-weight: 600;
    border-bottom-color: var(--el-color-primary);
  }
  :deep(.top-nav-menu.el-menu--horizontal > .el-menu-item .svg-icon),
  :deep(
      .top-nav-menu.el-menu--horizontal
        > .el-sub-menu
        > .el-sub-menu__title
        .svg-icon
    ) {
    margin-right: 8px;
  }
</style>
src/layout/components/AppMain.vue
@@ -49,7 +49,7 @@
  width: 100%;
  height: 100%;
  padding: var(--content-gap);
  padding-top: 0;
  /* padding-top: 0; */
}
.fixed-header + .app-main {
src/layout/components/Navbar.vue
@@ -1,29 +1,33 @@
<template>
  <div class="navbar">
    <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 class="navbar-left">
      <div class="navbar-logo"
           v-if="appStore.device !== 'mobile'"
           :style="{ width: logoWidth }">
        <logo v-if="settingsStore.sidebarLogo"
              :collapse="logoCollapse" />
      </div>
      <div v-if="settingsStore.topNav && appStore.device !== 'mobile'"
           class="top-nav-wrapper">
        <top-nav />
      </div>
      <div v-else
           class="left-menu-spacer"></div>
    </div>
    <div class="right-menu">
      <div class="search-wrapper">
        <el-icon class="search-icon"
                 @click="openHeaderSearch">
          <Search />
        </el-icon>
        <el-input v-model="topSearchKeyword"
                  placeholder="快速搜索..."
                  clearable
                  @keyup.enter="openHeaderSearch" />
      <div class="action-icons">
        <!-- 搜索图标 -->
        <div class="right-menu-item hover-effect action-icon-btn"
             @click="openHeaderSearch">
          <el-icon :size="18">
            <Search />
          </el-icon>
        </div>
        <header-search ref="headerSearchRef"
                       :keyword="topSearchKeyword"
                       class="search-popup-trigger" />
      </div>
      <div class="action-icons">
                       class="search-popup-trigger"
                       style="display: none;" />
        <!-- 通知图标 -->
        <el-popover v-model:visible="notificationVisible"
                    :width="500"
                    placement="bottom-end"
@@ -31,7 +35,7 @@
                    :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] }"
                    popper-class="notification-popover">
          <template #reference>
            <div class="notification-container right-menu-item hover-effect">
            <div class="notification-container right-menu-item hover-effect action-icon-btn">
              <el-badge :value="unreadCount"
                        :hidden="unreadCount === 0"
                        class="notification-badge">
@@ -44,7 +48,8 @@
          <NotificationCenter @unreadCountChange="handleUnreadCountChange"
                              ref="notificationCenterRef" />
        </el-popover>
        <div class="right-menu-item hover-effect screenfull-container">
        <!-- 全屏图标 -->
        <div class="right-menu-item hover-effect screenfull-container action-icon-btn">
          <screenfull />
        </div>
      </div>
@@ -104,13 +109,21 @@
</template>
<script setup>
  import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
  import { ElMessageBox } from "element-plus";
  import { Bell, Search } from "@element-plus/icons-vue";
  import Breadcrumb from "@/components/Breadcrumb";
  import Hamburger from "@/components/Hamburger";
  import {
    Bell,
    Search,
    User,
    Setting,
    SwitchButton,
    CaretBottom,
  } from "@element-plus/icons-vue";
  import Screenfull from "@/components/Screenfull";
  import HeaderSearch from "@/components/HeaderSearch";
  import NotificationCenter from "./NotificationCenter/index.vue";
  import Logo from "./Sidebar/Logo.vue";
  import TopNav from "@/components/TopNav/index.vue";
  import useAppStore from "@/store/modules/app";
  import useUserStore from "@/store/modules/user";
  import useSettingsStore from "@/store/modules/settings";
@@ -119,15 +132,27 @@
  const userStore = useUserStore();
  const settingsStore = useSettingsStore();
  const isTopNavLayout = computed(
    () => settingsStore.topNav && appStore.device !== "mobile"
  );
  const logoWidth = computed(() => {
    if (isTopNavLayout.value) {
      return "var(--sidebar-width)";
    }
    return appStore.sidebar.opened
      ? "var(--sidebar-width)"
      : "var(--sidebar-collapsed-width)";
  });
  const logoCollapse = computed(() => {
    if (isTopNavLayout.value) return false;
    return !appStore.sidebar.opened;
  });
  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);
@@ -205,134 +230,91 @@
    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);
    padding: 0 24px 0 0;
    background: #fff;
    border-bottom: 1px solid #d8dce5;
    box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
    z-index: var(--layout-header-z);
  }
  .left-menu {
  .navbar-left {
    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 {
    height: 100%;
    flex: 1;
    min-width: 0;
  }
    :deep(.el-breadcrumb__inner) {
      color: var(--navbar-text) !important;
      opacity: 0.85;
  .navbar-logo {
    height: 100%;
    display: flex;
    align-items: center;
    transition: width 0.25s ease;
    overflow: hidden;
    flex-shrink: 0;
    background-color: #fff; // 强制设为白色背景
    // border-right: 1px solid #d8dce5; // 增加右侧边框以区分导航栏主体
  }
      &:hover {
        color: #fff !important;
        opacity: 1;
      }
  .top-nav-wrapper {
    display: flex;
    align-items: center;
    height: 100%;
    min-width: 0;
    flex: 1;
    padding: 0 12px;
    overflow: hidden;
  }
      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;
    }
  .left-menu-spacer {
    flex: 1;
    min-width: 0;
  }
  .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);
        }
      }
    }
    gap: 16px; // 调整组之间的间距
    .action-icons {
      display: flex;
      align-items: center;
      gap: 4px;
      gap: 12px; // 图标之间的间距
      padding-right: 16px;
      border-right: 1px solid var(--surface-border); // 增加垂直分割线
      border-right: 1px solid var(--surface-border);
      .right-menu-item {
        padding: 0 8px;
        height: 34px;
      .action-icon-btn {
        width: 36px;
        height: 36px;
        padding: 0;
        display: flex;
        align-items: center;
        color: var(--navbar-text);
        border-radius: var(--radius-sm);
        justify-content: center;
        color: var(--text-secondary);
        border-radius: 50%; // 圆形背景
        background: rgba(0, 0, 0, 0.04); // 浅浅的圆形框底色
        transition: all 0.2s ease;
        cursor: pointer;
        &:hover {
          background: var(--navbar-hover);
          color: var(--menu-active-text);
          background: rgba(0, 0, 0, 0.08); // 悬停加深
          color: var(--el-color-primary);
        }
        :deep(.svg-icon) {
          width: 18px !important;
          height: 18px !important;
        }
        :deep(.el-icon) {
          font-size: 18px !important;
        }
      }
      .notification-container {
        display: flex;
        align-items: center;
        justify-content: center;
      }
    }
@@ -348,14 +330,12 @@
        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);
        border: 1px solid transparent;
        background: transparent;
        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);
          background: rgba(0, 0, 0, 0.05);
        }
      }
@@ -368,8 +348,8 @@
      .user-name {
        font-size: 13px;
        font-weight: 700;
        color: var(--navbar-text);
        font-weight: 500;
        color: var(--text-primary);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
@@ -386,8 +366,7 @@
        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);
        border: 1px solid rgba(0, 0, 0, 0.1);
        object-fit: cover;
        transition: 0.2s ease;
      }
@@ -403,12 +382,12 @@
        width: 9px;
        height: 9px;
        background: #10b981;
        border: 2px solid var(--navbar-bg);
        border: 2px solid #fff;
        border-radius: 999px;
      }
      .caret-icon {
        color: var(--navbar-text);
        color: var(--text-secondary);
        opacity: 0.76;
        font-size: 12px;
        transition: 0.2s ease;
src/layout/components/Settings/index.vue
@@ -153,7 +153,7 @@
  const sideTheme = ref(settingsStore.sideTheme);
  const storeSettings = computed(() => settingsStore);
  const predefineColors = ref([
    "#374D77",
    "#008C8C",
    "#81D8D0",
    "#E85827",
    "#008C8C",
src/layout/components/Sidebar/Logo.vue
@@ -1,145 +1,163 @@
<template>
  <div class="sidebar-logo-container" :class="{ collapse }">
  <div class="sidebar-logo-container"
       :class="{ collapse }">
    <transition name="sidebarLogoFade">
      <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 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="/">
        <img v-if="logoUrl" :src="logoUrl" class="sidebar-logo" @error="handleImageError" alt="公司Logo" />
        <h1 v-if="!logoUrl" class="sidebar-title">{{ title }}</h1>
      <router-link v-else
                   key="expand"
                   class="sidebar-logo-link"
                   :style="expandLogoLinkStyle"
                   to="/">
        <img v-if="logoUrl"
             :src="logoUrl"
             class="sidebar-logo"
             @error="handleImageError"
             alt="公司Logo" />
        <h1 v-if="!logoUrl"
            class="sidebar-title">{{ title }}</h1>
      </router-link>
    </transition>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import useUserStore from '@/store/modules/user'
import defaultLogo from '@/assets/logo/logo.png'
  import { ref, computed, onMounted, watch } from "vue";
  import useUserStore from "@/store/modules/user";
  import defaultLogo from "@/assets/logo/芯导软件(江苏)有限公司.png";
defineProps({
  collapse: {
    type: Boolean,
    required: true
  }
})
  defineProps({
    collapse: {
      type: Boolean,
      required: true,
    },
  });
const title = import.meta.env.VITE_APP_TITLE
const userStore = useUserStore()
const baseUrl = import.meta.env.BASE_URL || '/'
const faviconUrl = `${baseUrl.replace(/\/?$/, '/') }favicon.ico`.replace(/([^:]\/)\/+/g, '$1')
  const title = import.meta.env.VITE_APP_TITLE;
  const userStore = useUserStore();
  const baseUrl = import.meta.env.BASE_URL || "/";
  const faviconUrl = `${baseUrl.replace(/\/?$/, "/")}favicon.ico`.replace(
    /([^:]\/)\/+/g,
    "$1"
  );
const cleanFactoryName = computed(() => {
  if (!userStore.currentFactoryName) return ''
  return userStore.currentFactoryName.trim()
})
  const cleanFactoryName = computed(() => {
    if (!userStore.currentFactoryName) return "";
    return userStore.currentFactoryName.trim();
  });
const logoUrl = ref('')
  const logoUrl = ref("");
const expandLogoLinkStyle = computed(() => {
  if (!logoUrl.value) {
    return { '--logo-bg-image': 'none' }
  }
  const escaped = String(logoUrl.value).replace(/"/g, '\\"')
  return { '--logo-bg-image': `url("${escaped}")` }
})
const updateLogoUrl = () => {
  if (!cleanFactoryName.value) {
    logoUrl.value = defaultLogo
    return
  }
  try {
    const dynamicLogo = import.meta.glob('/src/assets/logo/*.png', { eager: true })
    const logoPath = `/src/assets/logo/${cleanFactoryName.value}.png`
    if (dynamicLogo[logoPath]) {
      logoUrl.value = dynamicLogo[logoPath].default
    } else {
      logoUrl.value = defaultLogo
  const expandLogoLinkStyle = computed(() => {
    if (!logoUrl.value) {
      return { "--logo-bg-image": "none" };
    }
  } catch (error) {
    console.error('加载工厂 Logo 失败:', error)
    logoUrl.value = defaultLogo
  }
}
    const escaped = String(logoUrl.value).replace(/"/g, '\\"');
    return { "--logo-bg-image": `url("${escaped}")` };
  });
onMounted(() => {
  updateLogoUrl()
  watch(() => userStore.currentFactoryName, updateLogoUrl)
})
  const updateLogoUrl = () => {
    if (!cleanFactoryName.value) {
      logoUrl.value = defaultLogo;
      return;
    }
const handleImageError = () => {
  logoUrl.value = defaultLogo
}
    try {
      const dynamicLogo = import.meta.glob("/src/assets/logo/*.png", {
        eager: true,
      });
      const logoPath = `/src/assets/logo/${cleanFactoryName.value}.png`;
      if (dynamicLogo[logoPath]) {
        logoUrl.value = dynamicLogo[logoPath].default;
      } else {
        logoUrl.value = defaultLogo;
      }
    } catch (error) {
      console.error("加载工厂 Logo 失败:", error);
      logoUrl.value = defaultLogo;
    }
  };
  onMounted(() => {
    updateLogoUrl();
    watch(() => userStore.currentFactoryName, updateLogoUrl);
  });
  const handleImageError = () => {
    logoUrl.value = defaultLogo;
  };
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables.module.scss';
  @import "@/assets/styles/variables.module.scss";
.sidebarLogoFade-enter-active {
  transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
  opacity: 0;
}
.sidebar-logo-container {
  position: relative;
  width: 100% !important;
  height: 64px !important;
  line-height: 64px;
  background: transparent;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  text-align: left;
  overflow: hidden;
  transition: all 0.3s ease;
  .sidebar-logo-link {
    height: 100%;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: flex-start; // 默认展开时靠左
    padding: 0 16px;
  .sidebarLogoFade-enter-active {
    transition: opacity 1.5s;
  }
  .sidebar-logo {
   height:100%;
   width:100%;
    width: auto;
    max-width: 100%;
    vertical-align: middle;
    object-fit: contain;
    display: block;
  .sidebarLogoFade-enter,
  .sidebarLogoFade-leave-to {
    opacity: 0;
  }
  .sidebar-title {
    display: inline-block;
    margin: 0;
    color: #fff;
    font-weight: 600;
    line-height: 64px;
    font-size: 16px;
    vertical-align: middle;
    margin-left: 12px;
    white-space: nowrap;
  }
  .sidebar-logo-container {
    position: relative;
    width: 100% !important;
    height: 100% !important;
    line-height: var(--topbar-height, 64px);
    background: transparent;
    text-align: left;
    overflow: hidden;
    transition: all 0.3s ease;
  &.collapse {
    .sidebar-logo-link {
      padding: 0;
      justify-content: center; // 折叠时绝对居中
      height: 100%;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center; // 始终居中
      padding: 0; // 移除两边间距,让图片能撑满
    }
    .sidebar-logo {
      width: 32px;
      height: 32px;
      width: 100%; // 宽度始终和侧边栏一致
      height: auto; // 高度自适应
      vertical-align: middle;
      object-fit: contain;
      display: block;
    }
    .sidebar-title {
      display: inline-block;
      margin: 0;
      color: #333; // 白色背景下需要深色字体
      font-weight: 600;
      line-height: var(--topbar-height, 64px);
      font-size: 16px;
      vertical-align: middle;
      margin-left: 12px;
      white-space: nowrap;
    }
    &.collapse {
      .sidebar-logo-link {
        padding: 0;
        justify-content: center; // 折叠时绝对居中
      }
      .sidebar-logo {
        width: 32px;
        height: 32px;
        margin: 0;
      }
    }
  }
}
</style>
src/layout/components/Sidebar/SidebarItem.vue
@@ -85,6 +85,13 @@
  if (isExternal(props.basePath)) {
    return props.basePath
  }
  if (String(routePath || '').startsWith('/')) {
    if (routeQuery) {
      let query = JSON.parse(routeQuery)
      return { path: getNormalPath(routePath), query: query }
    }
    return getNormalPath(routePath)
  }
  if (routeQuery) {
    let query = JSON.parse(routeQuery)
    return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
src/layout/components/Sidebar/index.vue
@@ -1,101 +1,100 @@
<template>
  <div :class="{ 'has-logo': showLogo }" class="sidebar-container">
    <logo v-if="showLogo" :collapse="isCollapse" />
  <div :class="{ 'has-logo': showLogo && appStore.device === 'mobile' }"
       class="sidebar-container">
    <logo v-if="showLogo && appStore.device === 'mobile'"
          :collapse="false" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="getMenuBackground"
        :text-color="getMenuTextColor"
        :unique-opened="true"
        :active-text-color="theme"
        :collapse-transition="false"
        mode="vertical"
        :class="sideTheme"
      >
        <sidebar-item
          v-for="(route, index) in sidebarRouters"
          :key="route.path + index"
          :item="route"
          :base-path="route.path"
        />
      <el-menu :default-active="activeMenu"
               :collapse="isCollapse"
               :background-color="getMenuBackground"
               :text-color="getMenuTextColor"
               :unique-opened="true"
               :active-text-color="theme"
               :collapse-transition="false"
               mode="vertical"
               :class="sideTheme">
        <sidebar-item v-for="(route, index) in sidebarRouters"
                      :key="route.path + index"
                      :item="route"
                      :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>
<script setup>
import Logo from "./Logo";
import SidebarItem from "./SidebarItem";
import useAppStore from "@/store/modules/app";
import useSettingsStore from "@/store/modules/settings";
import usePermissionStore from "@/store/modules/permission";
  import Logo from "./Logo";
  import SidebarItem from "./SidebarItem";
  import useAppStore from "@/store/modules/app";
  import useSettingsStore from "@/store/modules/settings";
  import usePermissionStore from "@/store/modules/permission";
const route = useRoute();
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
  const route = useRoute();
  const appStore = useAppStore();
  const settingsStore = useSettingsStore();
  const permissionStore = usePermissionStore();
const sidebarRouters = computed(() => permissionStore.sidebarRouters);
const showLogo = computed(() => settingsStore.sidebarLogo);
const sideTheme = computed(() => settingsStore.sideTheme);
const theme = computed(() => settingsStore.theme);
const isCollapse = computed(() => !appStore.sidebar.opened);
  const sidebarRouters = computed(() => permissionStore.sidebarRouters);
  const showLogo = computed(() => settingsStore.sidebarLogo);
  const sideTheme = computed(() => settingsStore.sideTheme);
  const theme = computed(() => settingsStore.theme);
  const isCollapse = computed(() => !appStore.sidebar.opened);
const getMenuBackground = computed(() => "var(--sidebar-bg)");
  const getMenuBackground = computed(() => "var(--sidebar-bg)");
const getMenuTextColor = computed(() => "var(--sidebar-text)");
  const getMenuTextColor = computed(() => "var(--sidebar-text)");
const activeMenu = computed(() => {
  const { meta, path } = route;
  if (meta.activeMenu) return meta.activeMenu;
  return path;
});
  const activeMenu = computed(() => {
    const { meta, path } = route;
    if (meta.activeMenu) return meta.activeMenu;
    return path;
  });
</script>
<style lang="scss" scoped>
.sidebar-container {
  background: transparent;
  border-radius: 0;
  overflow: hidden;
  .scrollbar-wrapper {
  .sidebar-container {
    background: transparent;
  }
    border-radius: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  .el-menu {
    border: none !important;
    height: 100%;
    width: 100% !important;
    background: transparent !important;
    :deep(.el-menu-item),
    :deep(.el-sub-menu__title) {
      margin-bottom: 4px;
      border-radius: var(--radius-md);
      color: var(--sidebar-text);
      font-size: 14px;
      transition: all 0.2s ease;
      display: flex;
      align-items: center;
      &:hover {
        background: var(--menu-hover) !important;
        color: #fff !important;
      }
    .scrollbar-wrapper {
      background: transparent;
    }
    :deep(.el-menu-item.is-active) {
      background: var(--menu-active-bg) !important;
      color: var(--menu-active-text) !important;
      font-weight: 600;
      box-shadow: var(--menu-active-glow);
    .el-menu {
      border: none !important;
      height: 100%;
      width: 100% !important;
      background: transparent !important;
      .svg-icon {
      :deep(.el-menu-item),
      :deep(.el-sub-menu__title) {
        margin-bottom: 4px;
        border-radius: var(--radius-md);
        color: var(--sidebar-text);
        font-size: 14px;
        transition: all 0.2s ease;
        display: flex;
        align-items: center;
        &:hover {
          background: var(--menu-hover) !important;
          color: #fff !important;
        }
      }
      :deep(.el-menu-item.is-active) {
        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;
        }
      }
    }
  }
}
</style>
src/layout/components/TagsView/index.vue
@@ -1,411 +1,462 @@
<template>
  <div id="tags-view-container" class="tags-view-container">
    <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
      <router-link
        v-for="tag in visitedViews"
        :key="tag.path"
        :data-path="tag.path"
        :class="isActive(tag) ? 'active' : ''"
        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
        class="tags-view-item"
        :style="activeStyle(tag)"
        @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
        @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" />
  <div id="tags-view-container"
       class="tags-view-container">
    <scroll-pane ref="scrollPaneRef"
                 class="tags-view-wrapper"
                 @scroll="handleScroll">
      <router-link v-for="tag in visitedViews"
                   :key="tag.path"
                   :data-path="tag.path"
                   :class="isActive(tag) ? 'active' : ''"
                   :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
                   class="tags-view-item"
                   :style="activeStyle(tag)"
                   @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
                   @contextmenu.prevent="openMenu(tag, $event)">
        <span class="tags-view-item-title">{{ tag.title }}</span>
        <span v-if="!isAffix(tag)"
              class="tags-view-close"
              @click.prevent.stop="closeSelectedTag(tag)">
          <el-icon class="el-icon-close">
            <Close />
          </el-icon>
        </span>
      </router-link>
    </scroll-pane>
    <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
    <ul v-show="visible"
        :style="{ left: left + 'px', top: top + 'px' }"
        class="contextmenu">
      <li @click="refreshSelectedTag(selectedTag)">
        <refresh-right style="width: 1em; height: 1em;" /> 刷新页面
        <el-icon>
          <RefreshRight />
        </el-icon> 刷新页面
      </li>
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
        <close style="width: 1em; height: 1em;" /> 关闭当前
      <li v-if="!isAffix(selectedTag)"
          @click="closeSelectedTag(selectedTag)">
        <el-icon>
          <Close />
        </el-icon> 关闭当前
      </li>
      <li @click="closeOthersTags">
        <circle-close style="width: 1em; height: 1em;" /> 关闭其他
        <el-icon>
          <CircleClose />
        </el-icon> 关闭其他
      </li>
      <li v-if="!isFirstView()" @click="closeLeftTags">
        <back style="width: 1em; height: 1em;" /> 关闭左侧
      <li v-if="!isFirstView()"
          @click="closeLeftTags">
        <el-icon>
          <Back />
        </el-icon> 关闭左侧
      </li>
      <li v-if="!isLastView()" @click="closeRightTags">
        <right style="width: 1em; height: 1em;" /> 关闭右侧
      <li v-if="!isLastView()"
          @click="closeRightTags">
        <el-icon>
          <Right />
        </el-icon> 关闭右侧
      </li>
      <li @click="closeAllTags(selectedTag)">
        <circle-close style="width: 1em; height: 1em;" /> 全部关闭
        <el-icon>
          <CircleClose />
        </el-icon> 全部关闭
      </li>
    </ul>
  </div>
</template>
<script setup>
import ScrollPane from './ScrollPane'
import { getNormalPath } from '@/utils/ruoyi'
import useTagsViewStore from '@/store/modules/tagsView'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
  import ScrollPane from "./ScrollPane";
  import { getNormalPath } from "@/utils/ruoyi";
  import useTagsViewStore from "@/store/modules/tagsView";
  import useSettingsStore from "@/store/modules/settings";
  import usePermissionStore from "@/store/modules/permission";
  import {
    Close,
    RefreshRight,
    CircleClose,
    Back,
    Right,
  } from "@element-plus/icons-vue";
const visible = ref(false)
const top = ref(0)
const left = ref(0)
const selectedTag = ref({})
const affixTags = ref([])
const scrollPaneRef = ref(null)
  const visible = ref(false);
  const top = ref(0);
  const left = ref(0);
  const selectedTag = ref({});
  const affixTags = ref([]);
  const scrollPaneRef = ref(null);
const { proxy } = getCurrentInstance()
const route = useRoute()
const router = useRouter()
  const { proxy } = getCurrentInstance();
  const route = useRoute();
  const router = useRouter();
const visitedViews = computed(() => useTagsViewStore().visitedViews)
const routes = computed(() => usePermissionStore().routes)
const theme = computed(() => useSettingsStore().theme)
  const visitedViews = computed(() => useTagsViewStore().visitedViews);
  const routes = computed(() => usePermissionStore().routes);
  const theme = computed(() => useSettingsStore().theme);
watch(route, () => {
  addTags()
  moveToCurrentTag()
})
  watch(route, () => {
    addTags();
    moveToCurrentTag();
  });
watch(visible, (value) => {
  if (value) {
    document.body.addEventListener('click', closeMenu)
  } else {
    document.body.removeEventListener('click', closeMenu)
  }
})
onMounted(() => {
  initTags()
  addTags()
})
function isActive(r) {
  return r.path === route.path
}
function activeStyle(tag) {
  if (!isActive(tag)) return {}
  return {
    "background-color": theme.value,
    "border-color": theme.value
  }
}
function isAffix(tag) {
  return tag.meta && tag.meta.affix
}
function isFirstView() {
  try {
    return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
  } catch (err) {
    return false
  }
}
function isLastView() {
  try {
    return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
  } catch (err) {
    return false
  }
}
function filterAffixTags(routes, basePath = '') {
  let tags = []
  routes.forEach(route => {
    if (route.meta && route.meta.affix) {
      const tagPath = getNormalPath(basePath + '/' + route.path)
      tags.push({
        fullPath: tagPath,
        path: tagPath,
        name: route.name,
        meta: { ...route.meta }
      })
  watch(visible, value => {
    if (value) {
      document.body.addEventListener("click", closeMenu);
    } else {
      document.body.removeEventListener("click", closeMenu);
    }
    if (route.children) {
      const tempTags = filterAffixTags(route.children, route.path)
      if (tempTags.length >= 1) {
        tags = [...tags, ...tempTags]
  });
  onMounted(() => {
    initTags();
    addTags();
  });
  function isActive(r) {
    return r.path === route.path;
  }
  function activeStyle(tag) {
    if (!isActive(tag)) return {};
    return {
      "background-color": theme.value,
      "border-color": theme.value,
    };
  }
  function isAffix(tag) {
    return tag.meta && tag.meta.affix;
  }
  function isFirstView() {
    try {
      return (
        selectedTag.value.fullPath === "/index" ||
        selectedTag.value.fullPath === visitedViews.value[1].fullPath
      );
    } catch (err) {
      return false;
    }
  }
  function isLastView() {
    try {
      return (
        selectedTag.value.fullPath ===
        visitedViews.value[visitedViews.value.length - 1].fullPath
      );
    } catch (err) {
      return false;
    }
  }
  function filterAffixTags(routes, basePath = "") {
    let tags = [];
    routes.forEach(route => {
      if (route.meta && route.meta.affix) {
        const tagPath = getNormalPath(basePath + "/" + route.path);
        tags.push({
          fullPath: tagPath,
          path: tagPath,
          name: route.name,
          meta: { ...route.meta },
        });
      }
    }
  })
  return tags
}
function initTags() {
  const res = filterAffixTags(routes.value)
  affixTags.value = res
  for (const tag of res) {
    // Must have tag name
    if (tag.name) {
       useTagsViewStore().addVisitedView(tag)
    }
  }
}
function addTags() {
  const { name } = route
  if (name) {
    useTagsViewStore().addView(route)
  }
}
function moveToCurrentTag() {
  nextTick(() => {
    for (const r of visitedViews.value) {
      if (r.path === route.path) {
        scrollPaneRef.value.moveToTarget(r)
        // when query is different then update
        if (r.fullPath !== route.fullPath) {
          useTagsViewStore().updateVisitedView(route)
      if (route.children) {
        const tempTags = filterAffixTags(route.children, route.path);
        if (tempTags.length >= 1) {
          tags = [...tags, ...tempTags];
        }
      }
    }
  })
}
function refreshSelectedTag(view) {
  proxy.$tab.refreshPage(view)
  if (route.meta.link) {
    useTagsViewStore().delIframeView(route)
    });
    return tags;
  }
}
function closeSelectedTag(view) {
  proxy.$tab.closePage(view).then(({ visitedViews }) => {
    if (isActive(view)) {
      toLastView(visitedViews, view)
  function initTags() {
    const res = filterAffixTags(routes.value);
    affixTags.value = res;
    for (const tag of res) {
      // Must have tag name
      if (tag.name) {
        useTagsViewStore().addVisitedView(tag);
      }
    }
  })
}
  }
function closeRightTags() {
  proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
    if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
      toLastView(visitedViews)
  function addTags() {
    const { name } = route;
    if (name) {
      useTagsViewStore().addView(route);
    }
  })
}
  }
function closeLeftTags() {
  proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
    if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
      toLastView(visitedViews)
  function moveToCurrentTag() {
    nextTick(() => {
      for (const r of visitedViews.value) {
        if (r.path === route.path) {
          scrollPaneRef.value.moveToTarget(r);
          // when query is different then update
          if (r.fullPath !== route.fullPath) {
            useTagsViewStore().updateVisitedView(route);
          }
        }
      }
    });
  }
  function refreshSelectedTag(view) {
    proxy.$tab.refreshPage(view);
    if (route.meta.link) {
      useTagsViewStore().delIframeView(route);
    }
  })
}
  }
function closeOthersTags() {
  router.push(selectedTag.value).catch(() => { })
  proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
    moveToCurrentTag()
  })
}
  function closeSelectedTag(view) {
    proxy.$tab.closePage(view).then(({ visitedViews }) => {
      if (isActive(view)) {
        toLastView(visitedViews, view);
      }
    });
  }
function closeAllTags(view) {
  proxy.$tab.closeAllPage().then(({ visitedViews }) => {
    if (affixTags.value.some(tag => tag.path === route.path)) {
      return
    }
    toLastView(visitedViews, view)
  })
}
  function closeRightTags() {
    proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
      if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
        toLastView(visitedViews);
      }
    });
  }
function toLastView(visitedViews, view) {
  const latestView = visitedViews.slice(-1)[0]
  if (latestView) {
    router.push(latestView.fullPath)
  } else {
    // now the default is to redirect to the home page if there is no tags-view,
    // you can adjust it according to your needs.
    if (view.name === 'Dashboard') {
      // to reload home page
      router.replace({ path: '/redirect' + view.fullPath })
  function closeLeftTags() {
    proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
      if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
        toLastView(visitedViews);
      }
    });
  }
  function closeOthersTags() {
    router.push(selectedTag.value).catch(() => {});
    proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
      moveToCurrentTag();
    });
  }
  function closeAllTags(view) {
    proxy.$tab.closeAllPage().then(({ visitedViews }) => {
      if (affixTags.value.some(tag => tag.path === route.path)) {
        return;
      }
      toLastView(visitedViews, view);
    });
  }
  function toLastView(visitedViews, view) {
    const latestView = visitedViews.slice(-1)[0];
    if (latestView) {
      router.push(latestView.fullPath);
    } else {
      router.push('/')
      // now the default is to redirect to the home page if there is no tags-view,
      // you can adjust it according to your needs.
      if (view.name === "Dashboard") {
        // to reload home page
        router.replace({ path: "/redirect" + view.fullPath });
      } else {
        router.push("/");
      }
    }
  }
}
function openMenu(tag, e) {
  const menuMinWidth = 105
  const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
  const offsetWidth = proxy.$el.offsetWidth // container width
  const maxLeft = offsetWidth - menuMinWidth // left boundary
  const l = e.clientX - offsetLeft + 15 // 15: margin right
  function openMenu(tag, e) {
    const menuMinWidth = 105;
    const offsetLeft = proxy.$el.getBoundingClientRect().left; // container margin left
    const offsetWidth = proxy.$el.offsetWidth; // container width
    const maxLeft = offsetWidth - menuMinWidth; // left boundary
    const l = e.clientX - offsetLeft + 15; // 15: margin right
  if (l > maxLeft) {
    left.value = maxLeft
  } else {
    left.value = l
    if (l > maxLeft) {
      left.value = maxLeft;
    } else {
      left.value = l;
    }
    top.value = e.clientY;
    visible.value = true;
    selectedTag.value = tag;
  }
  top.value = e.clientY
  visible.value = true
  selectedTag.value = tag
}
  function closeMenu() {
    visible.value = false;
  }
function closeMenu() {
  visible.value = false
}
function handleScroll() {
  closeMenu()
}
  function handleScroll() {
    closeMenu();
  }
</script>
<style lang="scss" scoped>
.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-container {
    height: 34px;
    width: 100%;
    background: #fff;
    border-bottom: 1px solid #d8dce5;
    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
    padding: 0;
    display: flex;
    align-items: center;
    position: relative;
    z-index: 10;
  .tags-view-wrapper {
    flex: 1;
    overflow: hidden;
    white-space: nowrap;
    .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;
      .tags-view-item {
        display: inline-flex;
        align-items: center;
        position: relative;
        cursor: pointer;
        height: 26px;
        line-height: 26px;
        color: #495060;
        background-color: #fff;
        border: 1px solid #d8dce5;
        border-radius: 2px;
        padding: 0 8px;
        font-size: 12px;
        margin-right: 5px;
        margin-top: 4px;
        margin-left: 5px;
        transition: all 0.3s ease;
        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%;
        .tags-view-item-title {
          margin-right: 2px;
          display: inline-block;
          vertical-align: middle;
        }
        &:hover {
          background-color: #f4f4f5;
        }
        &.active {
          background-color: var(--el-color-primary, #42b983);
          color: #fff;
          border-color: var(--el-color-primary, #42b983);
          box-shadow: none;
          font-weight: 400;
          &::before {
            content: "";
            background-color: #fff;
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 4px;
          }
          .tags-view-close {
            color: #fff;
            display: inline-block;
            vertical-align: middle;
            &:hover {
              background-color: rgba(255, 255, 255, 0.3);
            }
          }
        }
      }
    }
    .contextmenu {
      margin: 0;
      background: #fff;
      z-index: 3000;
      position: absolute;
      list-style-type: none;
      padding: 5px 0;
      border-radius: 4px;
      font-size: 12px;
      font-weight: 400;
      color: #333;
      box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
      li {
        margin: 0;
        padding: 7px 16px;
        cursor: pointer;
        display: flex;
        align-items: center;
        gap: 8px;
        transition: all 0.2s ease;
        white-space: nowrap;
        .el-icon {
          font-size: 14px;
        }
        &:hover {
          background: #eee;
        }
      }
    }
  }
  .contextmenu {
    margin: 0;
    background: #fff;
    z-index: 3000;
    position: absolute;
    list-style-type: none;
    padding: 4px 0;
    border-radius: var(--radius-md);
    font-size: 12px;
    color: var(--text-secondary);
    box-shadow: var(--shadow-md);
    border: 1px solid var(--surface-border);
    li {
      margin: 0;
      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: #f1f5f9;
        color: var(--accent-primary);
      }
    }
  }
}
</style>
<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;
  //reset element css of el-icon-close
  .tags-view-wrapper {
    .el-scrollbar__view {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 12px;
      height: 12px;
      line-height: 1;
      align-self: center;
      transform: translateY(1px);
      height: 34px;
    }
    .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);
    .tags-view-item {
      .tags-view-close {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 14px;
        height: 14px;
        line-height: 1;
        align-self: center;
        transform: translateY(0px);
        margin-left: 2px;
      }
      svg {
        display: block;
        width: 10px;
        height: 10px;
      }
      .el-icon-close {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 14px;
        height: 14px;
        line-height: 1;
        vertical-align: middle;
        border-radius: 50%;
        text-align: center;
        transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
        transform-origin: 100% 50%;
        align-self: center;
      &:hover {
        background-color: var(--tags-close-hover, #b4bccc);
        color: #fff;
        &:before {
          transform: scale(0.8);
          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
@@ -5,13 +5,28 @@
    <div v-if="device === 'mobile' && sidebar.opened"
         class="drawer-bg"
         @click="handleClickOutside" />
    <sidebar v-if="!sidebar.hide"
    <div :class="{ 'fixed-header': fixedHeader }"
         class="top-header">
      <navbar @setLayout="setLayout" />
    </div>
    <sidebar v-if="!sidebar.hide && (!settingsStore.topNav || device === 'mobile')"
             class="sidebar-container" />
    <div :class="{ hasTagsView: showTagsView, sidebarHide: sidebar.hide }"
         class="main-container main-layout">
      <div :class="{ 'fixed-header': fixedHeader, 'with-tags': showTagsView }">
        <navbar @setLayout="setLayout" />
      <div v-if="showTagsView"
           class="tags-view-wrapper"
           :class="{ 'fixed-tags-view': fixedHeader }">
        <tags-view />
      </div>
      <div class="breadcrumb-wrapper"
           :class="{ 'fixed-breadcrumb': fixedHeader }">
        <hamburger v-if="!sidebar.hide && (!settingsStore.topNav || device === 'mobile')"
                   id="hamburger-container"
                   :is-active="sidebar.opened"
                   class="hamburger-container"
                   @toggleClick="toggleSideBar" />
        <breadcrumb id="breadcrumb-container"
                    class="breadcrumb-container" />
      </div>
      <app-main />
      <settings ref="settingRef" />
@@ -25,6 +40,8 @@
  import { useRoute } from "vue-router";
  import Sidebar from "./components/Sidebar/index.vue";
  import { AppMain, Navbar, Settings, TagsView } from "./components";
  import Breadcrumb from "@/components/Breadcrumb";
  import Hamburger from "@/components/Hamburger";
  import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
  import defaultSettings from "@/settings";
@@ -34,17 +51,16 @@
  import useTagsViewStore from "@/store/modules/tagsView";
  const settingsStore = useSettingsStore();
  const appStore = useAppStore();
  const tagsViewStore = useTagsViewStore();
  const userStore = useUserStore();
  const route = useRoute();
  const theme = computed(() => settingsStore.theme);
  const sideTheme = computed(() => settingsStore.sideTheme);
  const sidebar = computed(() => useAppStore().sidebar);
  const device = computed(() => useAppStore().device);
  const sidebar = computed(() => appStore.sidebar);
  const device = computed(() => appStore.device);
  const needTagsView = computed(() => settingsStore.tagsView);
  const showTagsView = computed(
    () => needTagsView.value && tagsViewStore.visitedViews.length > 1
  );
  const showTagsView = computed(() => needTagsView.value);
  const fixedHeader = computed(() => settingsStore.fixedHeader);
  const aiEnabled = computed(() => Number(userStore.aiEnabled) === 1);
  const showGlobalAiChat = computed(() => {
@@ -68,22 +84,35 @@
    () => device.value,
    () => {
      if (device.value === "mobile" && sidebar.value.opened) {
        useAppStore().closeSideBar({ withoutAnimation: false });
        appStore.closeSideBar({ withoutAnimation: false });
      }
    }
  );
  watchEffect(() => {
    if (width.value - 1 < WIDTH) {
      useAppStore().toggleDevice("mobile");
      useAppStore().closeSideBar({ withoutAnimation: true });
      appStore.toggleDevice("mobile");
      appStore.closeSideBar({ withoutAnimation: true });
    } else {
      useAppStore().toggleDevice("desktop");
      appStore.toggleDevice("desktop");
    }
  });
  watchEffect(() => {
    if (settingsStore.topNav && device.value !== "mobile") {
      appStore.toggleSideBarHide(true);
      appStore.closeSideBar({ withoutAnimation: true });
    } else {
      appStore.toggleSideBarHide(false);
    }
  });
  function handleClickOutside() {
    useAppStore().closeSideBar({ withoutAnimation: false });
    appStore.closeSideBar({ withoutAnimation: false });
  }
  function toggleSideBar() {
    appStore.toggleSideBar();
  }
  const settingRef = ref(null);
@@ -113,8 +142,18 @@
    width: 100%;
    top: 0;
    height: 100%;
    position: absolute;
    z-index: 999;
    position: fixed;
    z-index: 1003;
  }
  .top-header {
    width: 100%;
    z-index: 1002;
  }
  .top-header.fixed-header {
    position: sticky;
    top: 0;
  }
  .main-layout {
@@ -123,26 +162,86 @@
    transition: margin-left 0.25s ease;
    display: flex;
    flex-direction: column;
    padding: 0;
  }
  .fixed-header {
    position: sticky;
    top: 0;
    z-index: var(--layout-header-z);
  .breadcrumb-wrapper {
    width: 100%;
    padding: 0;
    background: #fff;
    padding: 10px 24px;
    background: transparent;
    display: flex;
    flex-direction: column;
    gap: 2px; // 在 Navbar 和 TagsView 之间增加极小的空隙
    align-items: center;
    gap: 12px;
  }
  .fixed-header.with-tags {
    padding-bottom: 4px; // 底部留出一点距离,不要直接贴着主体内容
  .hamburger-container {
    height: 32px;
    width: 32px;
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary);
    cursor: pointer;
    transition: all 0.2s ease;
    flex-shrink: 0;
    &:hover {
      background: rgba(0, 0, 0, 0.05);
      color: var(--el-color-primary);
    }
  }
  .hideSidebar .fixed-header {
  .breadcrumb-container {
    min-width: 0;
    :deep(.el-breadcrumb__inner) {
      color: var(--text-secondary) !important;
      opacity: 0.85;
      &:hover {
        color: var(--el-color-primary) !important;
        opacity: 1;
      }
      a {
        color: inherit !important;
        font-weight: 500 !important;
      }
    }
    :deep(.no-redirect) {
      color: var(--text-primary) !important;
      font-weight: 600 !important;
      opacity: 1;
    }
    :deep(.el-breadcrumb__separator) {
      color: var(--text-tertiary);
      opacity: 0.5;
    }
  }
  .fixed-breadcrumb {
    position: sticky;
    top: var(--topbar-height);
    z-index: calc(var(--layout-header-z) - 1);
    background: var(--app-bg);
  }
  .hasTagsView {
    .fixed-breadcrumb {
      top: calc(var(--topbar-height) + var(--tagsbar-height));
    }
  }
  .tags-view-wrapper.fixed-tags-view {
    position: sticky;
    top: var(--topbar-height);
    z-index: calc(var(--layout-header-z) - 1);
    background: var(--app-bg);
  }
  .hideSidebar .breadcrumb-wrapper {
    width: 100%;
  }
@@ -150,9 +249,8 @@
    margin-left: var(--sidebar-collapsed-width);
  }
  .mobile .fixed-header {
  .mobile .top-header.fixed-header {
    width: 100%;
    padding: 8px 10px 0;
  }
  .mobile .main-layout,
src/settings.js
@@ -15,7 +15,7 @@
  /**
   * 是否显示顶部导航
   */
  topNav: false,
  topNav: true,
  /**
   * 是否显示 tagsView
src/store/modules/settings.js
@@ -17,7 +17,7 @@
const useSettingsStore = defineStore("settings", () => {
  const title = ref("");
  const theme = ref(storageSetting.theme || "#374D77");
  const theme = ref(storageSetting.theme || "#008C8C");
  const sideThemeValue = ref(storageSetting.sideTheme || sideTheme);
  const showSettingsValue = ref(showSettings);
  const topNavValue = ref(storageSetting.topNav === undefined ? topNav : storageSetting.topNav);
src/views/customerService/components/viewDia.vue
@@ -154,7 +154,7 @@
    transform: translateY(-50%);
    width: 4px;
    height: 1rem;
    background-color: #374d77;
    background-color: #008c8c;
    border-radius: 2px;
  }
</style>
src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -522,7 +522,7 @@
    transform: translateY(-50%);
    width: 4px;
    height: 1rem;
    background-color: #374d77; /* Element 默认红色 */
    background-color: #008c8c; /* Element 默认红色 */
    border-radius: 2px;
  }
</style>
src/views/login.vue
@@ -463,7 +463,7 @@
<style scoped lang="scss">
  .login-page {
    --accent: var(--accent-primary, var(--el-color-primary, #374d77));
    --accent: var(--accent-primary, var(--el-color-primary, #008c8c));
    --accent-rgb: var(--el-color-primary-rgb, 22, 116, 88);
    --text: #0f172a;
    --muted: #64748b;