6 小时以前 1c829f9177eab675b22be4d20ef2761ed1bbb423
refactor(layout): 调整布局样式并优化轮播卡片组件

- 更新AppMain.vue中的导航栏高度从50px调整为60px
- 替换轮播卡片组件中的图片箭头为SVG图标并添加aria标签
- 移除卡片图标背景图,使用CSS变量替代硬编码颜色
- 将轮播卡片组件的样式统一到BI设计系统变量
- 重构数据仪表板中的饼图组件,添加中心数值显示
- 优化生产分析页面的进度表格样式和颜色配置
- 调整PSI数据分析页面的图表配色和轴线样式
- 删除已废弃的质量分析轮播卡片组件文件
已删除2个文件
已添加7个文件
已修改55个文件
5634 ■■■■ 文件已修改
src/assets/styles/dashboard-dark.scss 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/index.scss 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/variables.module.scss 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppMain.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Logo.vue 251 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/settings.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/safetyMonitoring/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/CarouselCards.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/DateTypeSwitch.vue 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/ProductTypeSwitch.vue 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue 79 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue 131 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue 122 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/right-bottom.vue 278 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/right-top.vue 253 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/index.vue 85 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/theme.js 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/DateTypeSwitch.vue 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/PanelHeader.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue 372 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue 83 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue 294 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue 240 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index.vue 81 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index0.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/theme.js 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/financialAnalysis/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/CarouselCards.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/DateTypeSwitch.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/PanelHeader.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/ProductTypeSwitch.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-center.vue 109 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-top.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 150 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-top.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/index.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/theme.js 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/DateTypeSwitch.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/ProductTypeSwitch.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/center-center.vue 223 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/center-top.vue 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/left-top.vue 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/right-top.vue 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/index.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/theme.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/indicatorStats/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/dashboard-dark.scss
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
// æš—色大屏设计令牌:深蓝品牌风(呼应登录页 #09183a æš—部 + å“ç‰Œè“ #2c51d9)
// ç”¨æ³•:大屏根容器挂 .dashboard-dark ç±»ï¼Œå†…部用 .bi-panel / .bi-title-bar ç­‰å·¥å…·ç±»
// ä¾› reportAnalysis ä¸‹ 1920x1080 ç¼©æ”¾å¼å¤§å±å¤ç”¨ï¼›ECharts é…è‰²è§å„页面 theme.js
.dashboard-dark {
  --bi-bg-from: #0a1120;
  --bi-bg-to: #0e1a33;
  --bi-surface: #111c33;
  --bi-surface-2: #16233d;
  --bi-border: #1f2f4d;
  --bi-divider: rgba(159, 176, 207, 0.14);
  --bi-text: #e8eefc;
  --bi-text-secondary: #9fb0cf;
  --bi-text-tertiary: #64748f;
  --bi-primary: #2c51d9;
  --bi-accent: #5b8cff;
  --bi-success: #2dbd85;
  --bi-warning: #f5a524;
  --bi-danger: #f2635b;
  --bi-radius: 10px;
  // ç»Ÿä¸€é¢æ¿å®¹å™¨ï¼ˆæ›¿ä»£æ¨¡æ¿å›¾ç‰‡è¾¹æ¡†/青色描边)
  .bi-panel {
    background: var(--bi-surface);
    border: 1px solid var(--bi-border);
    border-radius: var(--bi-radius);
    box-sizing: border-box;
  }
  // é¢æ¿æ ‡é¢˜ï¼šå·¦ç«–条 + åº•部细分隔线,结构对齐首页 HomePanel
  .bi-title-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    height: 36px;
    padding: 0 12px;
    font-weight: 600;
    font-size: 16px;
    color: var(--bi-text);
    border-bottom: 1px solid var(--bi-divider);
    &::before {
      content: '';
      width: 3px;
      height: 16px;
      border-radius: 2px;
      background: var(--bi-accent);
      flex-shrink: 0;
    }
  }
  // ç­‰å®½æ•°å­—,风格对齐 .app-num
  .bi-num {
    font-variant-numeric: tabular-nums;
    font-feature-settings: 'tnum';
  }
}
src/assets/styles/index.scss
@@ -1,4 +1,5 @@
@import './tokens.scss';
@import './dashboard-dark.scss';
@import './variables.module.scss';
@import './mixin.scss';
@import './transition.scss';
src/assets/styles/sidebar.scss
@@ -22,8 +22,8 @@
    left: 0;
    z-index: 1001;
    overflow: hidden;
    -webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
    box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
    -webkit-box-shadow: 2px 0 8px rgba(0, 21, 41, 0.12);
    box-shadow: 2px 0 8px rgba(0, 21, 41, 0.12);
    // reset element-ui css
    .horizontal-collapse-transition {
@@ -45,7 +45,7 @@
    &.has-logo {
      .el-scrollbar {
        height: calc(100% - 50px);
        height: calc(100% - 60px);
      }
    }
@@ -80,46 +80,48 @@
      display: inline-block !important;
    }
    // menu hover
    // menu hover:统一走深底亮色 token
    .sub-menu-title-noDropdown,
    .el-sub-menu__title {
      &:hover {
        background-color: rgba(212, 221, 255, 0.8) !important;
        background-color: var(--menu-hover) !important;
      }
    }
    & .theme-light .is-active > .el-sub-menu__title {
      color: #fff !important;
    .is-active > .el-sub-menu__title {
      color: var(--menu-active-text) !important;
    }
    & .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .el-sub-menu .el-menu-item {
      min-width: $base-sidebar-width !important;
      background-color: var(--sidebar-sub-bg) !important;
      &:hover {
        background-color: rgba(212, 221, 255, 0.8) !important;
        background-color: var(--menu-hover) !important;
      }
      &.is-active {
        background-color: #fff !important;
      }
    }
    & .theme-light .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .theme-light .el-sub-menu .el-menu-item {
      //background-color: transparent;
      &:hover {
        background-color: rgba(212, 221, 255, 0.8) !important;
        background-color: var(--menu-active-bg) !important;
        color: var(--menu-active-text) !important;
      }
    }
  }
  .hideSidebar {
    .sidebar-container {
      width: 54px !important;
      width: 64px !important;
    }
    .main-container {
      margin-left: 54px;
      margin-left: 64px;
    }
    // æŠ˜å æ€èœå•项改圆角块、图标居中
    .el-menu-item,
    .el-sub-menu__title {
      margin: 4px 8px;
      border-radius: 8px;
      text-align: center;
    }
    .sub-menu-title-noDropdown {
@@ -130,7 +132,7 @@
        padding: 0 !important;
        .svg-icon {
          margin-left: 20px;
          margin-left: 16px;
        }
      }
    }
@@ -141,7 +143,7 @@
        padding: 0 !important;
        .svg-icon {
          margin-left: 20px;
          margin-left: 16px;
        }
      }
    }
@@ -208,14 +210,15 @@
    }
  }
  // æŠ˜å å¼¹å‡ºçš„二级菜单:跟随侧栏深底
  .nest-menu .el-sub-menu > .el-sub-menu__title,
  .el-menu-item {
    &:hover {
      // you can use $sub-menuHover
      background-color: rgba(212, 221, 255, 0.56) !important;
      background-color: var(--menu-hover) !important;
    }
    &.is-active {
      background-color: rgba(212, 221, 255, 0.56) !important;
      background-color: var(--menu-active-bg) !important;
      color: var(--menu-active-text) !important;
    }
  }
@@ -223,9 +226,10 @@
  > .el-menu--popup {
    max-height: 100vh;
    overflow-y: auto;
    background-color: var(--sidebar-bg) !important;
    &::-webkit-scrollbar-track-piece {
      background: #d3dce6;
      background: transparent;
    }
    &::-webkit-scrollbar {
@@ -233,7 +237,7 @@
    }
    &::-webkit-scrollbar-thumb {
      background: #99a9bf;
      background: rgba(255, 255, 255, 0.18);
      border-radius: 20px;
    }
  }
src/assets/styles/variables.module.scss
@@ -21,8 +21,8 @@
$menuLightActiveText: #002fa7;
// åŸºç¡€å˜é‡
$base-sidebar-width: 200px;
$sideBarWidth: 200px;
$base-sidebar-width: 220px;
$sideBarWidth: 220px;
// èœå•暗色变量
$base-menu-color: #bfcbd9;
@@ -67,13 +67,27 @@
// CSS变量定义
:root {
  /* äº®è‰²æ¨¡å¼å˜é‡ */
  --sidebar-bg: #{$menuBg};
  --sidebar-text: #{$menuText};
  --menu-hover: #{$menuHover};
  /* ä¾§è¾¹æ ï¼šç§‘技蓝渐变(呼应登录页 hero å®è“â†’深蓝) */
  --sidebar-bg: #1b3a94;
  --sidebar-sub-bg: rgba(0, 0, 0, 0.16);
  --sidebar-grad-from: #2f57e0;
  --sidebar-grad-to: #14264f;
  --sidebar-text: rgba(255, 255, 255, 0.72);
  --menu-hover: rgba(255, 255, 255, 0.10);
  --menu-active-bg: rgba(255, 255, 255, 0.18);
  --menu-active-text: #ffffff;
  --menu-active-indicator: #ffffff;
  --navbar-bg: #ffffff;
  --navbar-text: #303133;
  --navbar-border: #e8ecf1;
  /* é¡µç­¾æ  */
  --tags-bg: #ffffff;
  --tags-item-bg: #ffffff;
  --tags-item-border: #dfe5ee;
  --tags-item-text: #4e5969;
  --tags-item-hover: #f2f5fa;
  /* splitpanes default-theme å˜é‡ */
  --splitpanes-default-bg: #ffffff;
@@ -91,14 +105,20 @@
  /* ä¾§è¾¹æ  */
  --sidebar-bg: #141414;
  --sidebar-sub-bg: #1d1e1f;
  --sidebar-grad-from: #1d1e1f;
  --sidebar-grad-to: #141414;
  --sidebar-text: #ffffff;
  --menu-hover: #2d2d2d;
  --menu-active-bg: #2d2d2d;
  --menu-active-text: #{$menuActiveText};
  --menu-active-indicator: #{$menuActiveText};
  /* é¡¶éƒ¨å¯¼èˆªæ  */
  --navbar-bg: #141414;
  --navbar-text: #ffffff;
  --navbar-hover: #141414;
  --navbar-border: #303030;
  /* æ ‡ç­¾æ  */
  --tags-bg: #141414;
src/layout/components/AppMain.vue
@@ -38,8 +38,8 @@
<style lang="scss" scoped>
.app-main {
  /* 50= navbar  50  */
  min-height: calc(100vh - 50px);
  /* 60 = navbar */
  min-height: calc(100vh - 60px);
  width: 100%;
  position: relative;
  overflow: hidden;
@@ -52,17 +52,17 @@
}
.fixed-header + .app-main {
  padding-top: 50px;
  padding-top: 60px;
}
.hasTagsView {
  .app-main {
    /* 84 = navbar + tags-view = 50 + 34 */
    min-height: calc(100vh - 84px);
    /* 100 = navbar + tags-view = 60 + 40 */
    min-height: calc(100vh - 100px);
  }
  .fixed-header + .app-main {
    padding-top: 84px;
    padding-top: 100px;
  }
}
</style>
src/layout/components/Navbar.vue
@@ -1,11 +1,10 @@
<template>
  <div class="navbar">
    <div>
    <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>
    <!--    <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />-->
    <div class="right-menu">
      <!-- æ¶ˆæ¯é€šçŸ¥ -->
      <el-popover
@@ -19,7 +18,7 @@
        <template #reference>
          <div class="notification-container right-menu-item hover-effect">
            <el-badge :value="unreadCount" :hidden="unreadCount === 0" class="notification-badge">
              <el-icon :size="20" style="cursor: pointer;">
              <el-icon :size="20">
                <Bell />
              </el-icon>
            </el-badge>
@@ -30,27 +29,26 @@
          ref="notificationCenterRef"
        />
      </el-popover>
      <div class="avatar-container">
        <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
          <div class="avatar-wrapper">
            <img :src="userStore.avatar" class="user-avatar" />
            <el-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>
          </template>
        </el-dropdown>
      </div>
      <el-dropdown @command="handleCommand" trigger="click">
        <div class="avatar-container right-menu-item hover-effect">
          <img :src="userStore.avatar" class="user-avatar" />
          <span class="user-name">{{ userStore.nickName || userStore.name }}</span>
          <el-icon class="user-caret"><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>
        </template>
      </el-dropdown>
    </div>
  </div>
</template>
@@ -59,13 +57,7 @@
import { ElMessageBox } from 'element-plus'
import { Bell } from '@element-plus/icons-vue'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import NotificationCenter from './NotificationCenter/index.vue'
import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
@@ -115,10 +107,6 @@
  emits('setLayout')
}
function toggleTheme() {
  settingsStore.toggleTheme()
}
// æ¶ˆæ¯é€šçŸ¥ç›¸å…³
function handleUnreadCountChange(count) {
  unreadCount.value = count
@@ -159,114 +147,95 @@
<style lang='scss' scoped>
.navbar {
  height: 50px;
  overflow: hidden;
  height: 60px;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-right: 16px;
  background: var(--navbar-bg);
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  border-bottom: 1px solid var(--navbar-border);
  .left-menu {
    display: flex;
    align-items: center;
    height: 100%;
  }
  .hamburger-container {
    line-height: 46px;
    display: flex;
    align-items: center;
    height: 100%;
    float: left;
    padding: 0 14px;
    cursor: pointer;
    transition: background 0.3s;
    transition: background 0.2s;
    -webkit-tap-highlight-color: transparent;
    &:hover {
      background: rgba(0, 0, 0, 0.025);
      background: var(--app-surface-hover);
    }
  }
  .breadcrumb-container {
    float: left;
  }
  .topmenu-container {
    position: absolute;
    left: 50px;
  }
  .errLog-container {
    display: inline-block;
    vertical-align: top;
  }
  .right-menu {
    float: right;
    height: 100%;
    line-height: 50px;
    display: flex;
    align-items: center;
    gap: 6px;
    height: 100%;
    &:focus {
      outline: none;
    }
    // æ¯ä¸ªæŒ‰é’®ç»Ÿä¸€çš„圆角悬停底
    .right-menu-item {
      display: inline-block;
      padding: 0 8px;
      height: 100%;
      display: flex;
      align-items: center;
      height: 40px;
      padding: 0 12px;
      font-size: 18px;
      color: var(--navbar-text);
      vertical-align: text-bottom;
      border-radius: 10px;
      cursor: pointer;
      transition: background 0.2s;
      &.hover-effect {
        cursor: pointer;
        transition: background 0.3s;
        &:hover {
          background: rgba(0, 0, 0, 0.025);
        }
      }
      &.theme-switch-wrapper {
        display: flex;
        align-items: center;
        svg {
          transition: transform 0.3s;
          &:hover {
            transform: scale(1.15);
          }
        }
      &.hover-effect:hover {
        background: var(--app-surface-hover);
      }
    }
    .notification-container {
      margin-right: 20px;
    .notification-badge {
      display: flex;
      align-items: center;
      cursor: pointer;
      .notification-badge {
        :deep(.el-badge__content) {
          border: none;
        }
      :deep(.el-badge__content) {
        border: none;
      }
    }
    .avatar-container {
      margin-right: 40px;
      gap: 8px;
      .avatar-wrapper {
        margin-top: 5px;
        position: relative;
      .user-avatar {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        object-fit: cover;
        background: var(--app-surface-hover);
      }
        .user-avatar {
          cursor: pointer;
          width: 40px;
          height: 40px;
          border-radius: 50px;
        }
      .user-name {
        font-size: 14px;
        font-weight: 500;
        color: var(--app-text);
        max-width: 120px;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
        i {
          cursor: pointer;
          position: absolute;
          right: -20px;
          top: 14px;
          font-size: 12px;
        }
      .user-caret {
        font-size: 12px;
        color: var(--app-text-tertiary);
      }
    }
  }
src/layout/components/Sidebar/Logo.vue
@@ -1,129 +1,122 @@
<template>
  <div class="sidebar-logo-container" :class="{ 'collapse': collapse }">
    <transition name="sidebarLogoFade">
      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
        <img v-if="logoUrl" :src="logoUrl" class="sidebar-logo" @error="handleImageError" alt="公司Logo" />
        <h1 class="sidebar-title">{{ title }}</h1>
      </router-link>
      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
        <img v-if="logoUrl" :src="logoUrl" class="sidebar-logo" @error="handleImageError" alt="公司Logo" />
        <h1 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' // å¯¼å…¥é»˜è®¤logo
defineProps({
  collapse: {
    type: Boolean,
    required: true
  }
})
const title = import.meta.env.VITE_APP_TITLE
const userStore = useUserStore()
// å¤„理工厂名称,生成合法的文件名
const cleanFactoryName = computed(() => {
  if (!userStore.currentFactoryName) return ''
  return userStore.currentFactoryName.trim()
})
// åŠ¨æ€logo路径
const logoUrl = ref('')
// æ£€æŸ¥logo是否存在并设置url
const updateLogoUrl = () => {
  if (!cleanFactoryName.value) {
    logoUrl.value = defaultLogo
    return
  }
  // ä½¿ç”¨Vite的动态导入
  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 = (event) => {
  console.warn('Logo加载失败,使用默认Logo')
  logoUrl.value = defaultLogo
}
</script>
<style lang="scss" scoped>
@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: 50px !important;
  line-height: 50px;
  background: #fff;
  text-align: center;
  overflow: hidden;
  & .sidebar-logo-link {
    height: 100%;
    width: 100%;
    & .sidebar-logo {
      width: 100%;
      height: 100%;
      // height: 32px;
      vertical-align: middle;
      margin-right: 12px;
    }
    & .sidebar-title {
      display: inline-block;
      margin: 0;
      color: v-bind(getLogoTextColor);
      font-weight: 600;
      line-height: 50px;
      font-size: 14px;
      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
      vertical-align: middle;
    }
  }
  &.collapse {
    .sidebar-logo {
      margin-right: 0px;
    }
  }
}
</style>
<template>
  <div class="sidebar-logo-container" :class="{ 'collapse': collapse }">
    <transition name="sidebarLogoFade">
      <router-link key="logo" class="sidebar-logo-link" to="/">
        <span class="sidebar-logo-patch">
          <img v-if="logoUrl" :src="logoUrl" class="sidebar-logo" @error="handleImageError" alt="公司Logo" />
        </span>
      </router-link>
    </transition>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import useUserStore from '@/store/modules/user'
import defaultLogo from '@/assets/login/JCCKLogo.png' // ä¸Žç™»å½•页保持一致的品牌 Logo
defineProps({
  collapse: {
    type: Boolean,
    required: true
  }
})
const userStore = useUserStore()
// å¤„理工厂名称,生成合法的文件名
const cleanFactoryName = computed(() => {
  if (!userStore.currentFactoryName) return ''
  return userStore.currentFactoryName.trim()
})
// åŠ¨æ€logo路径
const logoUrl = ref('')
// æ£€æŸ¥logo是否存在并设置url
const updateLogoUrl = () => {
  if (!cleanFactoryName.value) {
    logoUrl.value = defaultLogo
    return
  }
  // ä½¿ç”¨Vite的动态导入
  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 = (event) => {
  console.warn('Logo加载失败,使用默认Logo')
  logoUrl.value = defaultLogo
}
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
  transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
  opacity: 0;
}
.sidebar-logo-container {
  position: relative;
  width: 100%;
  height: 60px;
  background: transparent;
  overflow: hidden;
  & .sidebar-logo-link {
    display: flex;
    align-items: center;
    height: 100%;
    width: 100%;
    padding: 7px 10px;
    box-sizing: border-box;
    // logo åŽŸå›¾è‡ªå¸¦ç™½åº•ï¼Œç”¨ç™½åœ†è§’è´´ç‰‡å¡«æ»¡æ•´æ¡ Logo æ 
    & .sidebar-logo-patch {
      flex: 1;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 2px 6px;
      box-sizing: border-box;
      background: #fff;
      border-radius: 8px;
      overflow: hidden;
    }
    & .sidebar-logo {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: contain;
    }
  }
}
</style>
src/layout/components/Sidebar/index.vue
@@ -2,9 +2,9 @@
  <div :class="{ 'has-logo': showLogo }" class="sidebar-container">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <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">
      <el-menu :default-active="activeMenu" :collapse="isCollapse" background-color="transparent"
        text-color="var(--sidebar-text)" :unique-opened="true" active-text-color="var(--menu-active-text)"
        :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>
@@ -15,7 +15,6 @@
<script setup>
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/assets/styles/variables.module.scss'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
@@ -28,25 +27,7 @@
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(() => {
  if (settingsStore.isDark) {
    return 'var(--sidebar-bg)'
  }
  // æµ…色主题时,直接用主题色
  return sideTheme.value === 'theme-dark' ? variables.menuBg : settingsStore.theme
})
// èŽ·å–èœå•æ–‡å­—é¢œè‰²
const getMenuTextColor = computed(() => {
  if (settingsStore.isDark) {
    return 'var(--sidebar-text)'
  }
  return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText
})
const activeMenu = computed(() => {
  const { meta, path } = route
@@ -59,35 +40,64 @@
<style lang="scss" scoped>
.sidebar-container {
  background-color: v-bind(getMenuBackground);
  background: linear-gradient(180deg, var(--sidebar-grad-from) 0%, var(--sidebar-grad-to) 100%);
  .scrollbar-wrapper {
    background-color: v-bind(getMenuBackground);
    background: transparent;
  }
  .el-menu {
    border: none;
    height: 100%;
    width: 100% !important;
    --el-menu-item-height: 46px;
    --el-menu-sub-item-height: 42px;
    padding: 6px 0;
    .el-menu-item,
    .el-sub-menu__title {
    // åœ†è§’行块 + hover/激活态
    :deep(.el-menu-item),
    :deep(.el-sub-menu__title) {
      position: relative;
      margin: 3px 10px;
      border-radius: 8px;
      color: var(--sidebar-text);
      background-color: transparent;
      &:hover {
        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
        background-color: var(--menu-hover) !important;
        color: var(--menu-active-text);
      }
    }
    .el-menu-item {
      color: v-bind(getMenuTextColor);
    :deep(.el-menu-item.is-active) {
      background-color: var(--menu-active-bg) !important;
      color: var(--menu-active-text) !important;
      font-weight: 600;
      &.is-active {
        color: var(--menu-active-text, #409eff);
        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
      // è´´ä¾§æ å·¦ç¼˜çš„亮蓝指示条
      &::before {
        content: '';
        position: absolute;
        left: -10px;
        top: 9px;
        bottom: 9px;
        width: 3px;
        border-radius: 0 3px 3px 0;
        background: var(--menu-active-indicator);
      }
    }
    .el-sub-menu__title {
      color: v-bind(getMenuTextColor);
    // å­èœå•层更深一档底色
    :deep(.el-sub-menu .el-menu) {
      background-color: var(--sidebar-sub-bg) !important;
      .el-menu-item {
        background-color: transparent;
      }
      .el-menu-item.is-active {
        background-color: var(--menu-active-bg) !important;
      }
    }
  }
}
src/layout/components/TagsView/index.vue
@@ -8,7 +8,6 @@
        :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)"
      >
@@ -45,7 +44,6 @@
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'
const visible = ref(false)
@@ -61,7 +59,6 @@
const visitedViews = computed(() => useTagsViewStore().visitedViews)
const routes = computed(() => usePermissionStore().routes)
const theme = computed(() => useSettingsStore().theme)
watch(route, () => {
  addTags()
@@ -83,14 +80,6 @@
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) {
@@ -259,42 +248,44 @@
<style lang="scss" scoped>
.tags-view-container {
  height: 34px;
  height: 40px;
  width: 100%;
  background: transparent;
  background: var(--tags-bg);
  border-bottom: 1px solid var(--navbar-border);
  .tags-view-wrapper {
    .tags-view-item {
      display: inline-block;
      display: inline-flex;
      align-items: center;
      gap: 6px;
      position: relative;
      cursor: pointer;
      height: 32px;
      line-height: 32px;
      //border: 1px solid var(--tags-item-border, #d8dce5);
      color: #4E5463;
      background: #E5E7EA;
      padding: 0 16px;
      height: 26px;
      margin: 7px 0 7px 8px;
      padding: 0 12px;
      font-size: 12px;
      //margin-left: 5px;
      //margin-top: 4px;
      color: var(--tags-item-text);
      background: var(--tags-item-bg);
      border: 1px solid var(--tags-item-border);
      border-radius: 8px;
      transition: color 0.2s, background-color 0.2s, border-color 0.2s;
      //&:first-of-type {
      //  margin-left: 8px;
      //}
      //
      //&:last-of-type {
      //  margin-right: 15px;
      //}
      &:last-of-type {
        margin-right: 8px;
      }
      &:not(.active):hover {
        color: var(--app-primary);
        background: var(--tags-item-hover);
      }
      &.active {
        background-color: #FFFFFF !important;
        color: #2C51D9;
        color: var(--app-primary);
        background: var(--app-primary-soft);
        border-color: var(--app-primary);
        font-weight: 500;
      }
    }
    //.tags-view-item div {
    //  transform: skew(12deg);
    //  display: inline-block;
    //}
  }
  .contextmenu {
@@ -304,11 +295,11 @@
    position: absolute;
    list-style-type: none;
    padding: 5px 0;
    border-radius: 4px;
    border-radius: var(--app-radius-sm);
    font-size: 12px;
    font-weight: 400;
    color: var(--tags-item-text, #333);
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
    box-shadow: var(--app-shadow-md);
    border: 1px solid var(--el-border-color-light, #e4e7ed);
    li {
src/layout/index.vue
@@ -101,7 +101,7 @@
}
.hideSidebar .fixed-header {
  width: calc(100% - 54px);
  width: calc(100% - 64px);
}
.sidebarHide .fixed-header {
src/settings.js
@@ -6,7 +6,7 @@
  /**
   * ä¾§è¾¹æ ä¸»é¢˜ æ·±è‰²ä¸»é¢˜theme-dark,浅色主题theme-light
   */
  sideTheme: 'theme-light',
  sideTheme: 'theme-dark',
  /**
   * æ˜¯å¦ç³»ç»Ÿå¸ƒå±€é…ç½®
   */
src/views/productionManagement/safetyMonitoring/index.vue
@@ -661,7 +661,7 @@
.safety-monitoring {
    padding: 20px;
    background-color: #f5f7fa;
    min-height: calc(100vh - 84px);
    min-height: calc(100vh - 100px);
}
.monitoring-card, .control-card {
src/views/projectManagement/projectType/index.vue
@@ -267,7 +267,7 @@
<style scoped lang="scss">
.app-container {
  background-color: #f5f7fa;
  height: calc(100vh - 84px);
  height: calc(100vh - 100px);
  padding: 20px;
  display: flex;
  flex-direction: column;
src/views/reportAnalysis/PSIDataAnalysis/components/CarouselCards.vue
@@ -1,11 +1,14 @@
<template>
  <div class="carousel-cards">
    <button
    <button
      v-if="canScrollLeft"
      class="nav-button nav-button-left"
      aria-label="向左滚动"
      @click="scrollLeftFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="左箭头" />
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M15 18l-6-6 6-6"/>
      </svg>
    </button>
    <div 
      class="cards-container" 
@@ -30,12 +33,15 @@
        </div>
      </div>
    </div>
    <button
    <button
      v-if="canScrollRight"
      class="nav-button nav-button-right"
      aria-label="向右滚动"
      @click="scrollRightFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="右箭头" />
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M9 6l6 6-6 6"/>
      </svg>
    </button>
  </div>
</template>
@@ -186,42 +192,32 @@
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  background: rgba(26, 88, 176, 0.6);
  border: 1px solid rgba(26, 88, 176, 0.8);
  width: 28px;
  height: 28px;
  background: var(--bi-panel-bg, rgba(20, 32, 66, 0.55));
  border: 1px solid var(--bi-panel-border, #1e3160);
  border-radius: 50%;
  color: var(--bi-text, #8fa3c8);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 10;
  transition: all 0.3s ease;
  transition: color 0.2s ease, border-color 0.2s ease;
  padding: 0;
}
.nav-button:hover {
  background: rgba(26, 88, 176, 0.8);
  transform: translateY(-50%) scale(1.1);
  color: var(--bi-text-strong, #eaf1ff);
  border-color: var(--bi-accent-bright, #4a6cf0);
}
.nav-button-left {
  left: -16px;
}
.nav-button-left img {
  width: 16px;
  height: 16px;
  transform: rotate(180deg);
  left: -14px;
}
.nav-button-right {
  right: -16px;
}
.nav-button-right img {
  width: 16px;
  height: 16px;
  right: -14px;
}
.card-item {
@@ -229,14 +225,17 @@
  min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
  display: flex;
  align-items: center;
  background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%);
  border-radius: 8px 8px 8px 8px;
  background: var(--bi-panel-bg, rgba(20, 32, 66, 0.55));
  border: 1px solid var(--bi-panel-border, #1e3160);
  border-left: 2px solid var(--bi-accent-bright, #4a6cf0);
  border-radius: 6px;
  padding: 12px 16px;
  transition: all 0.3s ease;
  transition: border-color 0.2s ease, background-color 0.2s ease;
}
.card-item:hover {
  transform: translateY(-2px);
  background: rgba(33, 49, 96, 0.65);
  border-color: var(--bi-accent-bright, #4a6cf0);
}
.card-icon {
@@ -259,7 +258,7 @@
.card-label {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  color: var(--bi-text, #8fa3c8);
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
@@ -280,7 +279,7 @@
  gap: 6px;
  font-weight: 400;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.85);
  color: var(--bi-text, #8fa3c8);
}
.rate-label {
@@ -292,15 +291,16 @@
}
.value-number {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  font-weight: 500;
  font-size: 16px;
  color: var(--bi-text-strong, #eaf1ff);
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.value-unit {
  font-size: 14px;
  color: #FFFFFF;
  font-size: 12px;
  color: var(--bi-text, #8fa3c8);
  font-weight: 400;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/DateTypeSwitch.vue
@@ -16,7 +16,7 @@
const props = defineProps({
  modelValue: {
    type: Number,
    default: 1, // é»˜è®¤é€‰ä¸­"周"
    default: 3, // é»˜è®¤é€‰ä¸­"季度"
  },
})
@@ -46,49 +46,49 @@
/* æœªé€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  background-color: rgba(20, 32, 66, 0.55);
  color: #8fa3c8;
  border-color: #1e3160;
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
  transition: color 0.2s ease, background-color 0.2s ease;
}
/* ç¬¬ä¸€ä¸ªæŒ‰é’®å·¦ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  border-top-left-radius: 6px;
  border-bottom-left-radius: 6px;
}
/* æœ€åŽä¸€ä¸ªæŒ‰é’®å³ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}
/* æŒ‰é’®ä¹‹é—´çš„分隔线 */
.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
  border-right: 1px solid #1e3160;
}
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ï¼šå“ç‰Œè“å®žè‰² */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  background: #2c51d9;
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  border-color: #2c51d9;
  box-shadow: none;
}
/* æ‚¬åœæ•ˆæžœ */
.date-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
  color: #eaf1ff;
}
/* é€‰ä¸­çŠ¶æ€æ‚¬åœ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  background: #4a6cf0;
  border-color: #4a6cf0;
  color: #ffffff;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue
@@ -16,18 +16,27 @@
<style scoped>
.panel-header {
  background-image: url("@/assets/BI/kehuhetongback@2x.png");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 4px;
  height: 36px;
}
/* å“ç‰Œè“ç»†ç«–条,呼应 HomePanel æ ‡é¢˜æ ·å¼ */
.panel-header::before {
  content: '';
  width: 3px;
  height: 14px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--bi-accent-bright, #4a6cf0) 0%, var(--bi-accent, #2c51d9) 100%);
  flex-shrink: 0;
}
.panel-title {
  width: 100%;
  font-weight: 500;
  font-size: 16px;
  color: #D9ECFF;
  padding-left: 46px;
  line-height: 36px;
  color: var(--bi-text-strong, #eaf1ff);
  letter-spacing: 1px;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/ProductTypeSwitch.vue
@@ -43,43 +43,43 @@
}
.product-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  background-color: rgba(20, 32, 66, 0.55);
  color: #8fa3c8;
  border-color: #1e3160;
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
  transition: color 0.2s ease, background-color 0.2s ease;
}
.product-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  border-top-left-radius: 6px;
  border-bottom-left-radius: 6px;
}
.product-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}
.product-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
  border-right: 1px solid #1e3160;
}
.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  background: #2c51d9;
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  border-color: #2c51d9;
  box-shadow: none;
}
.product-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
  color: #eaf1ff;
}
.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  background: #4a6cf0;
  border-color: #4a6cf0;
  color: #ffffff;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue
@@ -15,7 +15,7 @@
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#8FA3C8' } }"
        style="height: 260px"
      />
    </div>
@@ -29,8 +29,9 @@
import PanelHeader from './PanelHeader.vue'
import ProductTypeSwitch from './ProductTypeSwitch.vue'
import { productInOutAnalysis } from '@/api/viewIndex.js'
import { BI, withTooltip } from '../theme.js'
const productType = ref(1) // 1=原材料 2=半成品 3=成品
const productType = ref(1) // 1=原材料 2=成品 3=半成品(与 ProductTypeSwitch ä¸€è‡´ï¼‰
const chartStyle = { width: '100%', height: '130%' }
@@ -49,10 +50,10 @@
  itemGap: 24,
  itemWidth: 12,
  itemHeight: 12,
  textStyle: { color: '#B8C8E0', fontSize: 14 },
  textStyle: { color: BI.text, fontSize: 14 },
  data: [
    { name: '出库', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
    { name: '入库', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
    { name: '出库', itemStyle: { color: BI.accentBright } },
    { name: '入库', itemStyle: { color: '#35C6D6' } },
  ],
}
@@ -61,9 +62,9 @@
    type: 'category',
    data: [],
    axisTick: { show: false },
    axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } },
    axisLine: { show: false },
    axisLabel: { color: BI.text, fontSize: 12 },
    splitLine: { show: false },
  },
])
@@ -71,11 +72,11 @@
  {
    type: 'value',
    name: '单位: ä»¶',
    nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 0] },
    nameTextStyle: { color: BI.text, fontSize: 12, padding: [0, 0, 0, 0] },
    axisLine: { show: false },
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    splitLine: { lineStyle: { color: '#B8C8E0' } },
    axisLabel: { color: BI.text, fontSize: 12 },
    splitLine: { lineStyle: { color: 'rgba(143, 163, 200, 0.15)' } },
  },
]
@@ -83,16 +84,16 @@
  {
    name: '出库',
    type: 'line',
    smooth: false,
    smooth: true,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 },
    itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 },
    symbolSize: 6,
    lineStyle: { color: BI.accentBright, width: 2 },
    itemStyle: { color: BI.accentBright, borderWidth: 0 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(11, 137, 254, 0.40)' },
        { offset: 1, color: 'rgba(11, 137, 254, 0.05)' },
        { offset: 0, color: 'rgba(74, 108, 240, 0.25)' },
        { offset: 1, color: 'rgba(74, 108, 240, 0)' },
      ]),
    },
    data: [],
@@ -101,16 +102,16 @@
  {
    name: '入库',
    type: 'line',
    smooth: false,
    smooth: true,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 },
    itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 },
    symbolSize: 6,
    lineStyle: { color: '#35C6D6', width: 2 },
    itemStyle: { color: '#35C6D6', borderWidth: 0 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(11, 249, 254, 0.5)' },
        { offset: 1, color: 'rgba(11, 249, 254, 0.05)' },
        { offset: 0, color: 'rgba(53, 198, 214, 0.25)' },
        { offset: 1, color: 'rgba(53, 198, 214, 0)' },
      ]),
    },
    data: [],
@@ -118,11 +119,9 @@
  },
])
const tooltip = {
const tooltip = withTooltip({
  trigger: 'axis',
  axisPointer: { type: 'line' },
  borderWidth: 1,
  textStyle: { fontSize: 12 },
  formatter(params) {
    let result = params[0].axisValue + '<br/>'
    params.forEach((item) => {
@@ -130,7 +129,7 @@
    })
    return result
  },
}
})
const fetchData = () => {
  productInOutAnalysis({ type: productType.value })
@@ -172,7 +171,9 @@
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 428px;
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue
@@ -1,15 +1,8 @@
<template>
  <div>
    <!-- è®¾å¤‡ç»Ÿè®¡ -->
    <!-- äº§å“å‘¨è½¬å¤©æ•° -->
    <div class="equipment-stats">
      <div class="equipment-header">
        <img
          src="@/assets/BI/shujutongjiicon@2x.png"
          alt="图标"
          class="equipment-icon"
        />
        <span class="equipment-title">产品周转天数</span>
      </div>
      <PanelHeader title="产品周转天数" />
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
@@ -19,7 +12,7 @@
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        :options="baseOptions"
        style="height: 260px"
      />
    </div>
@@ -29,11 +22,14 @@
<script setup>
import { ref, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import { productTurnoverDays } from '@/api/viewIndex.js'
import { BI, baseChartOptions, withTooltip } from '../theme.js'
const chartStyle = { width: '100%', height: '100%' }
const grid = { left: '3%', right: '4%', bottom: '3%', top: '4%', containLabel: true }
const barLegend = { show: false, textStyle: { color: '#B8C8E0' }, data: ['周转天数'] }
const barLegend = { show: false, textStyle: { color: BI.text }, data: ['周转天数'] }
const baseOptions = baseChartOptions
const barSeries1 = ref([
  {
    name: '周转天数',
@@ -42,19 +38,20 @@
    barWidth: 30,
    emphasis: { focus: 'series' },
    itemStyle: {
      borderRadius: [4, 4, 0, 0],
      color: {
        type: 'linear',
        x: 0, y: 1, x2: 0, y2: 0,
        colorStops: [
          { offset: 0, color: 'rgba(0,164,237,0)' },
          { offset: 1, color: '#4EE4FF' },
          { offset: 0, color: 'rgba(74, 108, 240, 0.05)' },
          { offset: 1, color: BI.accentBright },
        ],
      },
    },
    data: [],
  },
])
const tooltip = {
const tooltip = withTooltip({
  trigger: 'axis',
  axisPointer: { type: 'shadow' },
  formatter(params) {
@@ -64,9 +61,9 @@
    })
    return result
  },
}
const xAxis1 = ref([{ type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] }])
const yAxis1 = [{ type: 'value', axisLabel: { color: '#B8C8E0' } }]
})
const xAxis1 = ref([{ type: 'category', axisTick: { show: false }, axisLabel: { color: BI.text }, data: [] }])
const yAxis1 = [{ type: 'value', axisLabel: { color: BI.text } }]
const fetchData = () => {
  productTurnoverDays()
@@ -89,41 +86,12 @@
<style scoped>
.equipment-stats {
  border: 1px solid #1a58b0;
  padding: 0 18px 18px;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 10px 18px 18px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.equipment-header {
  font-weight: 500;
  font-size: 21px;
  display: flex;
  border-bottom: 1px solid;
  border-image: linear-gradient(
      270deg,
      rgba(0, 126, 255, 0) 0%,
      rgba(0, 126, 255, 0.4549) 35%,
      #007eff 78%,
      #007eff 100%
    )
    1;
  padding-bottom: 2px;
}
.equipment-title {
  font-weight: 500;
  font-size: 18px;
  background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  line-height: 50px;
}
.equipment-icon {
  width: 50px;
  height: 50px;
  gap: 6px;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue
@@ -3,11 +3,27 @@
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <div
        v-for="item in statItems"
        v-for="(item, index) in statItems"
        :key="item.name"
        class="stat-card"
      >
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-icon" :style="{ color: iconColors[index % iconColors.length] }">
          <!-- é”€å”® -->
          <svg v-if="index % 3 === 0" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <path d="M3 17l6-6 4 4 8-8" />
            <path d="M15 7h6v6" />
          </svg>
          <!-- é‡‡è´­ -->
          <svg v-else-if="index % 3 === 1" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <path d="M21 8l-9-5-9 5 9 5 9-5z" />
            <path d="M3 8v8l9 5 9-5V8" />
          </svg>
          <!-- åº“å­˜ -->
          <svg v-else width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <rect x="3" y="4" width="18" height="16" rx="2" />
            <path d="M3 10h18M12 4v16" />
          </svg>
        </div>
        <div class="card-content">
          <span class="card-label">{{ item.name }}</span>
          <span class="card-value">{{ item.value }}</span>
@@ -26,8 +42,10 @@
<script setup>
import { ref, onMounted } from 'vue'
import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js'
import { BI } from '../theme.js'
const statItems = ref([])
const iconColors = [BI.accentBright, '#35C6D6', '#8E7CE6']
const formatPercent = (val) => {
  const num = Number(val) || 0
@@ -67,38 +85,56 @@
  flex: 1;
  display: flex;
  align-items: center;
  background-image: url('@/assets/BI/border@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  background: var(--bi-panel-bg);
  border: 1px solid var(--bi-panel-border);
  border-radius: 8px;
  position: relative;
  overflow: hidden;
  height: 142px;
}
/* é¡¶éƒ¨å“ç‰Œè“äº®çº¿ï¼Œæ›¿ä»£å›¾ç‰‡æè¾¹ */
.stat-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--bi-accent) 0%, transparent 70%);
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
  width: 52px;
  height: 52px;
  margin: 0 16px 0 20px;
  border-radius: 8px;
  background: rgba(74, 108, 240, 0.12);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
  gap: 8px;
  min-width: 0;
}
.card-value {
  font-weight: 500;
  font-size: 40px;
  background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  font-weight: 600;
  font-size: 36px;
  color: var(--bi-text-strong);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.card-label {
  font-weight: 400;
  font-size: 19px;
  color: rgba(208, 231, 255, 0.7);
  font-size: 16px;
  color: var(--bi-text);
}
.card-compare {
@@ -111,11 +147,12 @@
.card-compare > span:first-child {
  font-size: 13px;
  opacity: 0.8;
  color: var(--bi-text);
}
.compare-value {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.compare-icon {
@@ -126,12 +163,12 @@
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
  color: var(--bi-up, #34c77b);
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
  color: var(--bi-down, #ff6b6b);
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
@@ -3,8 +3,11 @@
    <PanelHeader title="采购品分布" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
      <div class="pie-chart-wrapper">
        <div class="pie-center">
          <span class="pie-center-value">{{ totalAmount }}</span>
          <span class="pie-center-label">合计(元)</span>
        </div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -22,15 +25,12 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
import { BI, baseChartOptions, withTooltip, formatAmount } from '../theme.js'
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
@@ -53,9 +53,13 @@
const cardItems = ref([])
// é¢œè‰²åˆ—表
const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
const landColors = BI.series
const landObjData = computed(() => array2obj(dataList.value, 'name'))
const totalAmount = computed(() =>
  formatAmount(dataList.value.reduce((sum, d) => sum + (Number(d.value) || 0), 0))
)
// å›¾ä¾‹é…ç½®ï¼ˆå³ä¾§ç«–排)
const landLegend = computed(() => {
@@ -63,7 +67,7 @@
    name: d.name,
    icon: 'circle',
    textStyle: {
      fontSize: 18,
      fontSize: 14,
      color: landColors[idx % landColors.length],
    },
  }))
@@ -72,34 +76,34 @@
    orient: 'vertical',
    top: 'center',
    left: '52%',
    itemGap: 30,
    itemGap: 24,
    data: data,
    formatter: function (name) {
      const item = landObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}`
      return `{title|${name}}{value|${formatAmount(item.value)}}{percent|${item.rate}}{unit|%}`
    },
    textStyle: {
      rich: {
        value: {
          color: '#43e8fc',
        title: {
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 10],
        },
        unit: {
          color: '#82baff',
          fontSize: 12,
          fontWeight: 600,
          padding: [0, 10, 0, 0],
        },
        percent: {
          color: '#43e8fc',
          fontSize: 14,
          fontWeight: 600,
          color: BI.text,
          padding: [0, 0, 0, 0],
        },
        title: {
        value: {
          color: BI.textStrong,
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 6, 0, 10],
        },
        percent: {
          color: BI.accentBright,
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 6],
        },
        unit: {
          color: BI.text,
          fontSize: 12,
          padding: [0, 0, 0, 0],
        },
@@ -109,21 +113,22 @@
})
// æç¤ºæ¡†
const landTooltip = {
const landTooltip = withTooltip({
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c}元 ({d}%)',
}
})
// åŒå±‚环形饼图
const landSeries = ref([
  {
    name: '产品采购金额分析',
    type: 'pie',
    radius: ['40%', '60%'],
    radius: ['48%', '68%'],
    center: ['25%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderColor: BI.sliceBorder,
      borderWidth: 2,
      borderRadius: 4,
      color: function (params) {
        return landColors[params.dataIndex % landColors.length]
      },
@@ -140,9 +145,9 @@
    },
  },
  {
    // å†…圈
    // å†…圈轨道环
    type: 'pie',
    radius: ['40%', '45%'],
    radius: ['40%', '43%'],
    center: ['25%', '50%'],
    silent: true,
    label: {
@@ -152,7 +157,7 @@
      show: false,
    },
    itemStyle: {
      color: 'rgba(0, 127, 255, 0.25)',
      color: 'rgba(74, 108, 240, 0.22)',
    },
    data: [1],
  },
@@ -163,22 +168,7 @@
  height: '100%',
}
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法
// å›¾è¡¨ä¸­å¿ƒæ˜¯ ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: dataList // ç›‘听数据变化,自动调整位置
})
const pieOptions = baseChartOptions
const fetchData = () => {
  rawMaterialPurchaseAmountRatio()
@@ -207,11 +197,6 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -223,7 +208,9 @@
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 449px;
@@ -236,20 +223,28 @@
  background: transparent;
}
.pie-background {
.pie-center {
  position: absolute;
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* ä½ç½®ç”± JS åŠ¨æ€è®¾ç½®ï¼Œé»˜è®¤å±…ä¸­ */
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  pointer-events: none;
  z-index: 2;
}
.pie-center-value {
  font-weight: 600;
  font-size: 20px;
  color: var(--bi-text-strong);
  font-variant-numeric: tabular-nums;
}
.pie-center-label {
  font-size: 12px;
  color: var(--bi-text);
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
@@ -3,8 +3,11 @@
    <PanelHeader title="销售品分布" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
      <div class="pie-chart-wrapper">
        <div class="pie-center">
          <span class="pie-center-value">{{ totalAmount }}</span>
          <span class="pie-center-label">合计(元)</span>
        </div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
@@ -21,15 +24,12 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
import { BI, baseChartOptions, withTooltip, formatAmount } from '../theme.js'
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
@@ -52,16 +52,20 @@
const echartsRef = ref(null)
const pieDatas = ref([])
const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
const pieColors = BI.series
const pieObjData = computed(() => array2obj(pieDatas.value, 'name'))
const totalAmount = computed(() =>
  formatAmount(pieDatas.value.reduce((sum, d) => sum + (Number(d.value) || 0), 0))
)
const pieLegend = computed(() => {
  const data = pieDatas.value.map((d, idx) => ({
    name: d.name,
    icon: 'circle',
    textStyle: {
      fontSize: 18,
      fontSize: 14,
      color: pieColors[idx % pieColors.length],
    },
  }))
@@ -70,34 +74,34 @@
    orient: 'vertical',
    top: 'center',
    left: '52%',
    itemGap: 30,
    itemGap: 24,
    data: data,
    formatter: function (name) {
      const item = pieObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}`
      return `{title|${name}}{value|${formatAmount(item.value)}}{percent|${item.rate}}{unit|%}`
    },
    textStyle: {
      rich: {
        value: {
          color: '#43e8fc',
        title: {
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 10],
        },
        unit: {
          color: '#82baff',
          fontSize: 12,
          fontWeight: 600,
          padding: [0, 10, 0, 0],
        },
        percent: {
          color: '#43e8fc',
          fontSize: 14,
          fontWeight: 600,
          color: BI.text,
          padding: [0, 0, 0, 0],
        },
        title: {
        value: {
          color: BI.textStrong,
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 6, 0, 10],
        },
        percent: {
          color: BI.accentBright,
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 6],
        },
        unit: {
          color: BI.text,
          fontSize: 12,
          padding: [0, 0, 0, 0],
        },
@@ -106,20 +110,21 @@
  }
})
const pieTooltip = {
const pieTooltip = withTooltip({
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c}元 ({d}%)',
}
})
const pieSeries = computed(() => [
  {
    name: '产品销售金额分析',
    type: 'pie',
    radius: '60%',
    radius: ['48%', '68%'],
    center: ['25%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderColor: BI.sliceBorder,
      borderWidth: 2,
      borderRadius: 4,
    },
    label: {
      show: false
@@ -134,23 +139,9 @@
  },
])
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
const pieOptions = baseChartOptions
const cardItems = ref([])
// ä½¿ç”¨å°è£…的背景位置调整方法(与其他文件保持一致)
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: pieDatas // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  productSalesAnalysis()
@@ -177,11 +168,6 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
@@ -193,28 +179,42 @@
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 449px;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
}
.pie-background {
.pie-center {
  position: absolute;
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  pointer-events: none;
  z-index: 2;
}
.pie-center-value {
  font-weight: 600;
  font-size: 20px;
  color: var(--bi-text-strong);
  font-variant-numeric: tabular-nums;
}
.pie-center-label {
  font-size: 12px;
  color: var(--bi-text);
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/right-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,278 @@
<template>
  <div>
    <PanelHeader title="客户贡献排名" />
    <div class="panel-item-customers">
      <div class="switch-container">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="{ show: false }"
        :series="series"
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="{ backgroundColor: 'transparent', textStyle: { color: BI.text } }"
        style="height: 360px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
import { customerContributionRanking } from '@/api/viewIndex.js'
import { BI, withTooltip } from '../theme.js'
const chartStyle = {
  width: '100%',
  height: '100%',
}
const dateType = ref(3) // 1=周 2=月 3=季度,默认季度
// åŽŸå§‹æ•°æ®ï¼ˆç»Ÿä¸€æˆ { NAME, NUM })
const dataArr = ref([])
const dataArray = computed(() => {
  const sortedAsc = [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
  return sortedAsc.length > 5 ? sortedAsc.slice(-5) : sortedAsc
})
const total = computed(() => dataArray.value.reduce((sum, v) => sum + Number(v.NUM || 0), 0))
const xdataName = computed(() => dataArray.value.map((v) => v.NAME))
// æŽ’名配色:Top1 é‡‘色,Top2-3 äº®è“ï¼Œå…¶ä½™æš—蓝
const rankColor = (rank) => {
  if (rank === 1) return '#F0B14A'
  if (rank <= 3) return BI.accentBright
  return '#2C4A9E'
}
const getRank = (i) => dataArray.value.length - i
const dataNum = computed(() => {
  return dataArray.value.map((v, i) => {
    const rank = getRank(i)
    return {
      value: Number(v.NUM),
      itemStyle: {
        color: rankColor(rank),
        barBorderRadius: [0, 10, 10, 0],
      },
    }
  })
})
const bgData = computed(() => {
  const maxValue = dataNum.value.length ? Math.max(...dataNum.value.map((v) => v.value)) : 0
  return dataNum.value.map(() => maxValue * 1.08 || 1)
})
const tooltip = withTooltip({
  trigger: 'axis',
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    result += `<div>${params[0].marker}${params[0].value}元</div>`
    return result
  },
})
const grid = computed(() => ({ top: 0, left: '20%', right: '10%', bottom: 0 }))
const xAxis = computed(() => [
  {
    splitLine: { show: false },
    axisLine: { show: false },
    axisLabel: { show: false },
    axisTick: { show: false },
  },
])
const yAxis = computed(() => [
  {
    type: 'category',
    inverse: false,
    data: xdataName.value,
    axisLabel: {
      formatter: (value) => {
        if (!value) return ''
        const maxLen = 6 // æ¯è¡Œæœ€å¤šå­—符数,可按需调整
        if (value.length <= maxLen) return `{a|${value}}`
        const lines = []
        for (let i = 0; i < value.length; i += maxLen) {
          lines.push(value.slice(i, i + maxLen))
        }
        return lines.map((line) => `{a|${line}}`).join('\n')
      },
      rich: {
        a: {
          width: 120,
          fontSize: 14,
          color: BI.textStrong,
          padding: [5, 4, 5, 0],
          align: 'right',
        },
      },
    },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false },
  },
  {
    type: 'category',
    data: dataNum.value.map((item) => item.value),
    axisLabel: {
      formatter: (params, index) => {
        const value = typeof params === 'object' ? params.value : params
        const percent = total.value ? ((value / total.value) * 100).toFixed(0) : 0
        const rank = getRank(index)
        return `{a${rank}|${percent} }{b${rank}|%}`
      },
      rich: dataNum.value.reduce((rich, _, i) => {
        const rank = getRank(i)
        const color = rank === 1 ? '#F0B14A' : rank <= 3 ? BI.accentBright : BI.text
        rich[`a${rank}`] = { fontSize: 16, color, verticalAlign: 'bottom' }
        rich[`b${rank}`] = { fontSize: 12, color, verticalAlign: 'bottom' }
        return rich
      }, {}),
    },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false },
  },
])
const series = computed(() => [
  {
    name: '背景',
    z: 5,
    type: 'bar',
    barWidth: 18,
    tooltip: { show: false },
    itemStyle: {
      color: 'rgba(143, 163, 200, 0.12)',
      barBorderRadius: [0, 10, 10, 0],
    },
    data: bgData.value,
  },
  {
    name: '金额',
    z: 6,
    type: 'bar',
    barWidth: 18,
    barGap: '-100%',
    data: dataNum.value,
  },
])
const normalizeItem = (item) => {
  const name =
    item?.NAME ??
    item?.name ??
    item?.customerName ??
    item?.customer ??
    item?.label ??
    '-'
  const num =
    item?.NUM ??
    item?.num ??
    item?.value ??
    item?.amount ??
    item?.money ??
    0
  return { NAME: String(name), NUM: Number(num) || 0 }
}
const getMockListByType = (type) => {
  // æ¨¡æ‹Ÿå‡æ•°æ®ï¼ˆé‡‘额贡献排名)
  // type: 1=周 2=月 3=季度
  if (type === 2) {
    return [
      { NAME: '华东精密', NUM: 5120000 },
      { NAME: '星辰电子', NUM: 3860000 },
      { NAME: '启航科技', NUM: 2720000 },
      { NAME: '铭诚制造', NUM: 2160000 },
      { NAME: '远景材料', NUM: 1430000 },
      { NAME: '德润贸易', NUM: 910000 },
      { NAME: '宏达配套', NUM: 680000 },
    ]
  }
  if (type === 3) {
    return [
      { NAME: '华东精密', NUM: 16800000 },
      { NAME: '星辰电子', NUM: 12960000 },
      { NAME: '启航科技', NUM: 9720000 },
      { NAME: '铭诚制造', NUM: 7560000 },
      { NAME: '远景材料', NUM: 5430000 },
      { NAME: '德润贸易', NUM: 3910000 },
      { NAME: '宏达配套', NUM: 2680000 },
    ]
  }
  return [
    { NAME: '华东精密', NUM: 1280000 },
    { NAME: '星辰电子', NUM: 860000 },
    { NAME: '启航科技', NUM: 720000 },
    { NAME: '铭诚制造', NUM: 560000 },
    { NAME: '远景材料', NUM: 430000 },
    { NAME: '德润贸易', NUM: 310000 },
    { NAME: '宏达配套', NUM: 180000 },
  ]
}
const setMockData = (type) => {
  dataArr.value = getMockListByType(type).map(normalizeItem)
}
const fetchCustomerRanking = () => {
  customerContributionRanking({ type: dateType.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
          NAME: item.customerName,
          NUM: item.totalAmount
        }))
      } else {
        setMockData(dateType.value)
      }
    })
    .catch((error) => {
      console.error('获取客户金额贡献排名失败:', error)
      setMockData(dateType.value)
    })
}
const handleDateTypeChange = () => {
  fetchCustomerRanking()
}
onMounted(() => {
  fetchCustomerRanking()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 449px;
}
.switch-container {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/right-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,253 @@
<template>
  <div>
    <PanelHeader title="供应商采购排名" />
    <div class="panel-item-customers">
      <div class="switch-container">
        <DateTypeSwitch v-model="radio1" @change="handleDateTypeChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :series="series"
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="{ backgroundColor: 'transparent', textStyle: { color: BI.text } }"
        style="height: 360px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
import { supplierPurchaseRanking } from '@/api/viewIndex.js'
import { BI, withTooltip } from '../theme.js'
const chartStyle = {
  width: '100%',
  height: '100%',
}
const radio1 = ref(3)
// åŽŸå§‹æ•°æ®
const dataArr = ref([])
// æŽ’序后的数据(升序:值越大越靠上)
const dataArray = computed(() => {
  return [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
})
// è®¡ç®—总数
const total = computed(() => {
  return dataArray.value.reduce((sum, v) => sum + Number(v.NUM), 0)
})
// x轴数据(名称)
const xdataName = computed(() => {
  return dataArray.value.map((v) => v.NAME)
})
// æŽ’名配色:Top1 é‡‘色,Top2-3 äº®è“ï¼Œå…¶ä½™æš—蓝
const rankColor = (rank) => {
  if (rank === 1) return '#F0B14A'
  if (rank <= 3) return BI.accentBright
  return '#2C4A9E'
}
const getRank = (i) => dataArray.value.length - i
// y轴数据(数值,带样式)
const dataNum = computed(() => {
  return dataArray.value.map((v, i) => {
    const rank = getRank(i)
    return {
      value: Number(v.NUM),
      itemStyle: {
        color: rankColor(rank),
        barBorderRadius: [0, 10, 10, 0],
      },
    }
  })
})
// èƒŒæ™¯è½¨é“数据
const bgData = computed(() => {
  const maxValue = dataNum.value.length ? Math.max(...dataNum.value.map((v) => v.value)) : 0
  return dataNum.value.map(() => maxValue * 1.08 || 1)
})
// tooltip
const tooltip = withTooltip({
  trigger: 'axis',
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    result += `<div>${params[0].marker}${params[0].value}元</div>`
    return result
  },
})
// grid
const grid = computed(() => {
  return { top: 0, left: '20%', right: '10%', bottom: 0 }
})
// xAxis
const xAxis = computed(() => {
  return [
    {
      splitLine: { show: false },
      axisLine: { show: false },
      axisLabel: { show: false },
      axisTick: { show: false },
    },
  ]
})
// yAxis
const yAxis = computed(() => {
  return [
    {
      type: 'category',
      inverse: false,
      data: xdataName.value,
      axisLabel: {
        formatter: (value) => {
          if (!value) return ''
          const maxLen = 6 // æ¯è¡Œæœ€å¤šå­—符数,可按需调整
          if (value.length <= maxLen) return `{a|${value}}`
          const lines = []
          for (let i = 0; i < value.length; i += maxLen) {
            lines.push(value.slice(i, i + maxLen))
          }
          // å¤šè¡Œæ–‡æœ¬ï¼Œæ¯è¡Œéƒ½å¥—同一个 rich æ ·å¼
          return lines.map((line) => `{a|${line}}`).join('\n')
        },
        rich: {
          a: {
            width: 120,
            fontSize: 14,
            color: BI.textStrong,
            padding: [5, 4, 5, 0],
            align: 'right',
          },
        },
      },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { show: false },
    },
    {
      type: 'category',
      data: dataNum.value.map((item) => item.value),
      axisLabel: {
        formatter: (params, index) => {
          const value = typeof params === 'object' ? params.value : params
          const percent = total.value ? ((value / total.value) * 100).toFixed(0) : 0
          const rank = getRank(index)
          return `{a${rank}|${percent} }{b${rank}|%}`
        },
        rich: dataNum.value.reduce((rich, _, i) => {
          const rank = getRank(i)
          const color = rank === 1 ? '#F0B14A' : rank <= 3 ? BI.accentBright : BI.text
          rich[`a${rank}`] = { fontSize: 16, color, verticalAlign: 'bottom' }
          rich[`b${rank}`] = { fontSize: 12, color, verticalAlign: 'bottom' }
          return rich
        }, {}),
      },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { show: false },
    },
  ]
})
// series:背景轨道 + å®žè‰²æ¸å˜æ¡
const series = computed(() => [
  {
    name: '背景',
    z: 5,
    type: 'bar',
    barWidth: 18,
    tooltip: { show: false },
    itemStyle: {
      color: 'rgba(143, 163, 200, 0.12)',
      barBorderRadius: [0, 10, 10, 0],
    },
    data: bgData.value,
  },
  {
    name: '金额',
    z: 6,
    type: 'bar',
    barWidth: 18,
    barGap: '-100%',
    data: dataNum.value,
  },
])
// ä¾›åº”商采购排名
const fetchSupplierRanking = () => {
  supplierPurchaseRanking({ type: radio1.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
          NAME: item.supplierName,
          NUM: item.totalAmount
        }))
      } else {
        // å¦‚果没有数据,使用模拟数据
        dataArr.value = [
          { NAME: '供应商A', NUM: 102 },
          { NAME: '供应商B', NUM: 122 },
          { NAME: '供应商C', NUM: 282 },
          { NAME: '供应商D', NUM: 453 },
          { NAME: '供应商E', NUM: 753 },
        ]
      }
    })
    .catch((error) => {
      console.error('获取供应商采购排名失败:', error)
      // ä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®
      dataArr.value = [
        { NAME: '供应商A', NUM: 102 },
        { NAME: '供应商B', NUM: 122 },
        { NAME: '供应商C', NUM: 282 },
        { NAME: '供应商D', NUM: 453 },
        { NAME: '供应商E', NUM: 753 },
      ]
    })
}
// å¤„理日期类型切换
const handleDateTypeChange = () => {
  fetchSupplierRanking()
}
onMounted(() => {
  fetchSupplierRanking()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 449px;
}
.switch-container {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/index.vue
@@ -13,7 +13,9 @@
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <div class="dashboard-header">
      <div class="header-line"></div>
      <div class="factory-name">PSI æ•°æ®åˆ†æž</div>
      <div class="header-line header-line-right"></div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
@@ -47,8 +49,8 @@
import autofit from 'autofit.js'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
import RightTop from '../dataDashboard/components/basic/right-top.vue'
import RightBottom from '../dataDashboard/components/basic/right-bottom.vue'
import RightTop from './components/right-top.vue'
import RightBottom from './components/right-bottom.vue'
import useUserStore from '@/store/modules/user'
import LeftTop from './components/left-top.vue'
import CenterTop from './components/center-top.vue'
@@ -163,7 +165,7 @@
position: relative;
width: 100%;
/* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
height: calc(100vh - 84px);
height: calc(100vh - 100px);
display: flex;
align-items: center;
justify-content: center;
@@ -176,11 +178,34 @@
position: relative;
width: 1920px;
height: 1080px;
background-image: url("@/assets/BI/backImage@2x.png");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform-origin: center center;
/* å¤§å±ä¸»é¢˜ä»¤ç‰Œï¼Œå­ç»„件通过 CSS å˜é‡ç»§æ‰¿ */
--bi-panel-bg: rgba(20, 32, 66, 0.55);
--bi-panel-border: #1e3160;
--bi-text: #8fa3c8;
--bi-text-strong: #eaf1ff;
--bi-accent: #2c51d9;
--bi-accent-bright: #4a6cf0;
--bi-up: #34c77b;
--bi-down: #ff6b6b;
background:
  radial-gradient(1200px 600px at 50% -10%, rgba(44, 81, 217, 0.18) 0%, transparent 60%),
  linear-gradient(180deg, #0a1229 0%, #0d1834 55%, #0a1430 100%);
}
/* æžæ·¡ç½‘格纹理,替代模板感底图 */
.data-dashboard::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image:
  linear-gradient(rgba(74, 108, 240, 0.045) 1px, transparent 1px),
  linear-gradient(90deg, rgba(74, 108, 240, 0.045) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.35) 100%);
}
/* å…¨å±çŠ¶æ€çš„æ ·å¼ - ä½œç”¨äºŽscale-container */
@@ -218,20 +243,42 @@
position: relative;
z-index: 1;
height: 86px;
background-image: url("@/assets/BI/biaoti.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
gap: 28px;
}
/* æ ‡é¢˜ä¸¤ä¾§æ¸éšåˆ†éš”线 */
.header-line {
width: 420px;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(74, 108, 240, 0.5) 100%);
}
.header-line-right {
background: linear-gradient(270deg, transparent 0%, rgba(74, 108, 240, 0.5) 100%);
}
.factory-name {
font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
font-size: 30px;
letter-spacing: 4px;
color: var(--bi-text-strong);
position: relative;
white-space: nowrap;
}
.factory-name::after {
content: '';
position: absolute;
left: 50%;
bottom: -8px;
transform: translateX(-50%);
width: 48px;
height: 2px;
background: var(--bi-accent-bright);
border-radius: 1px;
}
.fullscreen-btn {
@@ -240,21 +287,21 @@
left: 20px;
width: 40px;
height: 40px;
background: rgba(0, 20, 60, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
background: var(--bi-panel-bg);
border: 1px solid var(--bi-panel-border);
border-radius: 6px;
color: #00d4ff;
color: var(--bi-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
transition: color 0.2s ease, border-color 0.2s ease;
z-index: 10000;
}
.fullscreen-btn:hover {
background: rgba(0, 30, 90, 0.9);
border-color: rgba(0, 212, 255, 0.5);
color: var(--bi-text-strong);
border-color: var(--bi-accent-bright);
}
.dashboard-content {
src/views/reportAnalysis/PSIDataAnalysis/theme.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
// PSI å¤§å±ç»Ÿä¸€è§†è§‰ä»¤ç‰Œï¼ˆå“ç‰Œè“ #2c51d9 ä½“系的克制深色主题)
export const BI = {
  // å›¾è¡¨åºåˆ—色(克制专业,替代青蓝荧光)
  series: ['#4A6CF0', '#35C6D6', '#8E7CE6', '#F0B14A', '#2FB8A8', '#E86D6D'],
  text: '#8FA3C8',
  textStrong: '#EAF1FF',
  panelBg: 'rgba(20, 32, 66, 0.55)',
  panelBorder: '#1E3160',
  sliceBorder: '#0D1834',
  accent: '#2C51D9',
  accentBright: '#4A6CF0',
  up: '#34C77B',
  down: '#FF6B6B',
}
export const baseChartOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: BI.text },
}
// æ·±è‰²ç»Ÿä¸€ tooltip æ ·å¼ï¼Œä¿ç•™å„组件自定义 formatter
export const withTooltip = (tooltip = {}) => ({
  backgroundColor: 'rgba(13, 24, 52, 0.92)',
  borderColor: BI.panelBorder,
  borderWidth: 1,
  textStyle: { color: BI.textStrong, fontSize: 12 },
  ...tooltip,
})
// é‡‘额格式化:大数字折算为「万」,空值兜底
export const formatAmount = (val, unit = '') => {
  const num = Number(val)
  if (!Number.isFinite(num)) return `0${unit}`
  if (Math.abs(num) >= 10000) {
    return `${(num / 10000).toLocaleString('zh-CN', { maximumFractionDigits: 2 })}万${unit}`
  }
  return `${num.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}${unit}`
}
src/views/reportAnalysis/dataDashboard/components/DateTypeSwitch.vue
@@ -16,7 +16,7 @@
const props = defineProps({
  modelValue: {
    type: Number,
    default: 1, // é»˜è®¤é€‰ä¸­"周"
    default: 3, // é»˜è®¤é€‰ä¸­"季度"
  },
})
@@ -46,49 +46,49 @@
/* æœªé€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  background-color: rgba(20, 32, 66, 0.55);
  color: #8fa3c8;
  border-color: #1e3160;
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
  transition: color 0.2s ease, background-color 0.2s ease;
}
/* ç¬¬ä¸€ä¸ªæŒ‰é’®å·¦ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  border-top-left-radius: 6px;
  border-bottom-left-radius: 6px;
}
/* æœ€åŽä¸€ä¸ªæŒ‰é’®å³ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}
/* æŒ‰é’®ä¹‹é—´çš„分隔线 */
.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
  border-right: 1px solid #1e3160;
}
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ï¼šå“ç‰Œè“å®žè‰² */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  background: #2c51d9;
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  border-color: #2c51d9;
  box-shadow: none;
}
/* æ‚¬åœæ•ˆæžœ */
.date-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
  color: #eaf1ff;
}
/* é€‰ä¸­çŠ¶æ€æ‚¬åœ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  background: #4a6cf0;
  border-color: #4a6cf0;
  color: #ffffff;
}
</style>
src/views/reportAnalysis/dataDashboard/components/PanelHeader.vue
@@ -16,18 +16,27 @@
<style scoped>
.panel-header {
  background-image: url("@/assets/BI/kehuhetongback@2x.png");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 4px;
  height: 36px;
}
/* å“ç‰Œè“ç»†ç«–条,呼应 PSI å¤§å±æ ‡é¢˜æ ·å¼ */
.panel-header::before {
  content: '';
  width: 3px;
  height: 14px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--bi-accent-bright, #4a6cf0) 0%, var(--bi-accent, #2c51d9) 100%);
  flex-shrink: 0;
}
.panel-title {
  width: 100%;
  font-weight: 500;
  font-size: 16px;
  color: #D9ECFF;
  padding-left: 46px;
  line-height: 36px;
  color: var(--bi-text-strong, #eaf1ff);
  letter-spacing: 1px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue
@@ -3,7 +3,10 @@
    <PanelHeader title="人员分布" />
    <div class="main-panel panel-item-customers">
      <div class="pie-chart-wrapper">
        <div class="pie-background" :style="{ backgroundImage: `url(${roseBorderImg})` }"></div>
        <div class="pie-center">
          <span class="pie-center-value">{{ totalStaff }}</span>
          <span class="pie-center-label">合计(人)</span>
        </div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
@@ -12,8 +15,10 @@
          :tooltip="pieTooltip"
          :color="pieColors"
          :options="pieOptions"
          style="height: 320px"
        />
          style="height: 100%"
        >
          <div v-if="loaded && !pieDatas.length" class="chart-empty">暂无数据</div>
        </Echarts>
      </div>
    </div>
  </div>
@@ -24,128 +29,150 @@
import { deptStaffDistribution } from '@/api/viewIndex.js'
import PanelHeader from '../PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import roseBorderImg from '@/assets/BI/玫瑰图边框.png'
import { BI, baseChartOptions, withTooltip } from '../../theme.js'
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
 * @param {参数类型} array ä¼ å…¥çš„æ•°ç»„ [{a:"1",b:"2"},{a:"2",b:"3"}]
 * @param {参数类型} key  å±žæ€§å a
 * @return {返回类型说明}
 */
function array2obj(array, key) {
  const resObj = {}
  for (let i = 0; i < array.length; i++) {
    resObj[array[i][key]] = array[i]
  }
  return resObj
}
const echartsRef = ref(null)
const pieDatas = ref([])
const loaded = ref(false)
const pieColors = BI.series
// äººå‘˜æ€»æ•°
const totalStaff = computed(() =>
  pieDatas.value.reduce((sum, d) => sum + (Number(d.value) || 0), 0)
)
const chartStyle = {
  width: '100%',
  height: '100%',
}
const echartsRef = ref(null)
const pieDatas = ref([])
const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
const pieObjData = computed(() => {
  const obj = {}
  pieDatas.value.forEach((d) => {
    obj[d.name] = d
  })
  return obj
})
const pieObjData = computed(() => array2obj(pieDatas.value, 'name'))
// å›¾ä¾‹é…ç½®ï¼ˆå³ä¾§ç«–排)
const pieLegend = computed(() => {
  const data = pieDatas.value.map((d, idx) => ({
    name: d.name,
    icon: 'circle',
    textStyle: {
      fontSize: 18,
      fontSize: 14,
      color: pieColors[idx % pieColors.length],
    },
  }))
  return {
    show: data.length > 0,
    orient: 'vertical',
    top: 'center',
    left: '50%',
    itemGap: 30,
    data: data,
    formatter: function (name) {
    itemGap: 18,
    itemWidth: 8,
    itemHeight: 8,
    data,
    formatter(name) {
      const item = pieObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|人}{percent|${item.rate}}{unit|%}`
      const short = name.length > 6 ? `${name.slice(0, 6)}…` : name
      return `{title|${short}}{value|${item.value}}{unit|人}`
    },
    textStyle: {
      rich: {
        value: {
          color: '#43e8fc',
          fontSize: 18,
          fontWeight: 600,
          padding: [0, 10, 0, 30],
        title: {
          fontSize: 13,
          color: BI.text,
          width: 96,
          overflow: 'truncate',
        },
        unit: {
          color: '#82baff',
        value: {
          color: BI.textStrong,
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 50, 0, 0],
          padding: [0, 0, 0, 10],
        },
        percent: {
          color: '#43e8fc',
          fontSize: 18,
          fontWeight: 600,
          padding: [0, 10, 0, 0],
        },
        title: {
          fontSize: 18,
          padding: [0, 0, 0, 0],
        unit: {
          color: BI.text,
          fontSize: 12,
        },
      },
    },
  }
})
const pieTooltip = {
// æç¤ºæ¡†ï¼šäººæ•° + å æ¯”
const pieTooltip = withTooltip({
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c} ({d}%)',
}
  confine: true,
  formatter(params) {
    const d = params.data || {}
    const val = Number(d.value || 0)
    return `${params.marker}<b>${d.name || ''}</b>&nbsp;&nbsp;${val} äºº Â· ${params.percent}%`
  },
})
// åŒå±‚环形饼图
const pieSeries = computed(() => [
  {
    name: '人员分布',
    type: 'pie',
    radius: '70%',
    radius: ['48%', '68%'],
    center: ['20%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderWidth:5,
      borderColor: BI.sliceBorder,
      borderWidth: 2,
      borderRadius: 4,
    },
    label: {
      show: false
      show: false,
    },
    minAngle: 15,
    minAngle: 8,
    data: pieDatas.value,
    animationType: 'scale',
    animationEasing: 'elasticOut',
    animationDelay: function () {
      return Math.random() * 200
  },
  {
    // å†…圈轨道环
    type: 'pie',
    radius: ['40%', '43%'],
    center: ['20%', '50%'],
    silent: true,
    label: {
      show: false,
    },
    labelLine: {
      show: false,
    },
    itemStyle: {
      color: 'rgba(74, 108, 240, 0.22)',
    },
    data: [1],
  },
])
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
const pieOptions = baseChartOptions
const getDeptStaffDistribution = () => {
  deptStaffDistribution().then(res => {
    if (res.code === 200) {
      pieDatas.value = res.data.items.map(item => ({
  deptStaffDistribution()
    .then((res) => {
      const items = res.data?.items || []
      pieDatas.value = items.map((item) => ({
        name: item.name,
        value: parseInt(item.value),
        rate: item.rate
        value: Number(item.value) || 0,
        rate: item.rate,
      }))
    }
  }).catch(err => {
    console.error('获取部门人员分布数据失败:', err)
  })
    })
    .catch((err) => {
      console.error('获取部门人员分布数据失败:', err)
      pieDatas.value = []
    })
    .finally(() => {
      loaded.value = true
    })
}
onMounted(() => {
@@ -160,32 +187,60 @@
  gap: 20px;
}
/* 330 = ä¸­æ å‰©ä½™é«˜åº¦ï¼Œä¿è¯å·¦ä¸­å³ä¸‰æ åº•边对齐 */
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 370px;
  height: 330px;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
  height: 276px;
  background: transparent;
}
.pie-background {
.pie-center {
  position: absolute;
  left: 50%;
  left: 20%;
  top: 50%;
  transform: translate(-113%, -50%);
  width: 360px;
  height: 360px;
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  pointer-events: none;
  z-index: 2;
}
.pie-center-value {
  font-weight: 600;
  font-size: 22px;
  color: var(--bi-text-strong);
  font-variant-numeric: tabular-nums;
}
.pie-center-label {
  font-size: 12px;
  color: var(--bi-text);
}
.chart-empty {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 276px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--bi-text);
  font-size: 14px;
  letter-spacing: 1px;
  pointer-events: none;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
@@ -3,38 +3,60 @@
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-icon" style="color: #4a6cf0">
          <!-- å‘˜å·¥ -->
          <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <circle cx="12" cy="8" r="4" />
            <path d="M4 21v-1a8 8 0 0 1 16 0v1" />
          </svg>
        </div>
        <div class="card-content">
          <span class="card-label">员工总数</span>
          <span class="card-value">{{ totalStaff }}</span>
          <div class="card-compare" :class="compareClass(staffYoY)">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(staffYoY) }}</span>
            <span class="compare-icon">{{ staffYoY >= 0 ? '↑' : '↓' }}</span>
            <span v-if="!isZero(staffYoY)" class="compare-icon">{{ staffYoY > 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-icon" style="color: #35c6d6">
          <!-- å®¢æˆ· -->
          <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <circle cx="9" cy="8" r="3.5" />
            <path d="M2.5 20v-1a6.5 6.5 0 0 1 13 0v1" />
            <path d="M16 5a3.5 3.5 0 0 1 0 6.5" />
            <path d="M17.5 13.5A6.5 6.5 0 0 1 21.5 20" />
          </svg>
        </div>
        <div class="card-content">
          <span class="card-label">客户总数</span>
          <span class="card-value">{{ totalCustomers }}</span>
          <div class="card-compare" :class="compareClass(customersYoY)">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(customersYoY) }}</span>
            <span class="compare-icon">{{ customersYoY >= 0 ? '↑' : '↓' }}</span>
            <span v-if="!isZero(customersYoY)" class="compare-icon">{{ customersYoY > 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-icon" style="color: #8e7ce6">
          <!-- ä¾›åº”商 -->
          <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <rect x="1.5" y="5" width="13" height="11" rx="1.5" />
            <path d="M14.5 9h4l3 3.5v3.5h-2" />
            <circle cx="6" cy="18.5" r="1.8" />
            <circle cx="17.5" cy="18.5" r="1.8" />
          </svg>
        </div>
        <div class="card-content">
          <span class="card-label">供应商总数</span>
          <span class="card-value">{{ totalSuppliers }}</span>
          <div class="card-compare" :class="compareClass(suppliersYoY)">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(suppliersYoY) }}</span>
            <span class="compare-icon">{{ suppliersYoY >= 0 ? '↑' : '↓' }}</span>
            <span v-if="!isZero(suppliersYoY)" class="compare-icon">{{ suppliersYoY > 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
@@ -42,13 +64,12 @@
    <!-- è®¾å¤‡ç»Ÿè®¡ -->
    <div class="equipment-stats">
      <div class="equipment-header">
        <img
          src="@/assets/BI/shujutongjiicon@2x.png"
          alt="图标"
          class="equipment-icon"
        />
        <span class="equipment-title">设备统计</span>
      <div class="section-header">
        <svg class="section-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
          <circle cx="12" cy="12" r="3" />
          <path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.55V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1.11-1.55 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.55-1.03H3a2 2 0 1 1 0-4h.09a1.7 1.7 0 0 0 1.55-1.11 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34h.08A1.7 1.7 0 0 0 10.31 3.1V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1.03 1.55 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87v.08a1.7 1.7 0 0 0 1.55 1.03H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.55 1.03z" />
        </svg>
        <span class="section-title">设备统计</span>
      </div>
      <div class="equipment-items">
        <div class="equipment-item">
@@ -70,40 +91,28 @@
      </div>
    </div>
    <!-- äº‹ä»¶åç§° -->
    <!-- å¾…办事件 -->
    <div class="event-info">
      <div class="event-header">
        <img
          src="@/assets/BI/shijianmingxiicon@2x.png"
          alt="图标"
          class="event-icon"
        />
        <span class="event-title">事件名称</span>
      <div class="section-header">
        <svg class="section-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
          <rect x="4" y="4" width="16" height="17" rx="2" />
          <path d="M9 2.5h6v3H9z" />
          <path d="M8.5 11h7M8.5 15.5h5" />
        </svg>
        <span class="section-title">事件名称</span>
      </div>
      <div class="event-content">
        <ul class="todo-list" v-if="todoList.length > 0" ref="refTodoList">
          <li v-for="item in todoList" :key="item.id">
            <div
              style="
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                width: 100%;
                gap: 20px;
              "
            >
            <div class="todo-division">待办事由:{{ item.approveReason }}</div>
              <div style="display: flex;justify-content: space-between;align-items: center;"
              >
                <div class="todo-title">申请类型:{{ item.approveTypeName }}</div>
                <div class="todo-division">申请部门:{{ item.approveDeptName }}</div>
                <div class="todo-time">{{ item.approveTime }}</div>
              </div>
            <div class="todo-row">
              <span class="todo-division todo-reason">待办事由:{{ item.approveReason || '-' }}</span>
              <span class="todo-title">申请类型:{{ item.approveTypeName || '-' }}</span>
              <span class="todo-division">申请部门:{{ item.approveDeptName || '-' }}</span>
              <span class="todo-time">{{ item.approveTime || '-' }}</span>
            </div>
          </li>
        </ul>
        <div v-else style="text-align: center">暂无数据</div>
        <div v-else class="event-empty">暂无数据</div>
      </div>
    </div>
  </div>
@@ -139,17 +148,25 @@
  return `${Math.abs(num).toFixed(2)}%`
}
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
const isZero = (val) => !(Number(val) || 0)
const compareClass = (val) => {
  const num = Number(val) || 0
  if (num > 0) return 'compare-up'
  if (num < 0) return 'compare-down'
  return 'compare-flat'
}
// èŽ·å–å‘˜å·¥ã€å®¢æˆ·ã€ä¾›åº”å•†æ•°é‡
const getNum = () => {
  summaryStatistics().then((res) => {
    totalStaff.value = res.data.totalStaff
    staffYoY.value = res.data.staffGrowthRate
    totalCustomers.value = res.data.totalCustomer
    customersYoY.value = res.data.customerGrowthRate
    totalSuppliers.value = res.data.totalSupplier
    suppliersYoY.value = res.data.supplierGrowthRate
    const d = res.data || {}
    totalStaff.value = Number(d.totalStaff) || 0
    staffYoY.value = Number(d.staffGrowthRate) || 0
    totalCustomers.value = Number(d.totalCustomer) || 0
    customersYoY.value = Number(d.customerGrowthRate) || 0
    totalSuppliers.value = Number(d.totalSupplier) || 0
    suppliersYoY.value = Number(d.supplierGrowthRate) || 0
  }).catch(err => {
    console.error('获取基础统计数据失败:', err)
  })
@@ -162,16 +179,16 @@
    pageSize: -1,
  }
  getLedgerPage(params).then((res) => {
    equipmentNum.value = res.data.total
    equipmentNum.value = Number(res.data?.total) || 0
  })
  getRepairPage({ ...params, status: 0 }).then((res) => {
    equipmentRepair.value = res.data.total
    equipmentRepair.value = Number(res.data?.total) || 0
  })
  getUpkeepPage({ ...params, status: 0 }).then((res) => {
    equipmentMaintain.value = res.data.total
    equipmentMaintain.value = Number(res.data?.total) || 0
  })
  measuringInstrumentListPage(params).then((res) => {
    totalMeasuring.value = res.data.total
    totalMeasuring.value = Number(res.data?.total) || 0
  })
}
@@ -260,11 +277,14 @@
// å¾…办事项
const todoInfoS = () => {
  homeTodos().then((res) => {
    todoList.value = res.data
    todoList.value = res.data || []
    // åœ¨èŽ·å–åˆ°å¾…åŠžäº‹é¡¹æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
    nextTick(() => {
      initTodoListScroll()
    })
  }).catch(err => {
    console.error('获取待办事项失败:', err)
    todoList.value = []
  })
}
@@ -300,38 +320,56 @@
  flex: 1;
  display: flex;
  align-items: center;
  background-image: url('@/assets/BI/border@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  background: var(--bi-panel-bg);
  border: 1px solid var(--bi-panel-border);
  border-radius: 8px;
  position: relative;
  overflow: hidden;
  height: 142px;
}
/* é¡¶éƒ¨å“ç‰Œè“äº®çº¿ï¼Œæ›¿ä»£å›¾ç‰‡æè¾¹ */
.stat-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--bi-accent) 0%, transparent 70%);
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
  width: 52px;
  height: 52px;
  margin: 0 16px 0 20px;
  border-radius: 8px;
  background: rgba(74, 108, 240, 0.12);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
  gap: 8px;
  min-width: 0;
}
.card-value {
  font-weight: 500;
  font-size: 40px;
  background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  font-weight: 600;
  font-size: 36px;
  color: var(--bi-text-strong);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.card-label {
  font-weight: 400;
  font-size: 19px;
  color: rgba(208, 231, 255, 0.7);
  font-size: 16px;
  color: var(--bi-text);
}
.card-compare {
@@ -339,16 +377,17 @@
  align-items: center;
  gap: 6px;
  font-size: 15px;
  color: #d0e7ff;
}
.card-compare > span:first-child {
  font-size: 13px;
  opacity: 0.8;
  color: var(--bi-text);
}
.compare-value {
  font-weight: 600;
  color: var(--bi-text);
  font-variant-numeric: tabular-nums;
}
.compare-icon {
@@ -359,163 +398,150 @@
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
  color: var(--bi-up, #34c77b);
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
  color: var(--bi-down, #ff6b6b);
}
/* æŒå¹³ï¼šä¸æ˜¾ç¤ºç®­å¤´ï¼Œé¢œè‰²ä¸­æ€§ */
.compare-flat .compare-value {
  color: var(--bi-text);
}
.equipment-stats,
.event-info {
  background: var(--bi-panel-bg);
  border: 1px solid var(--bi-panel-border);
  border-radius: 8px;
  padding: 14px 18px;
}
.equipment-stats {
  border: 1px solid #1a58b0;
  padding: 18px;
  height: 240px;
  padding-top: 0px;
  margin-top: 20px;
  box-sizing: border-box;
}
.equipment-header {
  font-weight: 500;
  font-size: 21px;
.event-info {
  height: 186px;
  margin-top: 20px;
  box-sizing: border-box;
}
.section-header {
  display: flex;
  border-bottom: 1px solid;
  border-image: linear-gradient(
      270deg,
      rgba(0, 126, 255, 0) 0%,
      rgba(0, 126, 255, 0.4549) 35%,
      #007eff 78%,
      #007eff 100%
    )
    1;
  padding-bottom: 2px;
  align-items: center;
  gap: 8px;
  color: var(--bi-accent-bright);
  border-bottom: 1px solid rgba(143, 163, 200, 0.15);
  padding-bottom: 10px;
}
.equipment-title {
.section-icon {
  flex-shrink: 0;
}
.section-title {
  font-weight: 500;
  font-size: 18px;
  background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  line-height: 50px;
}
.equipment-icon {
  width: 50px;
  height: 50px;
  font-size: 16px;
  color: var(--bi-text-strong);
  letter-spacing: 1px;
}
.equipment-items {
  display: flex;
  justify-content: space-around;
  gap: 30px;
  gap: 20px;
  padding-top: 22px;
}
.equipment-item {
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.equipment-value {
  display: block;
  font-weight: 500;
  font-size: 40px;
  color: #ffffff;
  width: 120px;
  height: 110px;
  line-height: 110px;
  background-image: url('@/assets/BI/shujutongji@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  margin-bottom: 8px;
  font-weight: 600;
  font-size: 36px;
  color: var(--bi-text-strong);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.equipment-label {
  font-weight: 500;
  font-size: 16px;
  color: #fffffe;
  font-weight: 400;
  font-size: 14px;
  color: var(--bi-text);
}
.event-info {
  background-image: url('@/assets/BI/shijianmingchengbeijing@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  padding: 20px;
  padding-top: 10px;
  height: 186px;
}
.event-header {
  display: flex;
  align-items: center;
}
.event-icon {
  width: 40px;
  height: 40px;
}
.event-title {
  font-weight: 500;
  font-size: 18px;
  color: #fffffe;
  line-height: 30px;
.event-content {
  padding-top: 8px;
}
.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
  height: 120px; /* æŒ‰ç”¨æˆ·è¦æ±‚调整高度 */
  height: 96px;
  overflow: hidden;
  font-size: 15px;
  font-size: 14px;
}
.todo-list li {
  border-radius: 8px;
  margin-bottom: 12px;
  padding: 12px 40px;
  height: 74px;
  margin-bottom: 8px;
}
.todo-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
  padding: 9px 14px;
  background: rgba(13, 24, 52, 0.6);
  border: 1px solid rgba(30, 49, 96, 0.8);
  border-radius: 6px;
}
.todo-title {
  font-weight: 400;
  font-size: 16px;
  color: #fffffe;
  position: relative;
  font-size: 14px;
  color: var(--bi-text);
  white-space: nowrap;
}
.todo-division {
  font-weight: 400;
  font-size: 16px;
  color: #fffffe;
  position: relative;
  font-size: 14px;
  color: var(--bi-text);
  white-space: nowrap;
}
.todo-division::before {
  content: '';
  position: absolute;
  left: -20px;
  top: 50%;
  transform: translateY(-50%);
  width: 6px;
  height: 6px;
  background: #498ceb;
  border-radius: 50%;
.todo-reason {
  color: var(--bi-text-strong);
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 1;
}
.todo-time {
  font-weight: 400;
  font-size: 16px;
  color: #fffffe;
  background: rgba(24, 93, 190, 0.4);
border-radius: 5px 5px 5px 5px;
padding: 5px 10px;
  font-size: 13px;
  color: var(--bi-text);
  background: rgba(74, 108, 240, 0.15);
  border-radius: 4px;
  padding: 3px 10px;
  font-variant-numeric: tabular-nums;
}
.event-empty {
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--bi-text);
  font-size: 14px;
  letter-spacing: 1px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue
@@ -32,22 +32,25 @@
          :tooltip="tooltip"
          :xAxis="xAxis1"
          :yAxis="yAxis1"
          :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
          style="height: 260px"
        />
          :options="{ backgroundColor: 'transparent', textStyle: { color: '#8FA3C8' } }"
          style="height: 370px"
        >
          <div v-if="loaded && !hasRevenueData" class="chart-empty">暂无营收数据</div>
        </Echarts>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { customerRevenueAnalysis } from '@/api/viewIndex.js'
import { listCustomer } from '@/api/basicData/customerFile.js'
import { BI, withTooltip } from '../../theme.js'
const dateType = ref(1) // 1=周 2=月 3=季度
const dateType = ref(3) // 1=周 2=月 3=季度,默认季度
const customerValue = ref(null)
const customerOptions = ref([])
@@ -58,8 +61,16 @@
const chartStyle = {
  width: '100%',
  height: '150%',
  height: '100%',
}
// æ˜¯å¦å·²æ‹¿åˆ°å“åº”(区分加载中与真的无数据)
const loaded = ref(false)
// æœ‰æ•°æ®æ‰æ˜¾ç¤ºæŸ±å­
const hasRevenueData = computed(
  () => (revenueData.value.items || []).some((i) => Number(i.value) > 0)
)
const grid = {
  left: '3%',
@@ -70,7 +81,7 @@
const barLegend = {
  show: false,
  textStyle: { color: '#B8C8E0' },
  textStyle: { color: BI.text },
  data: ['营收'],
}
@@ -90,9 +101,8 @@
        x2: 0,
        y2: 0,
        colorStops: [
          // linear-gradient(360deg, rgba(0,164,237,0) 0%, #4EE4FF 100%)
          { offset: 0, color: 'rgba(0,164,237,0)' },
          { offset: 1, color: '#4EE4FF' },
          { offset: 0, color: 'rgba(74, 108, 240, 0)' },
          { offset: 1, color: '#4A6CF0' },
        ],
      },
    },
@@ -100,7 +110,7 @@
  },
])
const tooltip = {
const tooltip = withTooltip({
  trigger: 'axis',
  axisPointer: {
    type: 'shadow',
@@ -108,17 +118,19 @@
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}</div>`
      const val = Number(item.value || 0).toLocaleString('zh-CN')
      result += `<div>${item.marker} ${item.seriesName}: ${val} å…ƒ</div>`
    })
    return result
  },
}
})
const xAxis1 = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    axisLine: { show: false },
    axisLabel: { color: BI.text, fontSize: 12 },
    data: [],
  },
])
@@ -126,7 +138,10 @@
const yAxis1 = [
  {
    type: 'value',
    axisLabel: { color: '#B8C8E0' },
    axisLabel: { color: BI.text, fontSize: 12 },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { lineStyle: { color: 'rgba(143, 163, 200, 0.15)' } },
  },
]
@@ -144,6 +159,7 @@
    type: dateType.value
  }
  loaded.value = false
  customerRevenueAnalysis(params)
    .then((res) => {
      xAxis1.value[0].data = []
@@ -152,12 +168,16 @@
      const items = res.data?.items || []
      items.forEach((item) => {
        xAxis1.value[0].data.push(item.name)
        barSeries1.value[0].data.push(item.value)
        barSeries1.value[0].data.push(Number(item.value) || 0)
      })
      revenueData.value = res.data
      revenueData.value = res.data || { items: [] }
    })
    .catch((error) => {
      console.error('获取客户营收分析失败:', error)
      revenueData.value = { items: [] }
    })
    .finally(() => {
      loaded.value = true
    })
}
@@ -219,30 +239,47 @@
/* ä¸‹æ‹‰æ¡†é£Žæ ¼ï¼šä¸Ž DateTypeSwitch ä¿æŒä¸€è‡´ï¼ˆæ·±è‰²åŠé€æ˜Žã€æµ…色文字、细边框) */
.customer-select :deep(.el-input__wrapper),
.customer-select :deep(.el-select__wrapper) {
  background-color: rgba(26, 88, 176, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.2);
  background-color: rgba(20, 32, 66, 0.55);
  border: 1px solid #1e3160;
  box-shadow: none;
}
.customer-select :deep(.el-input__inner) {
  color: rgba(184, 200, 224, 0.9);
  color: #8fa3c8;
}
.customer-select :deep(.el-input__inner::placeholder) {
  color: rgba(184, 200, 224, 0.6);
  color: rgba(143, 163, 200, 0.6);
}
.customer-select :deep(.el-select__caret),
.customer-select :deep(.el-icon) {
  color: rgba(184, 200, 224, 0.8);
  color: #8fa3c8;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 478px;
}
.chart-empty {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 370px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--bi-text);
  font-size: 14px;
  letter-spacing: 1px;
  pointer-events: none;
}
</style>
<style>
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
@@ -2,8 +2,11 @@
  <div>
    <PanelHeader title="产品大类" />
    <div class="panel-item-customers">
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
      <div class="pie-chart-wrapper">
        <div class="pie-center">
          <span class="pie-center-value">{{ totalCategories }}</span>
          <span class="pie-center-label">合计(ç±»)</span>
        </div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
@@ -14,143 +17,130 @@
          :options="pieOptions"
          style="height: 100%"
          class="land-chart"
        />
        >
          <div v-if="loaded && !dataList.length" class="chart-empty">暂无数据</div>
        </Echarts>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, onMounted, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import { productCategoryDistribution } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
import { BI, baseChartOptions, withTooltip } from '../../theme.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const chart = ref(null)
// æ•°æ®åˆ—表(来自接口)
const dataList = ref([])
const loaded = ref(false)
// é¢œè‰²åˆ—表
const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
const landColors = BI.series
// label å¯Œæ–‡æœ¬ï¼šä¸ºæ¯ä¸ªé¢œè‰²ç”Ÿæˆä¸€ä¸ªå°åœ†ç‚¹æ ·å¼ï¼ˆç¡®ä¿åœ¨ label ä¸­å¯è§ï¼‰
const dotRich = landColors.reduce((acc, color, idx) => {
  acc[`dot${idx}`] = {
    width: 8,
    height: 8,
    borderRadius: 8,
    backgroundColor: color,
    align: 'center',
  }
  return acc
}, {})
// å¤§ç±»æ€»æ•°ï¼ˆå„分类下的类目数之和)
const totalCategories = computed(() =>
  dataList.value.reduce((sum, d) => sum + (Number(d.value) || 0), 0)
)
const landObjData = computed(() => {
  const obj = {}
  dataList.value.forEach((d) => {
    obj[d.name] = d
  })
  return obj
})
// å›¾ä¾‹é…ç½®ï¼ˆå³ä¾§ç«–排)
const landLegend = {
  show: false,
  icon: 'circle',
  data: [],
  right: '8%',
  top: '40%',
  orient: 'vertical',
  itemGap: 14,
  itemWidth: 6,
  itemHeight: 6,
  textStyle: {
    fontSize: 12,
    rich: {
      unit: {
        color: '#fff',
        fontSize: 12,
        padding: [0, 10, 0, 0],
      },
      text: {
        width: 60,
        color: '#fff',
        fontSize: 12,
const landLegend = computed(() => {
  const data = dataList.value.map((d, idx) => ({
    name: d.name,
    icon: 'circle',
    textStyle: {
      fontSize: 14,
      color: landColors[idx % landColors.length],
    },
  }))
  return {
    show: data.length > 0,
    orient: 'vertical',
    top: 'center',
    left: '50%',
    itemGap: 20,
    itemWidth: 8,
    itemHeight: 8,
    data,
    formatter(name) {
      const item = landObjData.value[name]
      if (!item) return name
      const short = name.length > 8 ? `${name.slice(0, 8)}…` : name
      return `{title|${short}}{value|${item.value}}{unit|ç±»}`
    },
    textStyle: {
      rich: {
        title: {
          fontSize: 13,
          color: BI.text,
          width: 108,
          overflow: 'truncate',
        },
        value: {
          color: BI.textStrong,
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 10],
        },
        unit: {
          color: BI.text,
          fontSize: 12,
        },
      },
    },
  },
  formatter: function (name) {
    const list = dataList.value || []
    const item = list.find((d) => d.name === name)
    if (!item) return name
    const val = Number(item.value || 0)
    const totalValue = list.reduce((sum, it) => sum + Number(it.value || 0), 0)
    const percent = totalValue ? ((val / totalValue) * 100).toFixed(2) : '0.00'
    return `{text|${name}}${val}{unit| å…¬é¡·}${percent}{unit|%}`
  },
}
  }
})
// æç¤ºæ¡†
const landTooltip = {
  // triggerOn: 'hover',
  alwaysShowContent: true,
  position: function (pt) {
    return [pt[0], 130]
// æç¤ºæ¡†ï¼šå æ¯” + å­åˆ†ç±»æ˜Žç»†
const landTooltip = withTooltip({
  trigger: 'item',
  confine: true,
  formatter(params) {
    const d = params.data || {}
    const val = Number(d.value || 0)
    let html = `${params.marker}<b>${d.name || ''}</b>&nbsp;&nbsp;${val} ç±» Â· ${params.percent}%`
    const children = Array.isArray(d.children) ? d.children : []
    if (children.length) {
      const shown = children.slice(0, 8).map((c) => c.name).join('、')
      html += `<br/><span style="color:${BI.text}">子类:</span>${shown}${children.length > 8 ? '…' : ''}`
    }
    return html
  },
  formatter: function (params) {
    return `${params.name} (${params.value}ç±»)`
  },
}
})
// åŒå±‚环形饼图
const landSeries = ref([
const landSeries = computed(() => [
  {
    name: '产品大类',
    type: 'pie',
    radius: ['35%', '55%'],
    center: ['50%', '50%'],
    label: {
      show: true,
      position: 'outside',
      color: '#fff',
      fontSize: 12,
      lineHeight: 18,
      rich: {
        ...dotRich,
        parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20, overflow: 'break' },
        child: { fontSize: 12, color: '#fff', lineHeight: 18 },
      },
      formatter: function (params) {
        const children = params?.data?.children || []
        const parentName = params?.data?.name || ''
        const rawVal = params?.data?.value
        const parentValue = typeof rawVal === 'number' && !Number.isNaN(rawVal) ? rawVal : (Number(rawVal) || 0)
        const dotKey = `dot${(params?.dataIndex || 0) % landColors.length}`
        const dot = `{${dotKey}|} `
        const parentLine = `${dot}{parent|${parentName} (${parentValue}ç±»)}`
        if (!children.length) return parentLine
        // çˆ¶çº§å…¨éƒ¨æ˜¾ç¤ºï¼›å­çº§æœ€å¤š 5 ä¸ªï¼Œè¶…出显示省略号
        const displayed = children.slice(0, 5).map((c) => `{child|${c.name}}`)
        if (children.length > 5) displayed.push('{child|…}')
        return [parentLine, ...displayed].join('\n')
      },
    },
    labelLine: {
      show: true,
      length: 20,
      length2: 20,
      lineStyle: {
        color: '#B8C8E0',
      },
    },
    radius: ['42%', '58%'],
    center: ['25%', '50%'],
    itemStyle: {
      color: function (params) {
        return landColors[params.dataIndex % landColors.length]
      },
      borderColor: BI.sliceBorder,
      borderWidth: 2,
      borderRadius: 4,
    },
    label: {
      show: false,
    },
    minAngle: 8,
    data: dataList.value,
    animationType: 'scale',
    animationEasing: 'elasticOut',
  },
  {
    // å†…圈
    // å†…圈轨道环
    type: 'pie',
    radius: ['35%', '40%'],
    center: ['50%', '50%'],
    radius: ['33%', '36%'],
    center: ['25%', '50%'],
    silent: true,
    label: {
      show: false,
@@ -159,7 +149,7 @@
      show: false,
    },
    itemStyle: {
      color: 'rgba(0, 127, 255, 0.25)',
      color: 'rgba(74, 108, 240, 0.22)',
    },
    data: [1],
  },
@@ -167,22 +157,10 @@
const chartStyle = {
  width: '100%',
  height: '126%',
  height: '100%',
}
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
// ä½¿ç”¨å°è£…的背景位置调整方法,可自定义偏移值
const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  offsetX: '-51.5%', // X è½´åç§»ï¼Œå¯åŠ¨æ€è°ƒæ•´
  offsetY: '-39%',   // Y è½´åç§»ï¼Œå¯åŠ¨æ€è°ƒæ•´
  watchData: dataList // ç›‘听数据变化,自动调整位置
})
const pieOptions = baseChartOptions
const loadData = async () => {
  try {
@@ -194,32 +172,24 @@
      rate: it.rate,
      children: Array.isArray(it.children) ? it.children : [],
    }))
    landLegend.data = dataList.value.map((d) => d.name)
    landSeries.value[0].data = dataList.value
    // æ•°æ®åŠ è½½å®ŒæˆåŽè°ƒæ•´èƒŒæ™¯ä½ç½®
    adjustBackgroundPosition()
  } catch (e) {
    console.error('获取产品大类分布失败:', e)
    dataList.value = []
    landLegend.data = []
    landSeries.value[0].data = []
  } finally {
    loaded.value = true
  }
}
onMounted(() => {
  loadData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 420px;
@@ -228,23 +198,47 @@
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
  height: 384px;
  background: transparent;
}
.pie-background {
.pie-center {
  position: absolute;
  width: 360px;
  height: 360px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* é»˜è®¤å±…中,会在 JS ä¸­åŠ¨æ€è°ƒæ•´ */
  left: 50%;
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -39%);
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  pointer-events: none;
  z-index: 2;
}
.pie-center-value {
  font-weight: 600;
  font-size: 22px;
  color: var(--bi-text-strong);
  font-variant-numeric: tabular-nums;
}
.pie-center-label {
  font-size: 12px;
  color: var(--bi-text);
}
.chart-empty {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 384px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--bi-text);
  font-size: 14px;
  letter-spacing: 1px;
  pointer-events: none;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
@@ -14,7 +14,7 @@
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        :options="{ backgroundColor: 'transparent', textStyle: { color: BI.text } }"
        style="height: 360px"
      />
    </div>
@@ -27,25 +27,14 @@
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { customerContributionRanking } from '@/api/viewIndex.js'
import { BI, withTooltip } from '../../theme.js'
const chartStyle = {
  width: '100%',
  height: '100%',
}
const dateType = ref(1) // 1=周 2=月 3=季度
// é£žæœºå›¾æ ‡ SVG path(与 right-top ä¸€è‡´ï¼‰
const aircraft =
  'path://M107.000,71.000 C104.936,71.000 102.665,70.806 100.273,70.467 C94.592,76.922 86.275,81.000 77.000,81.000 C70.794,81.000 65.020,79.170 60.172,76.029 C66.952,74.165 72.647,69.714 76.173,63.817 C69.821,61.362 64.063,58.593 60.000,56.039 L60.000,52.813 C70.456,53.950 80.723,55.000 83.000,55.000 C88.972,55.000 93.000,53.723 93.000,50.000 C93.000,47.071 89.222,45.000 83.000,45.000 C80.723,45.000 70.456,46.050 60.000,47.187 L60.000,43.989 C64.057,41.431 69.807,38.644 76.168,36.173 C72.641,30.281 66.948,25.834 60.172,23.971 C65.020,20.830 70.794,19.000 77.000,19.000 C86.270,19.000 94.584,23.074 100.265,29.524 C102.647,29.191 104.918,29.000 107.000,29.000 C129.644,29.000 148.000,50.000 148.000,50.000 C148.000,50.000 129.644,71.000 107.000,71.000 ZM113.000,38.000 C106.373,38.000 101.000,43.373 101.000,50.000 C101.000,56.627 106.373,62.000 113.000,62.000 C119.627,62.000 125.000,56.627 125.000,50.000 C125.000,43.373 119.627,38.000 113.000,38.000 ZM113.000,56.000 C109.686,56.000 107.000,53.314 107.000,50.000 C107.000,46.686 109.686,44.000 113.000,44.000 C116.314,44.000 119.000,46.686 119.000,50.000 C119.000,53.314 116.314,56.000 113.000,56.000 ZM110.500,19.000 C109.567,19.000 108.763,18.483 108.334,17.726 C100.231,9.857 89.187,5.000 77.000,5.000 C64.813,5.000 53.769,9.857 45.666,17.726 C45.237,18.483 44.433,19.000 43.500,19.000 C42.119,19.000 41.000,17.881 41.000,16.500 C41.000,15.847 41.256,15.259 41.665,14.813 L41.575,14.718 C50.629,5.628 63.156,-0.000 77.000,-0.000 C90.844,-0.000 103.371,5.628 112.425,14.718 L112.335,14.813 C112.744,15.259 113.000,15.847 113.000,16.500 C113.000,17.881 111.881,19.000 110.500,19.000 ZM53.000,49.484 C61.406,48.626 77.810,47.000 81.345,47.000 C87.353,47.000 91.000,48.243 91.000,50.000 C91.000,52.234 87.111,53.000 81.345,53.000 C77.810,53.000 61.406,51.374 53.000,50.516 L53.000,49.484 ZM53.000,47.000 L9.000,50.000 L53.000,53.000 L53.000,56.000 L-0.000,50.000 L53.000,44.000 L53.000,47.000 ZM43.500,81.000 C44.433,81.000 45.237,81.517 45.666,82.274 C53.769,90.143 64.813,95.000 77.000,95.000 C89.187,95.000 100.231,90.143 108.334,82.274 C108.763,81.517 109.567,81.000 110.500,81.000 C111.881,81.000 113.000,82.119 113.000,83.500 C113.000,84.153 112.744,84.741 112.335,85.187 L112.425,85.282 C103.371,94.372 90.844,100.000 77.000,100.000 C63.156,100.000 50.629,94.372 41.575,85.282 L41.665,85.187 C41.256,84.741 41.000,84.153 41.000,83.500 C41.000,82.119 42.119,81.000 43.500,81.000 Z'
// é¢œè‰²é…ç½®ï¼ˆä¸Ž right-top ä¸€è‡´ï¼‰
const color = {
  0: '#ff5676',
  1: '#ffd83e',
  2: '#fbff94',
  3: '#7daeff',
}
const dateType = ref(3) // 1=周 2=月 3=季度,默认季度
// åŽŸå§‹æ•°æ®ï¼ˆç»Ÿä¸€æˆ { NAME, NUM })
const dataArr = ref([])
@@ -59,56 +48,41 @@
const xdataName = computed(() => dataArray.value.map((v) => v.NAME))
// æŽ’名配色:Top1 é‡‘色,Top2-3 äº®è“ï¼Œå…¶ä½™æš—蓝
const rankColor = (rank) => {
  if (rank === 1) return '#F0B14A'
  if (rank <= 3) return BI.accentBright
  return '#2C4A9E'
}
const getRank = (i) => dataArray.value.length - i
const dataNum = computed(() => {
  return dataArray.value.map((v, i) => {
    const index = dataArray.value.length - i - 1
    const isTop3 = index < 3
    const rank = getRank(i)
    return {
      value: Number(v.NUM),
      itemStyle: {
        color: {
          type: 'linear',
          x: 1,
          y: 0,
          x2: 0,
          y2: 0,
          colorStops: [
            { offset: 0, color: isTop3 ? '#ffdae1' : '#ecf3ff' },
            { offset: 0.07, color: isTop3 ? color[index] : color[3] },
            {
              offset: 1,
              color: isTop3 ? 'rgba(255, 86, 118, .1)' : 'rgba(125,174,255, .1)',
            },
          ],
          global: false,
        },
        barBorderRadius: [0, 20, 20, 0],
        color: rankColor(rank),
        barBorderRadius: [0, 10, 10, 0],
      },
      symbol: isTop3 ? aircraft : 'none',
      symbolPosition: 'end',
      symbolSize: [30, 25],
      symbolOffset: [35, 0],
    }
  })
})
const bgData = computed(() => {
  const maxValue = Math.max(0, ...dataNum.value.map((v) => v.value))
  return dataNum.value.map(() => maxValue + 200)
  const maxValue = dataNum.value.length ? Math.max(...dataNum.value.map((v) => v.value)) : 0
  return dataNum.value.map(() => maxValue * 1.08 || 1)
})
const tooltip = computed(() => ({
const tooltip = withTooltip({
  trigger: 'axis',
  textStyle: { fontSize: '100%' },
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}元</div>`
    })
    result += `<div>${params[0].marker}${Number(params[0].value || 0).toLocaleString('zh-CN')}元</div>`
    return result
  },
}))
})
const grid = computed(() => ({ top: 0, left: '20%', right: '10%', bottom: 0 }))
@@ -142,7 +116,7 @@
        a: {
          width: 120,
          fontSize: 14,
          color: '#fff',
          color: BI.textStrong,
          padding: [5, 4, 5, 0],
          align: 'right',
        },
@@ -159,21 +133,16 @@
      formatter: (params, index) => {
        const value = typeof params === 'object' ? params.value : params
        const percent = total.value ? ((value / total.value) * 100).toFixed(0) : 0
        const rank = dataArray.value.length - index
        const isTop3 = rank < 4
        return `{a${isTop3 ? rank : ''}|${percent} }{b${isTop3 ? rank : ''}|%}`
        const rank = getRank(index)
        return `{a${rank}|${percent} }{b${rank}|%}`
      },
      rich: {
        a: { fontSize: 18, color: '#98bfff', verticalAlign: 'bottom' },
        a1: { fontSize: 18, color: '#ff7f97', verticalAlign: 'bottom' },
        a2: { fontSize: 18, color: '#ffce64', verticalAlign: 'bottom' },
        a3: { fontSize: 18, color: '#e8ed66', verticalAlign: 'bottom' },
        b: { fontSize: 12, color: '#98bfff', verticalAlign: 'bottom' },
        b1: { fontSize: 12, color: '#ff7f97', verticalAlign: 'bottom' },
        b2: { fontSize: 12, color: '#ffce64', verticalAlign: 'bottom' },
        b3: { fontSize: 12, color: '#e8ed66', verticalAlign: 'bottom' },
      },
      rich: dataNum.value.reduce((rich, _, i) => {
        const rank = getRank(i)
        const color = rank === 1 ? '#F0B14A' : rank <= 3 ? BI.accentBright : BI.text
        rich[`a${rank}`] = { fontSize: 16, color, verticalAlign: 'bottom' }
        rich[`b${rank}`] = { fontSize: 12, color, verticalAlign: 'bottom' }
        return rich
      }, {}),
    },
    axisLine: { show: false },
    axisTick: { show: false },
@@ -181,47 +150,26 @@
  },
])
// series:背景轨道 + å®žè‰²æ¡
const series = computed(() => [
  {
    name: '金额',
    z: 6,
    type: 'pictorialBar',
    data: dataNum.value,
  },
  {
    name: '背景',
    z: 6,
    z: 5,
    type: 'bar',
    barWidth: 25,
    barWidth: 18,
    tooltip: { show: false },
    itemStyle: {
      color: 'rgba(255,255,255,.1)',
      barBorderRadius: [0, 20, 20, 0],
      color: 'rgba(143, 163, 200, 0.12)',
      barBorderRadius: [0, 10, 10, 0],
    },
    data: bgData.value,
  },
  {
    name: '金额渐变',
    name: '金额',
    z: 6,
    type: 'bar',
    barWidth: 25,
    barWidth: 18,
    barGap: '-100%',
    tooltip: { show: false },
    itemStyle: {
      color: {
        type: 'linear',
        x: 1,
        y: 0,
        x2: 0,
        y2: 0,
        colorStops: [
          { offset: 0, color: 'rgba(255, 218, 220)' },
          { offset: 0.07, color: 'rgba(255, 86, 118)' },
          { offset: 1, color: 'rgba(255, 86, 118, 0)' },
        ],
        global: false,
      },
      barBorderRadius: [0, 20, 20, 0],
    },
    data: dataNum.value,
  },
])
@@ -289,10 +237,10 @@
const fetchCustomerRanking = () => {
  customerContributionRanking({ type: dateType.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
      if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
        dataArr.value = res.data.map((item) => ({
          NAME: item.customerName,
          NUM: item.totalAmount
          NUM: Number(item.totalAmount) || 0,
        }))
      } else {
        setMockData(dateType.value)
@@ -315,7 +263,9 @@
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 449px;
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
@@ -13,7 +13,7 @@
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        :options="{ backgroundColor: 'transparent', textStyle: { color: BI.text } }"
        style="height: 360px"
      />
    </div>
@@ -26,30 +26,19 @@
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { supplierPurchaseRanking } from '@/api/viewIndex.js'
import { BI, withTooltip } from '../../theme.js'
const chartStyle = {
  width: '100%',
  height: '100%',
}
const radio1 = ref(1)
// é£žæœºå›¾æ ‡ SVG path
const aircraft =
  'path://M107.000,71.000 C104.936,71.000 102.665,70.806 100.273,70.467 C94.592,76.922 86.275,81.000 77.000,81.000 C70.794,81.000 65.020,79.170 60.172,76.029 C66.952,74.165 72.647,69.714 76.173,63.817 C69.821,61.362 64.063,58.593 60.000,56.039 L60.000,52.813 C70.456,53.950 80.723,55.000 83.000,55.000 C88.972,55.000 93.000,53.723 93.000,50.000 C93.000,47.071 89.222,45.000 83.000,45.000 C80.723,45.000 70.456,46.050 60.000,47.187 L60.000,43.989 C64.057,41.431 69.807,38.644 76.168,36.173 C72.641,30.281 66.948,25.834 60.172,23.971 C65.020,20.830 70.794,19.000 77.000,19.000 C86.270,19.000 94.584,23.074 100.265,29.524 C102.647,29.191 104.918,29.000 107.000,29.000 C129.644,29.000 148.000,50.000 148.000,50.000 C148.000,50.000 129.644,71.000 107.000,71.000 ZM113.000,38.000 C106.373,38.000 101.000,43.373 101.000,50.000 C101.000,56.627 106.373,62.000 113.000,62.000 C119.627,62.000 125.000,56.627 125.000,50.000 C125.000,43.373 119.627,38.000 113.000,38.000 ZM113.000,56.000 C109.686,56.000 107.000,53.314 107.000,50.000 C107.000,46.686 109.686,44.000 113.000,44.000 C116.314,44.000 119.000,46.686 119.000,50.000 C119.000,53.314 116.314,56.000 113.000,56.000 ZM110.500,19.000 C109.567,19.000 108.763,18.483 108.334,17.726 C100.231,9.857 89.187,5.000 77.000,5.000 C64.813,5.000 53.769,9.857 45.666,17.726 C45.237,18.483 44.433,19.000 43.500,19.000 C42.119,19.000 41.000,17.881 41.000,16.500 C41.000,15.847 41.256,15.259 41.665,14.813 L41.575,14.718 C50.629,5.628 63.156,-0.000 77.000,-0.000 C90.844,-0.000 103.371,5.628 112.425,14.718 L112.335,14.813 C112.744,15.259 113.000,15.847 113.000,16.500 C113.000,17.881 111.881,19.000 110.500,19.000 ZM53.000,49.484 C61.406,48.626 77.810,47.000 81.345,47.000 C87.353,47.000 91.000,48.243 91.000,50.000 C91.000,52.234 87.111,53.000 81.345,53.000 C77.810,53.000 61.406,51.374 53.000,50.516 L53.000,49.484 ZM53.000,47.000 L9.000,50.000 L53.000,53.000 L53.000,56.000 L-0.000,50.000 L53.000,44.000 L53.000,47.000 ZM43.500,81.000 C44.433,81.000 45.237,81.517 45.666,82.274 C53.769,90.143 64.813,95.000 77.000,95.000 C89.187,95.000 100.231,90.143 108.334,82.274 C108.763,81.517 109.567,81.000 110.500,81.000 C111.881,81.000 113.000,82.119 113.000,83.500 C113.000,84.153 112.744,84.741 112.335,85.187 L112.425,85.282 C103.371,94.372 90.844,100.000 77.000,100.000 C63.156,100.000 50.629,94.372 41.575,85.282 L41.665,85.187 C41.256,84.741 41.000,84.153 41.000,83.500 C41.000,82.119 42.119,81.000 43.500,81.000 Z'
// é¢œè‰²é…ç½®
const color = {
  0: '#ff5676',
  1: '#ffd83e',
  2: '#fbff94',
  3: '#7daeff',
}
const radio1 = ref(3) // 1=周 2=月 3=季度,默认季度
// åŽŸå§‹æ•°æ®
const dataArr = ref([])
// æŽ’序后的数据
// æŽ’序后的数据(升序:值越大越靠上)
const dataArray = computed(() => {
  return [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
})
@@ -64,66 +53,43 @@
  return dataArray.value.map((v) => v.NAME)
})
// æŽ’名配色:Top1 é‡‘色,Top2-3 äº®è“ï¼Œå…¶ä½™æš—蓝
const rankColor = (rank) => {
  if (rank === 1) return '#F0B14A'
  if (rank <= 3) return BI.accentBright
  return '#2C4A9E'
}
const getRank = (i) => dataArray.value.length - i
// y轴数据(数值,带样式)
const dataNum = computed(() => {
  return dataArray.value.map((v, i) => {
    const index = dataArray.value.length - i - 1
    const isTop3 = index < 3
    const rank = getRank(i)
    return {
      value: Number(v.NUM),
      itemStyle: {
        color: {
          type: 'linear',
          x: 1,
          y: 0,
          x2: 0,
          y2: 0,
          colorStops: [
            {
              offset: 0,
              color: isTop3 ? '#ffdae1' : '#ecf3ff',
            },
            {
              offset: 0.07,
              color: isTop3 ? color[index] : color[3],
            },
            {
              offset: 1,
              color: isTop3
                ? 'rgba(255, 86, 118, .1)'
                : 'rgba(125,174,255, .1)',
            },
          ],
          global: false,
        },
        barBorderRadius: [0, 20, 20, 0],
        color: rankColor(rank),
        barBorderRadius: [0, 10, 10, 0],
      },
      symbol: isTop3 ? aircraft : 'none',
      symbolPosition: 'end',
      symbolSize: [30, 25],
      symbolOffset: [35, 0],
    }
  })
})
// èƒŒæ™¯æ•°æ®
// èƒŒæ™¯è½¨é“数据
const bgData = computed(() => {
  const maxValue = Math.max(...dataNum.value.map((v) => v.value))
  return dataNum.value.map(() => maxValue + 200)
  const maxValue = dataNum.value.length ? Math.max(...dataNum.value.map((v) => v.value)) : 0
  return dataNum.value.map(() => maxValue * 1.08 || 1)
})
// tooltip
const tooltip = computed(() => {
  return {
    trigger: 'axis',
    textStyle: { fontSize: '100%' },
    formatter: function (params) {
      let result = params[0].axisValueLabel + '<br/>'
      result += `<div>${params[0].marker}${params[0].value}元</div>`
      return result
    },
  }
const tooltip = withTooltip({
  trigger: 'axis',
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    result += `<div>${params[0].marker}${Number(params[0].value || 0).toLocaleString('zh-CN')}元</div>`
    return result
  },
})
// grid
@@ -167,7 +133,7 @@
          a: {
            width: 120,
            fontSize: 14,
            color: '#fff',
            color: BI.textStrong,
            padding: [5, 4, 5, 0],
            align: 'right',
          },
@@ -183,54 +149,17 @@
      axisLabel: {
        formatter: (params, index) => {
          const value = typeof params === 'object' ? params.value : params
          const percent = ((value / total.value) * 100).toFixed(0)
          const rank = dataArray.value.length - index
          const isTop3 = rank < 4
          return `{a${isTop3 ? rank : ''}|${percent} }{b${isTop3 ? rank : ''}|%}`
          const percent = total.value ? ((value / total.value) * 100).toFixed(0) : 0
          const rank = getRank(index)
          return `{a${rank}|${percent} }{b${rank}|%}`
        },
        rich: {
          a: {
            fontSize: 18,
            color: '#98bfff',
            verticalAlign: 'bottom',
          },
          a1: {
            fontSize: 18,
            color: '#ff7f97',
            verticalAlign: 'bottom',
          },
          a2: {
            fontSize: 18,
            color: '#ffce64',
            verticalAlign: 'bottom',
          },
          a3: {
            fontSize: 18,
            color: '#e8ed66',
            verticalAlign: 'bottom',
          },
          b: {
            fontSize: 12,
            color: '#98bfff',
            verticalAlign: 'bottom',
          },
          b1: {
            fontSize: 12,
            color: '#ff7f97',
            verticalAlign: 'bottom',
          },
          b2: {
            fontSize: 12,
            color: '#ffce64',
            verticalAlign: 'bottom',
          },
          b3: {
            fontSize: 12,
            color: '#e8ed66',
            verticalAlign: 'bottom',
          },
        },
        rich: dataNum.value.reduce((rich, _, i) => {
          const rank = getRank(i)
          const color = rank === 1 ? '#F0B14A' : rank <= 3 ? BI.accentBright : BI.text
          rich[`a${rank}`] = { fontSize: 16, color, verticalAlign: 'bottom' }
          rich[`b${rank}`] = { fontSize: 12, color, verticalAlign: 'bottom' }
          return rich
        }, {}),
      },
      axisLine: { show: false },
      axisTick: { show: false },
@@ -239,71 +168,41 @@
  ]
})
// series
const series = computed(() => {
  return [
    {
      z: 6,
      type: 'pictorialBar',
      data: dataNum.value,
// series:背景轨道 + å®žè‰²æ¡
const series = computed(() => [
  {
    name: '背景',
    z: 5,
    type: 'bar',
    barWidth: 18,
    tooltip: { show: false },
    itemStyle: {
      color: 'rgba(143, 163, 200, 0.12)',
      barBorderRadius: [0, 10, 10, 0],
    },
    {
      z: 6,
      type: 'bar',
      barWidth: 25,
      tooltip: { show: false },
      itemStyle: {
        color: 'rgba(255,255,255,.1)',
        barBorderRadius: [0, 20, 20, 0],
      },
      data: bgData.value,
    },
    {
      type: 'bar',
      barWidth: 25,
      barGap: '-100%',
      tooltip: { show: false },
      itemStyle: {
        color: {
          type: 'linear',
          x: 1,
          y: 0,
          x2: 0,
          y2: 0,
          colorStops: [
            {
              offset: 0,
              color: 'rgba(255, 218, 220)',
            },
            {
              offset: 0.07,
              color: 'rgba(255, 86, 118)',
            },
            {
              offset: 1,
              color: 'rgba(255, 86, 118, 0)',
            },
          ],
          global: false,
        },
        barBorderRadius: [0, 20, 20, 0],
      },
      data: dataNum.value,
    },
  ]
})
    data: bgData.value,
  },
  {
    name: '金额',
    z: 6,
    type: 'bar',
    barWidth: 18,
    barGap: '-100%',
    data: dataNum.value,
  },
])
// ä¾›åº”商采购排名
const fetchSupplierRanking = () => {
  supplierPurchaseRanking({ type: radio1.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
      if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
        dataArr.value = res.data.map((item) => ({
          NAME: item.supplierName,
          NUM: item.totalAmount
          NUM: Number(item.totalAmount) || 0,
        }))
      } else {
        // å¦‚果没有数据,使用模拟数据
        // æŽ¥å£æ— æ•°æ®æ—¶ç»™ä¸€ç»„模拟供应商,保证UI可用
        dataArr.value = [
          { NAME: '供应商A', NUM: 102 },
          { NAME: '供应商B', NUM: 122 },
@@ -315,7 +214,6 @@
    })
    .catch((error) => {
      console.error('获取供应商采购排名失败:', error)
      // ä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®
      dataArr.value = [
        { NAME: '供应商A', NUM: 102 },
        { NAME: '供应商B', NUM: 122 },
@@ -327,7 +225,7 @@
}
// å¤„理日期类型切换
const handleDateTypeChange = (value) => {
const handleDateTypeChange = () => {
  fetchSupplierRanking()
}
@@ -338,7 +236,9 @@
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 449px;
@@ -348,11 +248,5 @@
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
}
.section-title {
  font-weight: 500;
  font-size: 16px;
  color: #d9ecff;
}
</style>
src/views/reportAnalysis/dataDashboard/index.vue
@@ -13,7 +13,9 @@
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <div class="dashboard-header">
      <div class="header-line"></div>
      <div class="factory-name">基础数据</div>
      <div class="header-line header-line-right"></div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
@@ -163,7 +165,7 @@
position: relative;
width: 100%;
/* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
height: calc(100vh - 84px);
height: calc(100vh - 100px);
display: flex;
align-items: center;
justify-content: center;
@@ -176,11 +178,34 @@
position: relative;
width: 1920px;
height: 1080px;
background-image: url("@/assets/BI/backImage@2x.png");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform-origin: center center;
/* å¤§å±ä¸»é¢˜ä»¤ç‰Œï¼Œå­ç»„件通过 CSS å˜é‡ç»§æ‰¿ */
--bi-panel-bg: rgba(20, 32, 66, 0.55);
--bi-panel-border: #1e3160;
--bi-text: #8fa3c8;
--bi-text-strong: #eaf1ff;
--bi-accent: #2c51d9;
--bi-accent-bright: #4a6cf0;
--bi-up: #34c77b;
--bi-down: #ff6b6b;
background:
  radial-gradient(1200px 600px at 50% -10%, rgba(44, 81, 217, 0.18) 0%, transparent 60%),
  linear-gradient(180deg, #0a1229 0%, #0d1834 55%, #0a1430 100%);
}
/* æžæ·¡ç½‘格纹理,替代模板感底图 */
.data-dashboard::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image:
  linear-gradient(rgba(74, 108, 240, 0.045) 1px, transparent 1px),
  linear-gradient(90deg, rgba(74, 108, 240, 0.045) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.35) 100%);
}
/* å…¨å±çŠ¶æ€çš„æ ·å¼ - ä½œç”¨äºŽscale-container */
@@ -218,20 +243,42 @@
position: relative;
z-index: 1;
height: 86px;
background-image: url("@/assets/BI/biaoti.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
gap: 28px;
}
/* æ ‡é¢˜ä¸¤ä¾§æ¸éšåˆ†éš”线 */
.header-line {
width: 420px;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(74, 108, 240, 0.5) 100%);
}
.header-line-right {
background: linear-gradient(270deg, transparent 0%, rgba(74, 108, 240, 0.5) 100%);
}
.factory-name {
font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
font-size: 30px;
letter-spacing: 4px;
color: var(--bi-text-strong);
position: relative;
white-space: nowrap;
}
.factory-name::after {
content: '';
position: absolute;
left: 50%;
bottom: -8px;
transform: translateX(-50%);
width: 48px;
height: 2px;
background: var(--bi-accent-bright);
border-radius: 1px;
}
.fullscreen-btn {
@@ -240,21 +287,21 @@
left: 20px;
width: 40px;
height: 40px;
background: rgba(0, 20, 60, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
background: var(--bi-panel-bg);
border: 1px solid var(--bi-panel-border);
border-radius: 6px;
color: #00d4ff;
color: var(--bi-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
transition: color 0.2s ease, border-color 0.2s ease;
z-index: 10000;
}
.fullscreen-btn:hover {
background: rgba(0, 30, 90, 0.9);
border-color: rgba(0, 212, 255, 0.5);
color: var(--bi-text-strong);
border-color: var(--bi-accent-bright);
}
.dashboard-content {
src/views/reportAnalysis/dataDashboard/index0.vue
@@ -1473,7 +1473,7 @@
  position: relative;
    width: 100%;
    /* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
    height: calc(100vh - 84px);
    height: calc(100vh - 100px);
  display: flex;
  align-items: center;
  justify-content: center;
src/views/reportAnalysis/dataDashboard/theme.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
// åŸºç¡€æ•°æ®å¤§å±ç»Ÿä¸€è§†è§‰ä»¤ç‰Œï¼ˆä¸Ž PSI å¤§å±åŒä¸€å¥—品牌蓝克制深色主题)
export const BI = {
  // å›¾è¡¨åºåˆ—色(克制专业,替代青蓝荧光)
  series: ['#4A6CF0', '#35C6D6', '#8E7CE6', '#F0B14A', '#2FB8A8', '#E86D6D'],
  text: '#8FA3C8',
  textStrong: '#EAF1FF',
  panelBg: 'rgba(20, 32, 66, 0.55)',
  panelBorder: '#1E3160',
  sliceBorder: '#0D1834',
  accent: '#2C51D9',
  accentBright: '#4A6CF0',
  up: '#34C77B',
  down: '#FF6B6B',
}
export const baseChartOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: BI.text },
}
// æ·±è‰²ç»Ÿä¸€ tooltip æ ·å¼ï¼Œä¿ç•™å„组件自定义 formatter
export const withTooltip = (tooltip = {}) => ({
  backgroundColor: 'rgba(13, 24, 52, 0.92)',
  borderColor: BI.panelBorder,
  borderWidth: 1,
  textStyle: { color: BI.textStrong, fontSize: 12 },
  ...tooltip,
})
// æ•°å€¼æ ¼å¼åŒ–:大数字折算为「万」,空值兜底
export const formatNumber = (val, unit = '') => {
  const num = Number(val)
  if (!Number.isFinite(num)) return `0${unit}`
  if (Math.abs(num) >= 10000) {
    return `${(num / 10000).toLocaleString('zh-CN', { maximumFractionDigits: 2 })}万${unit}`
  }
  return `${num.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}${unit}`
}
src/views/reportAnalysis/financialAnalysis/index.vue
@@ -163,7 +163,7 @@
position: relative;
width: 100%;
/* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
height: calc(100vh - 84px);
height: calc(100vh - 100px);
display: flex;
align-items: center;
justify-content: center;
src/views/reportAnalysis/productionAnalysis/components/CarouselCards.vue
@@ -1,14 +1,15 @@
<template>
  <div class="carousel-cards">
    <button
    <button
      v-if="canScrollLeft"
      class="nav-button nav-button-left"
      aria-label="向左滚动"
      @click="scrollLeftFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="左箭头" />
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
    </button>
    <div
      class="cards-container"
    <div
      class="cards-container"
      :style="{ '--visible-count': visibleCount }"
      ref="cardsContainerRef"
    >
@@ -17,7 +18,6 @@
        :key="index"
        class="card-item"
      >
        <div v-if="item.icon" class="card-icon" :style="{ backgroundImage: `url(${item.icon})` }"></div>
        <div class="card-title">
          <div class="card-label">{{ item.label }}</div>
          <div class="card-value">
@@ -30,12 +30,13 @@
        </div>
      </div>
    </div>
    <button
    <button
      v-if="canScrollRight"
      class="nav-button nav-button-right"
      aria-label="向右滚动"
      @click="scrollRightFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="右箭头" />
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
    </button>
  </div>
</template>
@@ -48,9 +49,9 @@
    type: Array,
    default: () => [],
    validator: (value) => {
      return value.every(item =>
        item && typeof item.label !== 'undefined' &&
        typeof item.value !== 'undefined' &&
      return value.every(item =>
        item && typeof item.label !== 'undefined' &&
        typeof item.value !== 'undefined' &&
        typeof item.unit !== 'undefined'
      )
    }
@@ -79,7 +80,7 @@
const updateScrollState = () => {
  const container = cardsContainerRef.value
  if (!container) return
  currentScrollLeft.value = container.scrollLeft
  maxScrollLeft.value = container.scrollWidth - container.clientWidth
}
@@ -88,19 +89,19 @@
const scrollLeftFn = () => {
  const container = cardsContainerRef.value
  if (!container) return
  const scrollItems = Array.from(container.querySelectorAll('.card-item'))
  if (scrollItems.length === 0) return
  const itemWidth = scrollItems[0]?.offsetWidth || 0
  const gap = 12
  const scrollDistance = itemWidth + gap
  container.scrollBy({
    left: -scrollDistance,
    behavior: 'smooth'
  })
  // å»¶è¿Ÿæ›´æ–°çŠ¶æ€ï¼Œç­‰å¾…æ»šåŠ¨åŠ¨ç”»å®Œæˆ
  setTimeout(() => {
    updateScrollState()
@@ -111,19 +112,19 @@
const scrollRightFn = () => {
  const container = cardsContainerRef.value
  if (!container) return
  const scrollItems = Array.from(container.querySelectorAll('.card-item'))
  if (scrollItems.length === 0) return
  const itemWidth = scrollItems[0]?.offsetWidth || 0
  const gap = 12
  const scrollDistance = itemWidth + gap
  container.scrollBy({
    left: scrollDistance,
    behavior: 'smooth'
  })
  // å»¶è¿Ÿæ›´æ–°çŠ¶æ€ï¼Œç­‰å¾…æ»šåŠ¨åŠ¨ç”»å®Œæˆ
  setTimeout(() => {
    updateScrollState()
@@ -188,40 +189,30 @@
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  background: rgba(26, 88, 176, 0.6);
  border: 1px solid rgba(26, 88, 176, 0.8);
  background: var(--bi-surface-2);
  border: 1px solid var(--bi-border);
  border-radius: 50%;
  color: var(--bi-text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 10;
  transition: all 0.3s ease;
  transition: color 0.2s ease, border-color 0.2s ease;
  padding: 0;
}
.nav-button:hover {
  background: rgba(26, 88, 176, 0.8);
  transform: translateY(-50%) scale(1.1);
  color: var(--bi-accent);
  border-color: var(--bi-accent);
}
.nav-button-left {
  left: -16px;
}
.nav-button-left img {
  width: 16px;
  height: 16px;
  transform: rotate(180deg);
}
.nav-button-right {
  right: -16px;
}
.nav-button-right img {
  width: 16px;
  height: 16px;
}
.card-item {
@@ -229,24 +220,15 @@
  min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
  display: flex;
  align-items: center;
  background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%);
  border-radius: 8px 8px 8px 8px;
  background: var(--bi-surface-2);
  border: 1px solid var(--bi-border);
  border-radius: 8px;
  padding: 12px 16px;
  transition: all 0.3s ease;
  transition: border-color 0.2s ease;
}
.card-item:hover {
  transform: translateY(-2px);
}
.card-icon {
  width: 80px;
  height: 60px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  flex-shrink: 0;
  margin-right: 12px;
  border-color: rgba(91, 140, 255, 0.45);
}
.card-title {
@@ -254,12 +236,13 @@
  align-items: flex-start;
  flex-direction: column;
  flex: 1;
  min-width: 0;
}
.card-label {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  color: var(--bi-text-secondary);
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
@@ -280,7 +263,7 @@
  gap: 6px;
  font-weight: 400;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.85);
  color: var(--bi-text-secondary);
}
.rate-label {
@@ -292,15 +275,17 @@
}
.value-number {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  line-height: 1;
  font-weight: 600;
  font-size: 22px;
  color: var(--bi-text);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum';
}
.value-unit {
  font-size: 14px;
  color: #FFFFFF;
  font-size: 12px;
  color: var(--bi-text-secondary);
  font-weight: 400;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/DateTypeSwitch.vue
@@ -16,7 +16,7 @@
const props = defineProps({
  modelValue: {
    type: Number,
    default: 1, // é»˜è®¤é€‰ä¸­"周"
    default: 3, // é»˜è®¤é€‰ä¸­"季度"
  },
})
@@ -46,13 +46,13 @@
/* æœªé€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  background-color: var(--bi-surface);
  color: var(--bi-text-secondary);
  border-color: var(--bi-border);
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
/* ç¬¬ä¸€ä¸ªæŒ‰é’®å·¦ä¾§åœ†è§’ */
@@ -69,26 +69,26 @@
/* æŒ‰é’®ä¹‹é—´çš„分隔线 */
.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
  border-right: 1px solid var(--bi-border);
}
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ï¼šå“ç‰Œè“çº¯è‰² */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  background-color: var(--bi-primary);
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  border-color: var(--bi-primary);
  box-shadow: none;
}
/* æ‚¬åœæ•ˆæžœ */
.date-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
  color: var(--bi-text);
  border-color: var(--bi-accent);
}
/* é€‰ä¸­çŠ¶æ€æ‚¬åœ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  background-color: var(--app-primary-hover);
  color: #ffffff;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/PanelHeader.vue
@@ -1,7 +1,5 @@
<template>
  <div class="panel-header">
    <span class="panel-title">{{ title }}</span>
  </div>
  <div class="panel-header bi-title-bar">{{ title }}</div>
</template>
<script setup>
@@ -13,21 +11,3 @@
  }
})
</script>
<style scoped>
.panel-header {
  background-image: url("@/assets/BI/kehuhetongback@2x.png");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}
.panel-title {
  width: 100%;
  font-weight: 500;
  font-size: 16px;
  color: #D9ECFF;
  padding-left: 46px;
  line-height: 36px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/ProductTypeSwitch.vue
@@ -43,13 +43,13 @@
}
.product-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  background-color: var(--bi-surface);
  color: var(--bi-text-secondary);
  border-color: var(--bi-border);
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
  transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.product-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
@@ -63,23 +63,23 @@
}
.product-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
  border-right: 1px solid var(--bi-border);
}
.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  background-color: var(--bi-primary);
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  border-color: var(--bi-primary);
  box-shadow: none;
}
.product-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
  color: var(--bi-text);
  border-color: var(--bi-accent);
}
.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  background-color: var(--app-primary-hover);
  color: #ffffff;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue
@@ -2,7 +2,7 @@
  <div>
    <PanelHeader title="生产订单完成进度" />
    <div class="main-panel">
      <div class="panel-item-customers">
      <div class="panel-item-customers bi-panel">
        <CarouselCards :items="cardItems" :visible-count="4" />
        <div
          class="progress-table-container"
@@ -55,6 +55,7 @@
import { getProgressStatistics } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import { BI_COLORS } from '../theme.js'
const progressTableRef = ref(null)
const progressTableScrollTimer = ref(null)
@@ -92,10 +93,10 @@
// æ ¹æ®è¿›åº¦ç™¾åˆ†æ¯”返回颜色
const progressColor = (percentage) => {
  const p = percentage || 0
  if (p < 30) return '#f56c6c'
  if (p < 50) return '#e6a23c'
  if (p < 80) return '#409eff'
  return '#67c23a'
  if (p < 30) return BI_COLORS.red
  if (p < 50) return BI_COLORS.amber
  if (p < 80) return BI_COLORS.primary
  return BI_COLORS.green
}
const setRowRef = (el, index) => {
@@ -248,17 +249,19 @@
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 428px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.progress-table-container {
  height: 320px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  margin-top: 10px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
@@ -270,7 +273,7 @@
.progress-table {
  width: 100%;
  border-collapse: collapse;
  color: #b8c8e0;
  color: var(--bi-text-secondary);
  font-size: 12px;
  table-layout: fixed;
}
@@ -278,7 +281,7 @@
.progress-table thead {
  position: sticky;
  top: 0;
  background-color: rgba(26, 88, 176, 0.9);
  background-color: var(--bi-surface-2);
  z-index: 10;
}
@@ -286,8 +289,8 @@
  padding: 8px 6px;
  text-align: left;
  font-weight: 500;
  border-bottom: 1px solid rgba(184, 200, 224, 0.3);
  color: #b8c8e0;
  border-bottom: 1px solid var(--bi-divider);
  color: var(--bi-text);
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
@@ -320,7 +323,7 @@
.progress-table td {
  padding: 8px 6px;
  border-bottom: 1px solid rgba(184, 200, 224, 0.1);
  border-bottom: 1px solid var(--bi-divider);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
@@ -329,7 +332,7 @@
}
.progress-table tbody tr:hover {
  background-color: rgba(184, 200, 224, 0.1);
  background-color: rgba(159, 176, 207, 0.06);
}
.progress-table tbody tr.row-under-header {
@@ -341,11 +344,11 @@
}
.progress-table :deep(.el-progress-bar__outer) {
  background-color: rgba(184, 200, 224, 0.2);
  background-color: rgba(159, 176, 207, 0.15);
}
.progress-table :deep(.el-progress__text) {
  color: #b8c8e0;
  color: var(--bi-text-secondary);
  font-size: 11px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/center-center.vue
@@ -1,15 +1,8 @@
<template>
  <div>
    <!-- è®¾å¤‡ç»Ÿè®¡ -->
    <div class="equipment-stats">
      <div class="equipment-header">
        <img
          src="@/assets/BI/shujutongjiicon@2x.png"
          alt="图标"
          class="equipment-icon"
        />
        <span class="equipment-title">投入产出分析</span>
      </div>
    <!-- æŠ•入产出分析 -->
    <PanelHeader title="投入产出分析" />
    <div class="bi-panel chart-box">
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
@@ -19,7 +12,7 @@
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        :options="{ backgroundColor: 'transparent', textStyle: { color: BI_COLORS.text } }"
        style="height: 260px"
      />
    </div>
@@ -28,9 +21,10 @@
<script setup>
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import { inputOutputAnalysis } from '@/api/viewIndex.js'
import { BI_COLORS, BI_TOOLTIP, areaGradient } from '../theme.js'
const chartStyle = { width: '100%', height: '100%' }
const grid = {
@@ -48,10 +42,10 @@
  itemGap: 24,
  itemWidth: 12,
  itemHeight: 12,
  textStyle: { color: '#B8C8E0', fontSize: 14 },
  textStyle: { color: BI_COLORS.text, fontSize: 14 },
  data: [
    { name: '产出', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
    { name: '投入', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
    { name: '产出', itemStyle: { color: BI_COLORS.primary } },
    { name: '投入', itemStyle: { color: BI_COLORS.teal } },
  ],
}
@@ -60,9 +54,9 @@
    type: 'category',
    data: [],
    axisTick: { show: false },
    axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } },
    axisLine: { lineStyle: { color: BI_COLORS.axisLine } },
    axisLabel: { color: BI_COLORS.text, fontSize: 12 },
    splitLine: { show: false },
  },
])
@@ -70,11 +64,11 @@
  {
    type: 'value',
    name: '单位: ä»¶',
    nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 0] },
    nameTextStyle: { color: BI_COLORS.text, fontSize: 12 },
    axisLine: { show: false },
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    splitLine: { lineStyle: { color: '#B8C8E0' } },
    axisLabel: { color: BI_COLORS.text, fontSize: 12 },
    splitLine: { lineStyle: { color: BI_COLORS.splitLine } },
  },
]
@@ -82,36 +76,26 @@
  {
    name: '产出',
    type: 'line',
    smooth: false,
    smooth: true,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 },
    itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(11, 137, 254, 0.40)' },
        { offset: 1, color: 'rgba(11, 137, 254, 0.05)' },
      ]),
    },
    symbolSize: 6,
    lineStyle: { color: BI_COLORS.primary, width: 2 },
    itemStyle: { color: BI_COLORS.primary, borderWidth: 0 },
    areaStyle: { color: areaGradient(BI_COLORS.primary) },
    data: [],
    emphasis: { focus: 'series' },
  },
  {
    name: '投入',
    type: 'line',
    smooth: false,
    smooth: true,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 },
    itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(11, 249, 254, 0.5)' },
        { offset: 1, color: 'rgba(11, 249, 254, 0.05)' },
      ]),
    },
    symbolSize: 6,
    lineStyle: { color: BI_COLORS.teal, width: 2 },
    itemStyle: { color: BI_COLORS.teal, borderWidth: 0 },
    areaStyle: { color: areaGradient(BI_COLORS.teal) },
    data: [],
    emphasis: { focus: 'series' },
  },
@@ -120,8 +104,7 @@
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'line' },
  borderWidth: 1,
  textStyle: { fontSize: 12 },
  ...BI_TOOLTIP,
  formatter(params) {
    let result = params[0].axisValue + '<br/>'
    params.forEach((item) => {
@@ -152,42 +135,8 @@
</script>
<style scoped>
.equipment-stats {
  border: 1px solid #1a58b0;
  padding: 0 18px 18px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.equipment-header {
  font-weight: 500;
  font-size: 21px;
  display: flex;
  border-bottom: 1px solid;
  border-image: linear-gradient(
      270deg,
      rgba(0, 126, 255, 0) 0%,
      rgba(0, 126, 255, 0.4549) 35%,
      #007eff 78%,
      #007eff 100%
    )
    1;
  padding-bottom: 2px;
}
.equipment-title {
  font-weight: 500;
  font-size: 18px;
  background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  line-height: 50px;
}
.equipment-icon {
  width: 50px;
  height: 50px;
.chart-box {
  padding: 12px 18px 18px;
  margin-top: 4px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/center-top.vue
@@ -5,12 +5,11 @@
      <div
        v-for="item in statItems"
        :key="item.name"
        class="stat-card"
        class="stat-card bi-panel"
      >
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">{{ item.name }}</span>
          <span class="card-value">{{ item.value }}</span>
          <span class="card-value bi-num">{{ item.value }}</span>
          <div class="card-compare" :class="compareClass(Number(item.rate))">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(item.rate) }}</span>
@@ -18,6 +17,7 @@
          </div>
        </div>
      </div>
      <div v-if="!statItems.length" class="stats-empty bi-panel">暂无数据</div>
    </div>
  </div>
@@ -67,38 +67,41 @@
  flex: 1;
  display: flex;
  align-items: center;
  background-image: url('@/assets/BI/border@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  position: relative;
  height: 142px;
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
/* å·¦ä¾§å“ç‰Œè“ç«–条,呼应首页面板标题 */
.stat-card::before {
  content: '';
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 64px;
  border-radius: 2px;
  background: var(--bi-accent);
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-left: 36px;
}
.card-value {
  font-weight: 500;
  font-weight: 600;
  font-size: 40px;
  background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  line-height: 1.1;
  color: var(--bi-text);
}
.card-label {
  font-weight: 400;
  font-size: 16px;
  color: rgba(208, 231, 255, 0.7);
  color: var(--bi-text-secondary);
}
.card-compare {
@@ -106,7 +109,7 @@
  align-items: center;
  gap: 6px;
  font-size: 15px;
  color: #d0e7ff;
  color: var(--bi-text-secondary);
}
.card-compare > span:first-child {
@@ -121,17 +124,26 @@
.compare-icon {
  font-size: 14px;
  position: relative;
  top: -1px; /* è½»å¾®ä¸Šç§»ï¼Œè®©ç®­å¤´ä¸Žæ–‡å­—垂直居中对齐 */
  top: -1px;
}
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
  color: var(--bi-success);
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
  color: var(--bi-danger);
}
.stats-empty {
  flex: 1;
  height: 142px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--bi-text-tertiary);
  font-size: 14px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue
@@ -1,7 +1,7 @@
<template>
  <div>
    <PanelHeader title="在制品统计分析" />
    <div class="main-panel panel-item-customers">
    <div class="main-panel panel-item-customers bi-panel">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="chart-wrapper">
        <Echarts
@@ -13,7 +13,7 @@
          :tooltip="tooltip"
          :xAxis="workInProcessXAxis"
          :yAxis="workInProcessYAxis"
          :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
          :options="{ backgroundColor: 'transparent', textStyle: { color: BI_COLORS.text } }"
          style="height: 100%"
        />
      </div>
@@ -27,6 +27,7 @@
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import { getWorkInProcessTurnover } from '@/api/viewIndex.js'
import { BI_COLORS, BI_TOOLTIP, barGradient } from '../theme.js'
// åœ¨åˆ¶å“å‘¨è½¬ç»Ÿè®¡å¯¹è±¡
const workInProcessStatistics = ref({
@@ -53,10 +54,11 @@
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'shadow' },
  ...BI_TOOLTIP,
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}</div>`
    })
    return result
  },
@@ -67,46 +69,38 @@
  {
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    axisLabel: { color: BI_COLORS.text },
    data: [],
  },
])
const workInProcessYAxis = [
  {
    type: 'value',
    axisLabel: { color: '#B8C8E0' },
    axisLabel: { color: BI_COLORS.text },
    name: '',
    splitLine: { lineStyle: { color: BI_COLORS.splitLine } },
  },
]
const workInProcessBarLegend = {
  show: false,
  textStyle: { color: '#B8C8E0' },
  textStyle: { color: BI_COLORS.text },
  data: [],
}
const workInProcessBarSeries = ref([
  {
    name: '在制品数量',
    type: 'bar',
    barWidth: 25,
    barWidth: 20,
    barGap: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
        { offset: 1, color: 'rgba(0,164,237,0)' },
        { offset: 0, color: '#4EE4FF' },
        ],
      },
      borderRadius: [3, 3, 0, 0],
      color: barGradient(BI_COLORS.primary),
    },
    label: {
      show: true,
      position: 'top',
      color: '#B8C8E0',
      color: BI_COLORS.text,
    },
    data: [],
  },
@@ -156,7 +150,6 @@
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
src/views/reportAnalysis/productionAnalysis/components/left-top.vue
@@ -1,39 +1,43 @@
<template>
  <div>
    <PanelHeader title="工序产出分析" />
    <div class="main-panel panel-item-customers">
    <div class="panel-item-customers bi-panel">
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
          :legend="pieLegend"
          :series="pieSeries"
          :tooltip="pieTooltip"
          :color="pieColors"
          :options="pieOptions"
          style="height: 320px"
        />
      <div class="pie-chart-wrapper">
        <template v-if="hasData">
          <Echarts
            ref="echartsRef"
            :chartStyle="chartStyle"
            :legend="pieLegend"
            :series="pieSeries"
            :tooltip="pieTooltip"
            :color="pieColors"
            :options="pieOptions"
            style="height: 320px"
          />
          <!-- çŽ¯å¿ƒæ±‡æ€»ï¼Œæ›¿ä»£åŽŸçŽ«ç‘°å›¾è¾¹æ¡†è£…é¥°å›¾ -->
          <div class="pie-center">
            <span class="center-value">{{ totalValue }}</span>
            <span class="center-label">总产出(ä»¶)</span>
          </div>
        </template>
        <div v-else class="pie-empty">暂无数据</div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { processOutputAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import DateTypeSwitch from '@/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue'
import { useChartBackground } from '@/hooks/useChartBackground.js'
import DateTypeSwitch from './DateTypeSwitch.vue'
import { BI_COLORS, BI_PIE_PALETTE, BI_TOOLTIP } from '../theme.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const dateType = ref(1) // 1=周 2=月 3=季度
const dateType = ref(3) // 1=周 2=月 3=季度,默认季度
function array2obj(array, key) {
  const resObj = {}
@@ -50,9 +54,17 @@
const echartsRef = ref(null)
const pieDatas = ref([])
const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
const pieColors = BI_PIE_PALETTE
const pieObjData = computed(() => array2obj(pieDatas.value, 'name'))
// çŽ¯å¿ƒæ€»é‡ï¼ˆç©ºæ•°æ®å…œåº• 0)
const totalValue = computed(() =>
  pieDatas.value.reduce((sum, d) => sum + (Number(d.value) || 0), 0)
)
// å…¨é›¶æ•°æ®ä¸æ¸²æŸ“扇区:minAngle ä¼šæŠŠ 0 å€¼ä¹Ÿæ’‘成整环,造成误导
const hasData = computed(() => pieDatas.value.some((d) => Number(d.value) > 0))
const pieLegend = computed(() => {
  const data = pieDatas.value.map((d, idx) => ({
@@ -78,24 +90,25 @@
    textStyle: {
      rich: {
        value: {
          color: '#43e8fc',
          color: BI_COLORS.textStrong,
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 10],
        },
        unit: {
          color: '#82baff',
          color: BI_COLORS.text,
          fontSize: 12,
          fontWeight: 600,
          padding: [0, 10, 0, 0],
        },
        percent: {
          color: '#43e8fc',
          color: BI_COLORS.primary,
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 0],
        },
        title: {
          color: BI_COLORS.text,
          fontSize: 12,
          padding: [0, 0, 0, 0],
        },
@@ -107,47 +120,34 @@
const pieTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c}ä»¶ ({d}%)',
  ...BI_TOOLTIP,
}
const pieSeries = computed(() => [
  {
    name: '工序产出分析',
    type: 'pie',
    radius: '60%',
    center: ['25%', '50%'],
    radius: ['46%', '68%'],
    center: ['27%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderColor: BI_COLORS.surface,
      borderWidth: 2,
      borderRadius: 4,
    },
    label: {
      show: false
    },
    minAngle: 15,
    data: pieDatas.value,
    data: hasData.value ? pieDatas.value : [],
    animationType: 'scale',
    animationEasing: 'elasticOut',
    animationDelay: function () {
      return Math.random() * 200
    },
    animationEasing: 'cubicOut',
  },
])
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
  textStyle: { color: BI_COLORS.text },
}
// ä½¿ç”¨å°è£…的背景位置调整方法
// å›¾è¡¨ä¸­å¿ƒæ˜¯ ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // å›¾è¡¨ä¸­å¿ƒ X æ˜¯ 25%
  top: '50%',        // å›¾è¡¨ä¸­å¿ƒ Y æ˜¯ 50%
  offsetX: '-51.5%', // X è½´åç§»
  offsetY: '-50%',   // Y è½´åç§»
  watchData: pieDatas // ç›‘听数据变化,自动调整位置
})
const fetchData = () => {
  processOutputAnalysis({ dateType: dateType.value })
@@ -172,26 +172,17 @@
onMounted(() => {
  fetchData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.filters-row {
@@ -206,22 +197,41 @@
  position: relative;
  width: 100%;
  height: 320px;
  background: transparent;
}
.pie-background {
/* çŽ¯å¿ƒæ•°å­—ä¸Žé¥¼å›¾åœ†å¿ƒï¼ˆ27%,50%)对齐 */
.pie-center {
  position: absolute;
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* ä½ç½®ç”± JS åŠ¨æ€è®¾ç½®ï¼Œé»˜è®¤å±…ä¸­ */
  left: 25%;
  left: 27%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  pointer-events: none;
}
.center-value {
  font-weight: 600;
  font-size: 26px;
  line-height: 1;
  color: var(--bi-text);
  font-variant-numeric: tabular-nums;
}
.center-label {
  font-size: 12px;
  color: var(--bi-text-secondary);
}
.pie-empty {
  position: absolute;
  left: 27%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-size: 14px;
  color: var(--bi-text-tertiary);
  pointer-events: none;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue
@@ -1,7 +1,7 @@
<template>
  <div>
    <PanelHeader title="生产核算分析" />
    <div class="main-panel panel-item-customers">
    <div class="main-panel panel-item-customers bi-panel">
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
@@ -14,7 +14,7 @@
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        :options="{ backgroundColor: 'transparent', textStyle: { color: BI_COLORS.text } }"
        style="height: 260px"
      />
    </div>
@@ -27,19 +27,20 @@
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { BI_COLORS, BI_TOOLTIP, barGradient } from '../theme.js'
const dateType = ref(1)
const dateType = ref(3) // 1=周 2=月 3=季度,默认季度
const chartStyle = {
  width: '100%',
  height: '140%',
  height: '100%',
}
const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const barLegend = {
  show: true,
  textStyle: { color: '#B8C8E0' },
  textStyle: { color: BI_COLORS.text },
  data: ['完成数量', '工资金额', '合格率'],
}
@@ -51,17 +52,8 @@
    barGap: '40%',
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(0, 164, 237, 0)' },
          { offset: 0, color: 'rgba(78, 228, 255, 1)' },
        ],
      },
      borderRadius: [3, 3, 0, 0],
      color: barGradient(BI_COLORS.primary),
    },
    data: [],
  },
@@ -72,17 +64,8 @@
    barWidth: 20,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
          { offset: 0, color: 'rgba(144, 97, 248, 1)' },
        ],
      },
      borderRadius: [3, 3, 0, 0],
      color: barGradient(BI_COLORS.violet),
    },
    data: [],
  },
@@ -90,42 +73,44 @@
    name: '合格率',
    type: 'line',
    yAxisIndex: 1,
    smooth: true,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
    itemStyle: { color: 'rgba(90, 216, 166, 1)' },
    symbolSize: 6,
    lineStyle: { color: BI_COLORS.amber, width: 2 },
    itemStyle: { color: BI_COLORS.amber },
    data: [],
    emphasis: { focus: 'series' },
  },
])
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
  { type: 'category', axisTick: { show: false }, axisLine: { lineStyle: { color: BI_COLORS.axisLine } }, axisLabel: { color: BI_COLORS.text }, data: [] },
])
const yAxis1 = [
  {
    type: 'value',
    name: '数量/金额',
    axisLabel: { color: '#B8C8E0' },
    nameTextStyle: { color: '#B8C8E0' },
    // splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
    axisLabel: { color: BI_COLORS.text },
    nameTextStyle: { color: BI_COLORS.text },
    splitLine: { lineStyle: { color: BI_COLORS.splitLine } },
  },
  {
    type: 'value',
    name: '合格率(%)',
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
    nameTextStyle: { color: '#B8C8E0' },
    splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
    axisLabel: { color: BI_COLORS.text, formatter: '{value}%' },
    nameTextStyle: { color: BI_COLORS.text },
    splitLine: { show: false },
  },
]
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  ...BI_TOOLTIP,
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
@@ -178,7 +163,6 @@
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
src/views/reportAnalysis/productionAnalysis/components/right-top.vue
@@ -1,7 +1,7 @@
<template>
  <div>
    <PanelHeader title="工单执行效率分析" />
    <div class="main-panel panel-item-customers">
    <div class="main-panel panel-item-customers bi-panel">
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
@@ -14,7 +14,7 @@
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        :options="{ backgroundColor: 'transparent', textStyle: { color: BI_COLORS.text } }"
        style="height: 260px"
      />
    </div>
@@ -27,19 +27,20 @@
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
import { BI_COLORS, BI_TOOLTIP, barGradient } from '../theme.js'
const dateType = ref(1) // 1=周 2=月 3=季度
const dateType = ref(3) // 1=周 2=月 3=季度,默认季度
const chartStyle = {
  width: '100%',
  height: '140%',
  height: '100%',
}
const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const barLegend = {
  show: true,
  textStyle: { color: '#B8C8E0' },
  textStyle: { color: BI_COLORS.text },
  data: ['开工', '完成', '良品率'],
}
@@ -51,17 +52,8 @@
    barGap: '40%',
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(0, 164, 237, 0)' },
          { offset: 0, color: 'rgba(78, 228, 255, 1)' },
        ],
      },
      borderRadius: [3, 3, 0, 0],
      color: barGradient(BI_COLORS.primary),
    },
    data: [],
  },
@@ -72,17 +64,8 @@
    barWidth: 20,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
          { offset: 0, color: 'rgba(144, 97, 248, 1)' },
        ],
      },
      borderRadius: [3, 3, 0, 0],
      color: barGradient(BI_COLORS.green),
    },
    data: [],
  },
@@ -90,11 +73,12 @@
    name: '良品率',
    type: 'line',
    yAxisIndex: 1,
    smooth: true,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
    itemStyle: { color: 'rgba(90, 216, 166, 1)' },
    symbolSize: 6,
    lineStyle: { color: BI_COLORS.amber, width: 2 },
    itemStyle: { color: BI_COLORS.amber },
    data: [],
    emphasis: { focus: 'series' },
  },
@@ -103,6 +87,7 @@
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  ...BI_TOOLTIP,
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
@@ -114,19 +99,25 @@
}
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
  { type: 'category', axisTick: { show: false }, axisLine: { lineStyle: { color: BI_COLORS.axisLine } }, axisLabel: { color: BI_COLORS.text }, data: [] },
])
const yAxis1 = [
  { type: 'value', name: 'ä»¶', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  {
    type: 'value',
    name: 'ä»¶',
    axisLabel: { color: BI_COLORS.text },
    nameTextStyle: { color: BI_COLORS.text },
    splitLine: { lineStyle: { color: BI_COLORS.splitLine } },
  },
  {
    type: 'value',
    name: '良品率(%)',
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
    nameTextStyle: { color: '#B8C8E0' },
    splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
    axisLabel: { color: BI_COLORS.text, formatter: '{value}%' },
    nameTextStyle: { color: BI_COLORS.text },
    splitLine: { show: false },
  },
]
@@ -173,7 +164,6 @@
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
src/views/reportAnalysis/productionAnalysis/index.vue
@@ -1,6 +1,6 @@
<template>
  <div class="scale-container">
    <div class="data-dashboard" :style="{ transform: `scale(${scaleRatio})` }">
    <div class="data-dashboard dashboard-dark" :style="{ transform: `scale(${scaleRatio})` }">
    <!-- å…¨å±æŒ‰é’® - ç§»åŠ¨åˆ°å·¦ä¸Šè§’ -->
    <button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏显示'">
      <svg v-if="!isFullscreen" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -164,11 +164,11 @@
position: relative;
width: 100%;
/* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
height: calc(100vh - 84px);
height: calc(100vh - 100px);
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
background-color: #0a1120;
overflow: hidden;
}
@@ -177,10 +177,9 @@
position: relative;
width: 1920px;
height: 1080px;
background-image: url("@/assets/BI/backImage@2x.png");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background:
  radial-gradient(1200px 520px at 50% -10%, rgba(44, 81, 217, 0.16), transparent 60%),
  linear-gradient(180deg, var(--bi-bg-to) 0%, var(--bi-bg-from) 100%);
transform-origin: center center;
}
@@ -219,20 +218,35 @@
position: relative;
z-index: 1;
height: 86px;
background-image: url("@/assets/BI/biaoti.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
gap: 28px;
padding: 0 80px;
}
/* æ ‡é¢˜ä¸¤ä¾§å¯¹ç§°æ¸éšç»†çº¿ï¼Œæ›¿ä»£å›¾ç‰‡æ ‡é¢˜æ  */
.dashboard-header::before,
.dashboard-header::after {
content: '';
flex: 1;
height: 1px;
}
.dashboard-header::before {
background: linear-gradient(90deg, transparent, rgba(91, 140, 255, 0.4));
}
.dashboard-header::after {
background: linear-gradient(90deg, rgba(91, 140, 255, 0.4), transparent);
}
.factory-name {
font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
position: absolute;
font-size: 36px;
letter-spacing: 6px;
color: var(--bi-text);
white-space: nowrap;
}
.fullscreen-btn {
@@ -241,21 +255,21 @@
left: 20px;
width: 40px;
height: 40px;
background: rgba(0, 20, 60, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 6px;
color: #00d4ff;
background: var(--bi-surface);
border: 1px solid var(--bi-border);
border-radius: var(--app-radius-sm);
color: var(--bi-accent);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
transition: background-color 0.2s ease, border-color 0.2s ease;
z-index: 10000;
}
.fullscreen-btn:hover {
background: rgba(0, 30, 90, 0.9);
border-color: rgba(0, 212, 255, 0.5);
background: var(--bi-surface-2);
border-color: var(--bi-accent);
}
.dashboard-content {
src/views/reportAnalysis/productionAnalysis/theme.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
// æš—色大屏 ECharts ä¸»é¢˜å¸¸é‡ï¼šCSS å˜é‡æ— æ³•用于 JS é…ç½®ï¼Œç»Ÿä¸€åœ¨æ­¤æ”¶å£
// è‰²å€¼ä¸º tokens.scss è¯­ä¹‰è‰²åœ¨æ·±åº•上的提亮版,与 dashboard-dark.scss ä¿æŒå¯¹åº”
const toRgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16))
export const BI_COLORS = {
  primary: '#5b8cff',
  green: '#2dbd85',
  amber: '#f5a524',
  red: '#f2635b',
  violet: '#8b7cf8',
  teal: '#2bc8c8',
  text: '#9fb0cf',
  textStrong: '#e8eefc',
  surface: '#111c33',
  axisLine: 'rgba(159, 176, 207, 0.25)',
  splitLine: 'rgba(159, 176, 207, 0.12)',
}
// é¦–页 PIE_COLORS çš„æš—底提亮版
export const BI_PIE_PALETTE = [
  BI_COLORS.primary,
  BI_COLORS.green,
  BI_COLORS.amber,
  BI_COLORS.red,
  BI_COLORS.violet,
  BI_COLORS.teal,
  '#ff8a3d',
  '#4a6cf0',
  '#b37feb',
  '#1377ef',
]
export const BI_TOOLTIP = {
  backgroundColor: '#16233d',
  borderColor: '#1f2f4d',
  borderWidth: 1,
  textStyle: { color: BI_COLORS.textStrong, fontSize: 12 },
}
// æŸ±é¡¶å®žè‰²ã€æŸ±åº•渐隐,替代原青色荧光渐变
export const barGradient = (hex) => {
  const rgb = toRgb(hex).join(',')
  return {
    type: 'linear',
    x: 0,
    y: 0,
    x2: 0,
    y2: 1,
    colorStops: [
      { offset: 0, color: `rgba(${rgb}, 0.9)` },
      { offset: 1, color: `rgba(${rgb}, 0.05)` },
    ],
  }
}
// æŠ˜çº¿é¢ç§¯æ¸å˜ï¼Œé…æ–¹å¯¹é½é¦–页 HomeInvoiceChart(0.22 â†’ 0.02)
export const areaGradient = (hex) => {
  const rgb = toRgb(hex).join(',')
  return {
    type: 'linear',
    x: 0,
    y: 0,
    x2: 0,
    y2: 1,
    colorStops: [
      { offset: 0, color: `rgba(${rgb}, 0.22)` },
      { offset: 1, color: `rgba(${rgb}, 0.02)` },
    ],
  }
}
src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue
ÎļþÒÑɾ³ý
src/views/reportAnalysis/qualityAnalysis/components/DateTypeSwitch.vue
@@ -46,49 +46,49 @@
/* æœªé€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  background-color: rgba(20, 32, 66, 0.55);
  color: #8fa3c8;
  border-color: #1e3160;
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
  transition: color 0.2s ease, background-color 0.2s ease;
}
/* ç¬¬ä¸€ä¸ªæŒ‰é’®å·¦ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  border-top-left-radius: 6px;
  border-bottom-left-radius: 6px;
}
/* æœ€åŽä¸€ä¸ªæŒ‰é’®å³ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}
/* æŒ‰é’®ä¹‹é—´çš„分隔线 */
.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
  border-right: 1px solid #1e3160;
}
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ï¼šå“ç‰Œè“å®žè‰² */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  background: #2c51d9;
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  border-color: #2c51d9;
  box-shadow: none;
}
/* æ‚¬åœæ•ˆæžœ */
.date-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
  color: #eaf1ff;
}
/* é€‰ä¸­çŠ¶æ€æ‚¬åœ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  background: #4a6cf0;
  border-color: #4a6cf0;
  color: #ffffff;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue
@@ -16,18 +16,27 @@
<style scoped>
.panel-header {
  background-image: url("@/assets/BI/kehuhetongback@2x.png");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 4px;
  height: 36px;
}
/* å“ç‰Œè“ç»†ç«–条,与基础数据大屏标题样式一致 */
.panel-header::before {
  content: '';
  width: 3px;
  height: 14px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--bi-accent-bright, #4a6cf0) 0%, var(--bi-accent, #2c51d9) 100%);
  flex-shrink: 0;
}
.panel-title {
  width: 100%;
  font-weight: 500;
  font-size: 16px;
  color: #D9ECFF;
  padding-left: 46px;
  line-height: 36px;
  color: var(--bi-text-strong, #eaf1ff);
  letter-spacing: 1px;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/ProductTypeSwitch.vue
ÎļþÒÑɾ³ý
src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue
@@ -1,5 +1,5 @@
<template>
  <div>
  <div class="panel-root">
    <div class="chart-header">
      <div class="chart-header-title">
        <PanelHeader title="完成检验数" />
@@ -16,8 +16,8 @@
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 260px"
        :options="baseChartOptions"
        style="height: 100%"
      />
    </div>
  </div>
@@ -28,17 +28,18 @@
import { completedInspectionCount } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { BI, baseChartOptions, withTooltip } from '../theme.js'
const chartStyle = {
  width: '100%',
  height: '140%',
  height: '100%',
}
const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const barLegend = {
  show: true,
  textStyle: { color: '#B8C8E0' },
  textStyle: { color: BI.text },
  data: ['合格', '不合格', '合格率'],
}
@@ -57,8 +58,8 @@
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(0, 164, 237, 0)' },
          { offset: 0, color: 'rgba(78, 228, 255, 1)' },
          { offset: 1, color: 'rgba(74, 108, 240, 0.05)' },
          { offset: 0, color: BI.accentBright },
        ],
      },
    },
@@ -78,8 +79,8 @@
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
          { offset: 0, color: 'rgba(144, 97, 248, 1)' },
          { offset: 1, color: 'rgba(142, 124, 230, 0.05)' },
          { offset: 0, color: BI.series[2] },
        ],
      },
    },
@@ -92,14 +93,14 @@
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
    itemStyle: { color: 'rgba(90, 216, 166, 1)' },
    lineStyle: { color: BI.up, width: 2 },
    itemStyle: { color: BI.up },
    data: [],
    emphasis: { focus: 'series' },
  },
])
const tooltip = {
const tooltip = withTooltip({
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  formatter(params) {
@@ -110,22 +111,22 @@
    })
    return result
  },
}
})
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
  { type: 'category', axisTick: { show: false }, axisLine: { show: false }, axisLabel: { color: BI.text, fontSize: 12 }, data: [] },
])
const yAxis1 = [
  { type: 'value', name: 'ä»¶', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  { type: 'value', name: 'ä»¶', axisLabel: { color: BI.text, fontSize: 12 }, nameTextStyle: { color: BI.text }, splitLine: { lineStyle: { color: 'rgba(143, 163, 200, 0.15)' } } },
  {
    type: 'value',
    name: '合格率(%)',
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
    nameTextStyle: { color: '#B8C8E0' },
    splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
    axisLabel: { color: BI.text, fontSize: 12, formatter: '{value}%' },
    nameTextStyle: { color: BI.text },
    splitLine: { show: false },
  },
]
@@ -176,36 +177,49 @@
.warn-range {
  position: absolute;
  right: 0;
  top: 0;
  height: 32px;
  padding: 0 14px;
  top: 2px;
  height: 28px;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  color: #ffffff;
  font-weight: 600;
  font-size: 14px;
  background: linear-gradient(180deg, rgba(51, 120, 255, 1) 0%, rgba(0, 164, 237, 1) 100%);
  border: 1px solid rgba(78, 228, 255, 0.25);
  border-radius: 6px;
  color: var(--bi-text-strong);
  font-weight: 500;
  font-size: 13px;
  background: rgba(74, 108, 240, 0.15);
  border: 1px solid rgba(74, 108, 240, 0.35);
  cursor: pointer;
  transition: border-color 0.2s ease;
  z-index: 10;
}
.warn-range:hover {
  background: linear-gradient(180deg, rgba(51, 140, 255, 1) 0%, rgba(0, 184, 237, 1) 100%);
  border-color: var(--bi-accent-bright);
}
.panel-root {
  flex: 1;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.main-panel {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 436px;
  box-sizing: border-box;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/center-center.vue
@@ -1,5 +1,5 @@
<template>
  <div>
  <div class="panel-root">
    <div class="warn-panel">
      <div class="warn-header">
        <div class="warn-header-left">
@@ -25,26 +25,12 @@
</template>
<script setup>
import { computed, getCurrentInstance, ref, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { getCurrentInstance, ref, onMounted } from 'vue'
import { nonComplianceWarning } from '@/api/viewIndex.js'
const { proxy } = getCurrentInstance() || {}
const warnings = ref([])
// å æ¯”数据
const ratios = ref({
  rawMaterialRatio: 0,
  semiFinishedProductRatio: 0,
  finishedProductRatio: 0,
})
const TAG_COLORS = {
  raw: '#7C4DFF',
  final: '#F5A000',
  semi: '#FF66CC',
}
const tagClass = (type) => {
  if (type === 'raw') return 'tag-raw'
@@ -60,71 +46,11 @@
  return 'raw' // é»˜è®¤å€¼
}
const pieChartStyle = { width: '100%', height: '100%' }
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
const pieTooltip = {
  trigger: 'item',
  formatter: (p) => `${p.name}:${p.value}%`,
}
const pieData = computed(() => {
  return [
    { name: '原材料', value: ratios.value.rawMaterialRatio, itemStyle: { color: TAG_COLORS.raw } },
    { name: '半成品', value: ratios.value.semiFinishedProductRatio, itemStyle: { color: TAG_COLORS.semi } },
    { name: '成品', value: ratios.value.finishedProductRatio, itemStyle: { color: TAG_COLORS.final } },
  ]
})
const pieSeries = computed(() => {
  return [
    {
      type: 'pie',
      radius: ['0%', '68%'],
      center: ['50%', '50%'],
      startAngle: 90,
      clockwise: true,
      avoidLabelOverlap: true,
      label: { show: false },
      labelLine: { show: false },
      itemStyle: {
        borderColor: '#071a3a',
        borderWidth: 4,
        shadowBlur: 14,
        shadowColor: 'rgba(0, 0, 0, 0.35)',
      },
      data: pieData.value,
    },
    {
      // å†…圈暗环,增强层次
      type: 'pie',
      radius: ['70%', '74%'],
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      labelLine: { show: false },
      itemStyle: { color: 'rgba(78, 228, 255, 0.12)' },
      data: [1],
    },
  ]
})
const fetchWarnings = async () => {
  try {
    const res = await nonComplianceWarning()
    if (res?.code === 200 && res?.data) {
      const data = res.data
      // æ›´æ–°å æ¯”数据
      ratios.value = {
        rawMaterialRatio: data.rawMaterialRatio ?? 0,
        semiFinishedProductRatio: data.semiFinishedProductRatio ?? 0,
        finishedProductRatio: data.finishedProductRatio ?? 0,
      }
      // æ›´æ–°è­¦å‘Šåˆ—表
      const children = data.children || []
@@ -167,26 +93,32 @@
</script>
<style scoped>
.panel-root {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.warn-panel {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 0 18px 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  height: 100%;
  flex: 1;
  min-height: 0;
  box-sizing: border-box;
  overflow: hidden;
}
.warn-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid;
  border-image: linear-gradient(270deg,
      rgba(0, 126, 255, 0) 0%,
      rgba(0, 126, 255, 0.4549) 35%,
      #007eff 78%,
      #007eff 100%) 1;
  border-bottom: 1px solid rgba(143, 163, 200, 0.15);
  padding: 10px 0 6px;
}
@@ -196,43 +128,50 @@
  gap: 10px;
}
/* å“ç‰Œè“ç»†ç«–条,与 PanelHeader å‘¼åº” */
.warn-badge {
  width: 18px;
  height: 18px;
  background: linear-gradient(180deg, #2aa8ff 0%, #4ee4ff 100%);
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
  box-shadow: 0 0 12px rgba(78, 228, 255, 0.25);
  width: 3px;
  height: 14px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--bi-accent-bright) 0%, var(--bi-accent) 100%);
  flex-shrink: 0;
}
.warn-title {
  font-weight: 600;
  font-size: 18px;
  background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  font-weight: 500;
  font-size: 16px;
  color: var(--bi-text-strong);
  letter-spacing: 1px;
  line-height: 24px;
}
.warn-range {
  height: 32px;
  padding: 0 14px;
  height: 28px;
  padding: 0 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  color: #ffffff;
  font-weight: 600;
  background: linear-gradient(180deg, rgba(51, 120, 255, 1) 0%, rgba(0, 164, 237, 1) 100%);
  border: 1px solid rgba(78, 228, 255, 0.25);
  border-radius: 6px;
  color: var(--bi-text-strong);
  font-weight: 500;
  font-size: 13px;
  background: rgba(74, 108, 240, 0.15);
  border: 1px solid rgba(74, 108, 240, 0.35);
  cursor: pointer;
  transition: border-color 0.2s ease;
}
.warn-range:hover {
  border-color: var(--bi-accent-bright);
}
.warn-body {
  display: grid;
  gap: 18px;
  align-items: stretch;
  min-height: 260px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}
.warn-list {
@@ -247,7 +186,7 @@
  grid-template-columns: 130px 1fr auto 110px;
  align-items: center;
  gap: 12px;
  color: #b8c8e0;
  color: var(--bi-text);
  font-size: 14px;
  line-height: 1.2;
  padding: 8px 0;
@@ -256,100 +195,58 @@
}
.warn-item:hover {
  color: #ff4d4f;
  background-color: rgba(255, 77, 79, 0.06);
  color: var(--bi-down);
  background-color: rgba(255, 107, 107, 0.06);
}
.warn-item:hover .warn-text {
  color: #ff4d4f;
  color: var(--bi-down);
}
.warn-tag {
  height: 28px;
  height: 24px;
  padding: 0 10px;
  border-radius: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-weight: 500;
  font-size: 12px;
  color: #ffffff;
  white-space: nowrap;
  overflow: hidden;
}
/* åºåˆ—色区分检测环节,与大屏图表配色一致 */
.tag-raw {
  background: #7c4dff;
  background: var(--bi-accent-bright);
}
.tag-final {
  background: #f5a000;
  background: #8E7CE6;
}
.tag-semi {
  background: #ff66cc;
  background: #35C6D6;
}
.warn-text {
  color: #e8f1ff;
  color: var(--bi-text-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.warn-action {
  color: #ff4d4f;
  font-weight: 700;
  color: var(--bi-down);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}
.warn-date {
  color: rgba(184, 200, 224, 0.75);
  color: var(--bi-text);
  white-space: nowrap;
}
.warn-chart {
  display: flex;
  align-items: center;
  justify-content: center;
}
.chart-frame {
  width: 100%;
  height: 260px;
  border: 2px dashed rgba(184, 200, 224, 0.35);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 50%, rgba(78, 228, 255, 0.08) 0%, rgba(0, 0, 0, 0) 65%);
}
/* å¤–圈刻度环 */
.chart-frame::before {
  content: '';
  position: absolute;
  width: 220px;
  height: 220px;
  border-radius: 50%;
  background: repeating-conic-gradient(from 0deg, rgba(78, 228, 255, 0.75) 0 1deg, rgba(78, 228, 255, 0) 1deg 9deg);
  -webkit-mask: radial-gradient(circle, transparent 62%, #000 63%);
  mask: radial-gradient(circle, transparent 62%, #000 63%);
  opacity: 0.5;
  pointer-events: none;
}
/* åå­—辅助线 */
.chart-frame::after {
  content: '';
  position: absolute;
  width: 240px;
  height: 240px;
  background:
    linear-gradient(to right, rgba(78, 228, 255, 0) 0%, rgba(78, 228, 255, 0.55) 50%, rgba(78, 228, 255, 0) 100%),
    linear-gradient(to bottom, rgba(78, 228, 255, 0) 0%, rgba(78, 228, 255, 0.55) 50%, rgba(78, 228, 255, 0) 100%);
  background-size: 100% 1px, 1px 100%;
  background-position: center, center;
  background-repeat: no-repeat;
  opacity: 0.35;
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/center-top.vue
@@ -2,8 +2,25 @@
  <div>
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <div v-for="item in statItems" :key="item.name" class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
      <div v-for="(item, index) in statItems" :key="item.name" class="stat-card">
        <div class="card-icon" :style="{ color: ICON_COLORS[index % ICON_COLORS.length] }">
          <!-- æ£€éªŒå• -->
          <svg v-if="index % 3 === 0" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <rect x="5" y="4" width="14" height="17" rx="2" />
            <path d="M9 2.5h6v3H9z" />
            <path d="M9 13l2 2 4-4" />
          </svg>
          <!-- å¾…完成:时钟 -->
          <svg v-else-if="index % 3 === 1" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <circle cx="12" cy="12" r="9" />
            <path d="M12 7v5l3.5 2" />
          </svg>
          <!-- å·²å®Œæˆï¼šå¯¹å‹¾åœ†çޝ -->
          <svg v-else width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <circle cx="12" cy="12" r="9" />
            <path d="M8.5 12.5l2.5 2.5 5-5.5" />
          </svg>
        </div>
        <div class="card-content">
          <span class="card-label">{{ item.name }}</span>
          <span class="card-value">{{ item.value }}</span>
@@ -22,8 +39,12 @@
<script setup>
import { ref, onMounted } from 'vue'
import { qualityInspectionCount } from '@/api/viewIndex.js'
import { BI } from '../theme.js'
const statItems = ref([])
// å¡ç‰‡å›¾æ ‡ç‚¹ç¼€è‰²ï¼ˆæ£€éªŒ/待完成/已完成)
const ICON_COLORS = [BI.accentBright, BI.series[3], BI.up]
const formatPercent = (val) => {
  const num = Number(val) || 0
@@ -77,38 +98,56 @@
  flex: 1;
  display: flex;
  align-items: center;
  background-image: url('@/assets/BI/border@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  background: var(--bi-panel-bg);
  border: 1px solid var(--bi-panel-border);
  border-radius: 8px;
  position: relative;
  overflow: hidden;
  height: 142px;
}
/* é¡¶éƒ¨å“ç‰Œè“äº®çº¿ï¼Œæ›¿ä»£å›¾ç‰‡æè¾¹ */
.stat-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--bi-accent) 0%, transparent 70%);
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
  width: 52px;
  height: 52px;
  margin: 0 16px 0 20px;
  border-radius: 8px;
  background: rgba(74, 108, 240, 0.12);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
  gap: 8px;
  min-width: 0;
}
.card-value {
  font-weight: 500;
  font-size: 40px;
  background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  font-weight: 600;
  font-size: 36px;
  color: var(--bi-text-strong);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.card-label {
  font-weight: 400;
  font-size: 16px;
  color: rgba(208, 231, 255, 0.7);
  color: var(--bi-text);
}
.card-compare {
@@ -116,7 +155,7 @@
  align-items: center;
  gap: 6px;
  font-size: 15px;
  color: #d0e7ff;
  color: var(--bi-text);
  white-space: nowrap;
  flex-wrap: nowrap;
}
@@ -128,6 +167,7 @@
.compare-value {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.compare-icon {
@@ -139,11 +179,11 @@
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
  color: var(--bi-up);
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
  color: var(--bi-down);
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/left-top.vue
@@ -1,5 +1,5 @@
<template>
  <div>
  <div class="panel-root">
    <PanelHeader title="质量指标合格分析" />
    <div class="main-panel panel-item-customers">
      <div v-for="section in sections" :key="section.key" class="inspect-block">
@@ -51,10 +51,11 @@
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
import { rawMaterialDetection, processDetection, factoryDetection } from '@/api/viewIndex.js'
import { BI, baseChartOptions } from '../theme.js'
const QUALIFIED_COLOR = '#4EE4FF'
const UNQUALIFIED_COLOR = '#3378FF'
const TRACK_COLOR = 'rgba(78, 228, 255, 0.12)'
const QUALIFIED_COLOR = BI.accentBright
const UNQUALIFIED_COLOR = BI.series[2]
const TRACK_COLOR = 'rgba(74, 108, 240, 0.12)'
const apiMap = {
  raw: rawMaterialDetection,
@@ -89,7 +90,7 @@
  {
    key: 'raw',
    title: '原材料检测',
    dateType: 1,
    dateType: 3,
    qualifiedCount: 0,
    unqualifiedCount: 0,
    qualifiedRate: 0,
@@ -98,7 +99,7 @@
  {
    key: 'process',
    title: '过程检测',
    dateType: 1,
    dateType: 3,
    qualifiedCount: 0,
    unqualifiedCount: 0,
    qualifiedRate: 0,
@@ -107,7 +108,7 @@
  {
    key: 'final',
    title: '成品出厂检测',
    dateType: 1,
    dateType: 3,
    qualifiedCount: 0,
    unqualifiedCount: 0,
    qualifiedRate: 0,
@@ -120,10 +121,7 @@
  height: '110px',
}
const ringOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
const ringOptions = baseChartOptions
const ringTooltip = {
  show: false,
@@ -144,7 +142,7 @@
  const unqualified = Number(section.unqualifiedCount || 0)
  const total = qualified + unqualified
  return [
  const series = [
    {
      type: 'pie',
      radius: ['68%', '82%'],
@@ -155,7 +153,10 @@
      itemStyle: { color: TRACK_COLOR },
      data: [1],
    },
    {
  ]
  // æ•°æ®ä¸ºç©ºæ—¶ä¸æ¸²æŸ“主环,避免 ECharts ç©ºé¥¼å›¾æ¸²æŸ“出灰色圆环
  if (total > 0) series.push({
      name: section.title,
      type: 'pie',
      radius: ['68%', '82%'],
@@ -167,41 +168,36 @@
      clockwise: true,
      minAngle: total > 0 ? 8 : 0,
      itemStyle: {
        borderColor: 'rgba(10, 28, 58, 0.95)',
        borderColor: BI.sliceBorder,
        borderWidth: 2,
        borderRadius: 4,
      },
      data: [
        {
          value: qualified,
          name: '合格数',
          itemStyle: {
            color: QUALIFIED_COLOR,
            shadowBlur: 16,
            shadowColor: 'rgba(78, 228, 255, 0.45)',
          },
          itemStyle: { color: QUALIFIED_COLOR },
        },
        {
          value: unqualified,
          name: '不合格数',
          itemStyle: {
            color: UNQUALIFIED_COLOR,
            shadowBlur: 10,
            shadowColor: 'rgba(51, 120, 255, 0.35)',
          },
          itemStyle: { color: UNQUALIFIED_COLOR },
        },
      ],
    },
    {
    })
  series.push({
      type: 'pie',
      radius: ['52%', '56%'],
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      labelLine: { show: false },
      itemStyle: { color: 'rgba(0, 127, 255, 0.22)' },
      itemStyle: { color: 'rgba(74, 108, 240, 0.22)' },
      data: [1],
    },
  ]
    })
  return series
}
const handleDateTypeChange = (key, dateType) => {
@@ -221,10 +217,19 @@
</script>
<style scoped lang="scss">
.panel-root {
  flex: 1;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.main-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  flex: 1;
  min-height: 0;
  gap: 0;
}
@@ -237,74 +242,35 @@
  .filters-row-left {
    width: 50%;
    color: white;
    /* ç”¨flex替代float,让子元素对齐更稳定 */
    color: var(--bi-text-strong);
    display: flex;
    align-items: center;
    gap: 8px;
    /* å“ç‰Œè“ç»†ç«–条,与 PanelHeader å‘¼åº” */
    span {
      /* æ ¸å¿ƒï¼šçˆ¶çº§ç›¸å¯¹å®šä½ï¼Œä½œä¸ºä¼ªå…ƒç´ åŸºå‡† */
      position: relative;
      display: inline-block;
      /* ç»™ä¼ªå…ƒç´ å’Œæ–‡å­—留空间 */
      padding-left: 22px;
      /* æ–‡å­—垂直居中 */
      line-height: 23px;
      margin-right: 8px;
      &::after {
        content: '';
        display: inline-block;
        width: 16px;
        height: 16px;
        clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
        background: #217AFF;
        position: absolute;
        top: 50%;
        left: 0;
        transform: translateY(-50%);
        /* ç¡®ä¿è±å½¢åœ¨æ¸å˜å—上方 */
        z-index: 1;
      }
      &::before {
        content: '';
        display: inline-block;
        width: 18px;
        height: 7px;
        border-radius: 8px;
        background: linear-gradient(360deg, rgba(33, 133, 255, 0.4) 0%, rgba(33, 221, 255, 0) 100%);
        position: absolute;
        top: 50%;
        left: -1px;
        /* ç²¾å‡†è´´åœ¨è±å½¢æ­£ä¸‹æ–¹ */
        transform: translateY(calc(0% + 8px));
        z-index: 0;
      }
      flex-shrink: 0;
      width: 3px;
      height: 14px;
      border-radius: 2px;
      background: linear-gradient(180deg, var(--bi-accent-bright) 0%, var(--bi-accent) 100%);
    }
    p {
      width: 100px;
      height: 23px;
      /* æ¸å˜èµ·å§‹è‰²å’Œè±å½¢ç»Ÿä¸€ï¼Œæ›´åè°ƒ */
      background: linear-gradient(90deg, #217AFF 0%, rgba(33, 221, 255, 0) 100%);
      /* ç²¾å‡†åž‚直居中 */
      line-height: 26px;
      text-align: center;
      color: white;
      /* ç”¨é«˜åº¦çš„一半做圆角,确保左边是完美半圆 */
      border-radius: 12px 0 0 12px;
      /* å¯é€‰ï¼šåŠ ä¸€ç‚¹å·¦å†…è¾¹è·ï¼Œè®©æ–‡å­—ä¸è´´è¾¹ */
      padding-left: 4px;
      font-size: 14px;
      font-weight: 500;
      letter-spacing: 1px;
      color: var(--bi-text-strong);
    }
  }
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 14px 18px;
  width: 100%;
  height: 958px;
  box-sizing: border-box;
}
@@ -325,7 +291,7 @@
  right: 0;
  bottom: 0;
  height: 1px;
  background: linear-gradient(90deg, rgba(33, 122, 255, 0) 0%, rgba(33, 122, 255, 0.55) 50%, rgba(33, 122, 255, 0) 100%);
  background: linear-gradient(90deg, rgba(74, 108, 240, 0) 0%, rgba(74, 108, 240, 0.55) 50%, rgba(74, 108, 240, 0) 100%);
  pointer-events: none;
}
@@ -348,32 +314,6 @@
  justify-content: center;
}
/* å¤–圈刻度(点状环) */
.ring::before {
  content: '';
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  background: repeating-conic-gradient(from 0deg,
      rgba(78, 228, 255, 0.75) 0 1deg,
      rgba(78, 228, 255, 0) 1deg 9deg);
  -webkit-mask: radial-gradient(circle, transparent 62%, #000 63%);
  mask: radial-gradient(circle, transparent 62%, #000 63%);
  opacity: 0.35;
  pointer-events: none;
}
/* æŸ”和发光背景 */
.ring::after {
  content: '';
  position: absolute;
  inset: -20px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(78, 228, 255, 0.18) 0%, rgba(78, 228, 255, 0.06) 40%, rgba(0, 0, 0, 0) 70%);
  filter: blur(0.2px);
  pointer-events: none;
}
.stats {
  width: 240px;
  flex: 0 0 240px;
@@ -388,17 +328,16 @@
  align-items: center;
  height: 100%;
  padding: 10px 14px;
  border-radius: 4px;
  border: 1px solid rgba(78, 228, 255, 0.22);
  background: linear-gradient(90deg, rgba(33, 122, 255, 0.28) 0%, rgba(10, 28, 58, 0.35) 55%, rgba(10, 28, 58, 0.2) 100%);
  box-shadow: inset 0 0 18px rgba(16, 45, 95, 0.25);
  border-radius: 6px;
  border: 1px solid var(--bi-panel-border);
  background: rgba(13, 24, 52, 0.6);
}
.stat-left {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: #b8c8e0;
  color: var(--bi-text);
  font-size: 12px;
}
@@ -407,15 +346,14 @@
  height: 10px;
  border-radius: 2px;
  display: inline-block;
  box-shadow: 0 0 10px rgba(78, 228, 255, 0.25);
}
.dot-qualified {
  background: rgba(184, 200, 224, 0.85);
  background: var(--bi-accent-bright);
}
.dot-unqualified {
  background: #4ee4ff;
  background: #8E7CE6;
}
.stat-right {
@@ -425,19 +363,20 @@
}
.stat-value {
  color: #ffffff;
  color: var(--bi-text-strong);
  font-size: 14px;
  font-weight: 600;
  min-width: 40px;
  text-align: right;
  text-shadow: 0 0 10px rgba(78, 228, 255, 0.15);
  font-variant-numeric: tabular-nums;
}
.stat-percent {
  color: rgba(184, 200, 224, 0.95);
  color: var(--bi-text);
  font-size: 12px;
  min-width: 40px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* è®©åˆ‡æ¢æŒ‰é’®æ›´è´´è¿‘截图(更紧凑) */
src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue
@@ -1,9 +1,8 @@
<template>
  <div>
  <div class="panel-root">
    <PanelHeader title="不合格检品处理分析" />
    <div class="panel-item-customers">
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
      <div class="pie-chart-wrapper">
        <Echarts ref="chart" :chartStyle="chartStyle" :legend="landLegend" :series="computedSeries"
          :tooltip="landTooltip" :color="landColors" :options="pieOptions" style="height: 100%" class="land-chart" />
      </div>
@@ -12,21 +11,19 @@
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { ref, computed, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import { unqualifiedProductProcessingAnalysis } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
import { BI, baseChartOptions, withTooltip } from '../theme.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const chart = ref(null)
//  æ•°æ®åˆ—表
const dataList = ref([])
//  é¢œè‰²åˆ—表
const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
//  é¢œè‰²åˆ—表(大屏统一序列色)
const landColors = BI.series
//  label å¯Œæ–‡æœ¬æ ·å¼
const dotRich = landColors.reduce((acc, color, idx) => {
@@ -49,16 +46,16 @@
  top: '40%',
  orient: 'vertical',
  textStyle: {
    color: '#fff',
    color: BI.textStrong,
    rich: {
      unit: { color: '#fff', fontSize: 12, padding: [0, 10, 0, 0] },
      text: { width: 60, color: '#fff', fontSize: 12 },
      unit: { color: BI.textStrong, fontSize: 12, padding: [0, 10, 0, 0] },
      text: { width: 60, color: BI.textStrong, fontSize: 12 },
    }
  }
})
//  æç¤ºæ¡†é…ç½®
const landTooltip = {
const landTooltip = withTooltip({
  trigger: 'item',
  alwaysShowContent: false,
  position: function (pt) {
@@ -70,24 +67,33 @@
    const { name, value, rate } = params.data
    return `${name}<br/>数量:${value}个<br/>占比:${rate}%`
  },
}
})
//  ä½¿ç”¨è®¡ç®—属性处理 Series
const computedSeries = computed(() => {
  const isEmpty = dataList.value.length === 0
  return [
    {
      name: '不合格检品处理分析',
      type: 'pie',
      radius: ['35%', '55%'],
      center: ['50%', '50%'],
      silent: isEmpty,
      itemStyle: isEmpty
        ? { color: 'rgba(74, 108, 240, 0.12)', borderWidth: 0 }
        : {
          borderColor: BI.sliceBorder,
          borderWidth: 2,
          borderRadius: 4,
        },
      label: {
        show: true,
        show: !isEmpty,
        position: 'outside',
        color: '#fff',
        color: BI.textStrong,
        rich: {
          ...dotRich,
          parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20 },
          child: { fontSize: 12, color: '#fff', lineHeight: 18 },
          parent: { fontSize: 14, fontWeight: 600, color: BI.textStrong, lineHeight: 20 },
          child: { fontSize: 12, color: BI.text, lineHeight: 18 },
        },
        formatter: function (params) {
          if (!params.data) return ''
@@ -96,11 +102,11 @@
        },
      },
      labelLine: {
        show: true,
        show: !isEmpty,
        length: 20,
        lineStyle: { color: '#B8C8E0' },
        lineStyle: { color: 'rgba(143, 163, 200, 0.4)' },
      },
      data: dataList.value,
      data: isEmpty ? [1] : dataList.value,
    },
    {
      // å†…圈装饰
@@ -109,23 +115,14 @@
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      itemStyle: { color: 'rgba(0, 127, 255, 0.25)' },
      itemStyle: { color: 'rgba(74, 108, 240, 0.22)' },
      data: [1],
    },
  ]
})
const chartStyle = { width: '100%', height: '126%' }
const pieOptions = { backgroundColor: 'transparent' }
//  èƒŒæ™¯å¤„理钩子
const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  offsetX: '-51.5%',
  offsetY: '-39%',
  watchData: dataList
})
const chartStyle = { width: '100%', height: '100%' }
const pieOptions = baseChartOptions
const loadData = async () => {
  try {
@@ -134,14 +131,9 @@
      dataList.value = (res.data || []).map((it) => ({
        name: it.name,
        value: Number(it.value || 0),
        rate: it.rate,
        rate: it.rate,
      }))
      landLegend.value.data = dataList.value.map((d) => d.name)
      // æ•°æ®æ›´æ–°åŽå¾®è°ƒèƒŒæ™¯
      setTimeout(() => {
        adjustBackgroundPosition()
      }, 100)
    }
  } catch (e) {
    console.error('获取数据失败:', e)
@@ -150,40 +142,33 @@
onMounted(() => {
  loadData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
<style scoped>
.panel-root {
  flex: 1;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 449px;
  box-sizing: border-box;
  flex: 1;
  min-height: 0;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
  height: 100%;
  min-height: 0;
}
.pie-background {
  position: absolute;
  width: 360px;
  height: 360px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  left: 50%;
  top: 50%;
  transform: translate(-51.5%, -39%);
}
</style>
</style>
src/views/reportAnalysis/qualityAnalysis/components/right-top.vue
@@ -1,9 +1,9 @@
<template>
  <div>
  <div class="panel-root">
    <PanelHeader title="不合格产品排名" />
    <div class="main-panel panel-item-customers">
      <div class="main-panel-container">
        <div style="color: white" class="main-panel-box" v-for="(item, index) in panelList" :key="index">
        <div class="main-panel-box" v-for="(item, index) in panelList" :key="index">
          <!-- <div style="flex: 1" class="main-panel-box-left">{{ item.rank }}</div> -->
          <div style="flex: 1" class="main-panel-box-left">{{ item.productName }}</div>
          <div style="flex: 3" class="main-panel-box-right">
@@ -71,13 +71,20 @@
  flex-direction: row;
  align-items: center;
  height: 40px;
  color: var(--bi-text);
  .main-panel-box-left {
    background: red;
    border-radius: 20px;
    background: rgba(74, 108, 240, 0.15);
    border: 1px solid rgba(74, 108, 240, 0.35);
    border-radius: 6px;
    color: var(--bi-text-strong);
    text-align: center;
    line-height: 32px;
    margin: 0 20px;
    padding: 0 12px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .main-panel-box-right {
@@ -87,8 +94,8 @@
    .main-panel-box-right-title {
      font-size: 14px;
      font-weight: 600;
      color: #ffffff;
      font-weight: 500;
      color: var(--bi-text-strong);
      margin-bottom: 6px;
    }
@@ -98,11 +105,20 @@
      justify-content: space-between;
      padding-right: 60px;
      margin-bottom: 4px;
      font-variant-numeric: tabular-nums;
    }
    .main-panel-box-right-progress {
      :deep(.el-progress__text) {
        color: white !important;
        color: var(--bi-text) !important;
      }
      :deep(.el-progress-bar__outer) {
        background-color: rgba(74, 108, 240, 0.12);
      }
      :deep(.el-progress-bar__inner) {
        background: var(--bi-accent-bright);
      }
    }
  }
@@ -116,17 +132,29 @@
  overflow-y: auto;
}
.panel-root {
  flex: 1;
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.main-panel {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  border: 1px solid var(--bi-panel-border);
  background: var(--bi-panel-bg);
  border-radius: 8px;
  padding: 18px;
  width: 100%;
  height: 449px;
  box-sizing: border-box;
  overflow: hidden;
}
</style>
src/views/reportAnalysis/qualityAnalysis/index.vue
@@ -13,7 +13,9 @@
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <div class="dashboard-header">
      <div class="header-line"></div>
      <div class="factory-name">质量数据分析</div>
      <div class="header-line header-line-right"></div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
@@ -42,7 +44,6 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import autofit from 'autofit.js'
import CenterCenter from './components/center-center.vue'
import RightTop from './components/right-top.vue'
import RightBottom from './components/right-bottom.vue'
@@ -68,23 +69,21 @@
  const container = document.querySelector('.scale-container')
  if (!container) return
  // èŽ·å–å®¹å™¨çš„å®žé™…å°ºå¯¸
  const rect = container.getBoundingClientRect?.()
  const containerWidth = container.clientWidth || rect?.width || window.innerWidth
  const containerHeight = container.clientHeight || rect?.height || window.innerHeight
  const containerWidth = container.clientWidth || window.innerWidth
  const containerHeight = container.clientHeight || window.innerHeight
  // è®¡ç®—宽高缩放比例,取较小值以保证内容完整显示(等比缩放)
  const scaleX = containerWidth / designWidth
  const scaleY = containerHeight / designHeight
  scaleRatio.value = Math.min(scaleX, scaleY)
  // ç­‰æ¯”缩放,取较小值保证 1920×1080 å†…容完整显示
  scaleRatio.value = Math.min(containerWidth / designWidth, containerHeight / designHeight)
}
// çª—口大小变化处理
// çª—口大小变化处理(rAF èŠ‚æµï¼Œé¿å…é«˜é¢‘é‡æŽ’ï¼‰
let resizeRafId = null
const handleResize = () => {
  // å»¶è¿Ÿæ‰§è¡Œï¼Œç¡®ä¿DOM更新完成
  setTimeout(() => {
  if (resizeRafId) cancelAnimationFrame(resizeRafId)
  resizeRafId = requestAnimationFrame(() => {
    resizeRafId = null
    calculateScale()
  }, 100)
  })
}
// å…¨å±åŠŸèƒ½å®žçŽ° - é’ˆå¯¹scale-container元素
@@ -144,13 +143,7 @@
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.removeEventListener('MSFullscreenChange', handleFullscreenChange)
  // ç§»é™¤æˆ‘们添加的autofit动态调整监听器
  if (window._autofitUpdateHandler) {
    window.removeEventListener('resize', window._autofitUpdateHandler)
    delete window._autofitUpdateHandler
  }
  // å…³é—­autofit
  autofit.off()
  if (resizeRafId) cancelAnimationFrame(resizeRafId)
})
</script>
@@ -159,8 +152,8 @@
.scale-container {
position: relative;
width: 100%;
/* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
height: calc(100vh - 84px);
/* å¸¸è§„布局下减去顶栏 60 + é¡µç­¾ 40 = 100px,避免内容被裁切 */
height: calc(100vh - 100px);
display: flex;
align-items: center;
justify-content: center;
@@ -173,11 +166,34 @@
position: relative;
width: 1920px;
height: 1080px;
background-image: url("@/assets/BI/backImage@2x.png");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform-origin: center center;
/* å¤§å±ä¸»é¢˜ä»¤ç‰Œï¼Œå­ç»„件通过 CSS å˜é‡ç»§æ‰¿ */
--bi-panel-bg: rgba(20, 32, 66, 0.55);
--bi-panel-border: #1e3160;
--bi-text: #8fa3c8;
--bi-text-strong: #eaf1ff;
--bi-accent: #2c51d9;
--bi-accent-bright: #4a6cf0;
--bi-up: #34c77b;
--bi-down: #ff6b6b;
background:
  radial-gradient(1200px 600px at 50% -10%, rgba(44, 81, 217, 0.18) 0%, transparent 60%),
  linear-gradient(180deg, #0a1229 0%, #0d1834 55%, #0a1430 100%);
}
/* æžæ·¡ç½‘格纹理,替代模板感底图 */
.data-dashboard::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image:
  linear-gradient(rgba(74, 108, 240, 0.045) 1px, transparent 1px),
  linear-gradient(90deg, rgba(74, 108, 240, 0.045) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.35) 100%);
}
/* å…¨å±çŠ¶æ€çš„æ ·å¼ - ä½œç”¨äºŽscale-container */
@@ -215,20 +231,42 @@
position: relative;
z-index: 1;
height: 86px;
background-image: url("@/assets/BI/biaoti.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
gap: 28px;
}
/* æ ‡é¢˜ä¸¤ä¾§æ¸éšåˆ†éš”线 */
.header-line {
width: 420px;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(74, 108, 240, 0.5) 100%);
}
.header-line-right {
background: linear-gradient(270deg, transparent 0%, rgba(74, 108, 240, 0.5) 100%);
}
.factory-name {
font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
font-size: 30px;
letter-spacing: 4px;
color: var(--bi-text-strong);
position: relative;
white-space: nowrap;
}
.factory-name::after {
content: '';
position: absolute;
left: 50%;
bottom: -8px;
transform: translateX(-50%);
width: 48px;
height: 2px;
background: var(--bi-accent-bright);
border-radius: 1px;
}
.fullscreen-btn {
@@ -237,21 +275,21 @@
left: 20px;
width: 40px;
height: 40px;
background: rgba(0, 20, 60, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
background: var(--bi-panel-bg);
border: 1px solid var(--bi-panel-border);
border-radius: 6px;
color: #00d4ff;
color: var(--bi-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
transition: color 0.2s ease, border-color 0.2s ease;
z-index: 10000;
}
.fullscreen-btn:hover {
background: rgba(0, 30, 90, 0.9);
border-color: rgba(0, 212, 255, 0.5);
color: var(--bi-text-strong);
border-color: var(--bi-accent-bright);
}
.dashboard-content {
src/views/reportAnalysis/qualityAnalysis/theme.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
// è´¨é‡æ•°æ®åˆ†æžå¤§å±ç»Ÿä¸€è§†è§‰ä»¤ç‰Œï¼ˆä¸ŽåŸºç¡€æ•°æ®/PSI å¤§å±åŒä¸€å¥—品牌蓝克制深色主题)
export const BI = {
  // å›¾è¡¨åºåˆ—色(克制专业,替代青蓝荧光)
  series: ['#4A6CF0', '#35C6D6', '#8E7CE6', '#F0B14A', '#2FB8A8', '#E86D6D'],
  text: '#8FA3C8',
  textStrong: '#EAF1FF',
  panelBg: 'rgba(20, 32, 66, 0.55)',
  panelBorder: '#1E3160',
  sliceBorder: '#0D1834',
  accent: '#2C51D9',
  accentBright: '#4A6CF0',
  up: '#34C77B',
  down: '#FF6B6B',
}
export const baseChartOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: BI.text },
}
// æ·±è‰²ç»Ÿä¸€ tooltip æ ·å¼ï¼Œä¿ç•™å„组件自定义 formatter
export const withTooltip = (tooltip = {}) => ({
  backgroundColor: 'rgba(13, 24, 52, 0.92)',
  borderColor: BI.panelBorder,
  borderWidth: 1,
  textStyle: { color: BI.textStrong, fontSize: 12 },
  ...tooltip,
})
src/views/salesManagement/indicatorStats/index.vue
@@ -407,7 +407,7 @@
.indicator-stats {
  padding: 20px;
  background: #f5f7fa;
  min-height: calc(100vh - 84px);
  min-height: calc(100vh - 100px);
}
.page-header {