zhangwencui
4 天以前 766ce6b9beedcc89ee83fe5daed0523bbd8c7e33
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,519 @@
<template>
  <div>
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <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>
          </div>
        </div>
      </div>
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <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>
          </div>
        </div>
      </div>
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <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>
          </div>
        </div>
      </div>
    </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>
      <div class="equipment-items">
        <div class="equipment-item">
          <span class="equipment-value">{{ equipmentNum }}</span>
          <span class="equipment-label">设备总数</span>
        </div>
        <div class="equipment-item">
          <span class="equipment-value">{{ equipmentRepair }}</span>
          <span class="equipment-label">待维修设备</span>
        </div>
        <div class="equipment-item">
          <span class="equipment-value">{{ equipmentMaintain }}</span>
          <span class="equipment-label">待保养设备</span>
        </div>
        <div class="equipment-item">
          <span class="equipment-value">{{ totalMeasuring }}</span>
          <span class="equipment-label">计量器具总数</span>
        </div>
      </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>
      <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>
          </li>
        </ul>
        <div v-else style="text-align: center">暂无数据</div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { homeTodos, summaryStatistics } from '@/api/viewIndex.js'
import { getLedgerPage } from '@/api/equipmentManagement/ledger.js'
import { getRepairPage } from '@/api/equipmentManagement/repair.js'
import { getUpkeepPage } from '@/api/equipmentManagement/upkeep.js'
import { measuringInstrumentListPage } from '@/api/equipmentManagement/measurementEquipment.js'
// ç»Ÿè®¡æ•°æ®
const totalStaff = ref(0)
const totalCustomers = ref(0)
const totalSuppliers = ref(0)
// åŒæ¯”
const staffYoY = ref(0)
const customersYoY = ref(0)
const suppliersYoY = ref(0)
const equipmentNum = ref(0)
const equipmentRepair = ref(0)
const equipmentMaintain = ref(0)
const totalMeasuring = ref(0)
// å¾…办事项
const todoList = ref([])
const refTodoList = ref(null)
const formatPercent = (val) => {
  const num = Number(val) || 0
  return `${Math.abs(num).toFixed(2)}%`
}
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
// èŽ·å–å‘˜å·¥ã€å®¢æˆ·ã€ä¾›åº”å•†æ•°é‡
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
  }).catch(err => {
    console.error('获取基础统计数据失败:', err)
  })
}
// èŽ·å–è®¾å¤‡ç›¸å…³æ•°é‡
const getLedgerNum = () => {
  const params = {
    pageNum: -1,
    pageSize: -1,
  }
  getLedgerPage(params).then((res) => {
    equipmentNum.value = res.data.total
  })
  getRepairPage({ ...params, status: 0 }).then((res) => {
    equipmentRepair.value = res.data.total
  })
  getUpkeepPage({ ...params, status: 0 }).then((res) => {
    equipmentMaintain.value = res.data.total
  })
  measuringInstrumentListPage(params).then((res) => {
    totalMeasuring.value = res.data.total
  })
}
// åˆå§‹åŒ–待办事项列表滚动功能
const initTodoListScroll = () => {
  const todoListEl = refTodoList.value
  // å¼ºåˆ¶å¯ç”¨æ»šåŠ¨ï¼Œä¸æ£€æŸ¥ä»»ä½•æ¡ä»¶
  if (todoListEl) {
    // åˆ›å»ºä¸€ä¸ªå…‹éš†é¡¹ï¼Œç”¨äºŽå®žçŽ°æ— ç¼æ»šåŠ¨
    const scrollItems = Array.from(todoListEl.querySelectorAll('li'))
    if (scrollItems.length > 0) {
      // ç¡®ä¿æœ‰è¶³å¤Ÿçš„项目用于滚动
      // å¦‚果项目太少,多复制几次以确保滚动效果
      if (scrollItems.length < 4) {
        const originalItems = [...scrollItems]
        for (let i = 0; i < 4; i++) {
          originalItems.forEach((item) => {
            const clone = item.cloneNode(true)
            todoListEl.appendChild(clone)
          })
        }
        // é‡æ–°èŽ·å–æ‰€æœ‰é¡¹ç›®
        scrollItems.push(
          ...Array.from(todoListEl.querySelectorAll('li')).slice(
            scrollItems.length
          )
        )
      }
      const itemHeight = scrollItems[0]?.offsetHeight || 0
      const containerHeight = todoListEl.clientHeight
      const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
      // å…‹éš†å‰å‡ ä¸ªé¡¹ç›®å¹¶æ·»åŠ åˆ°åˆ—è¡¨æœ«å°¾ï¼Œå®žçŽ°æ— ç¼æ»šåŠ¨
      for (let i = 0; i < cloneCount; i++) {
        const clone = scrollItems[i % scrollItems.length].cloneNode(true)
        todoListEl.appendChild(clone)
      }
      let scrollPosition = 0
      const scrollSpeed = 1.5 // å¢žåŠ æ»šåŠ¨é€Ÿåº¦ï¼Œä½¿æ»šåŠ¨æ›´åŠ æ˜Žæ˜¾
      const pauseTime = 3000 // æ»šåŠ¨æš‚åœæ—¶é—´
      let isPaused = false
      let lastTimestamp = 0
      // è¿žç»­æ»šåŠ¨åŠ¨ç”»å‡½æ•°
      function scrollAnimation(timestamp) {
        if (!lastTimestamp) lastTimestamp = timestamp
        const deltaTime = timestamp - lastTimestamp
        lastTimestamp = timestamp
        if (!isPaused) {
          scrollPosition += scrollSpeed * (deltaTime / 16) // æ ‡å‡†åŒ–为60fps的速度
          // å½“滚动超过原始内容长度时,重置位置实现无缝滚动
          const maxScroll = Math.max(
            todoListEl.scrollHeight -
              containerHeight -
              cloneCount * itemHeight,
            itemHeight * scrollItems.length
          )
          if (scrollPosition >= maxScroll) {
            scrollPosition = 0
            todoListEl.scrollTop = 0
          } else {
            todoListEl.scrollTop = scrollPosition
          }
        }
        todoListEl._animationFrame = requestAnimationFrame(scrollAnimation)
      }
      // å¯åŠ¨æ»šåŠ¨åŠ¨ç”»
      todoListEl._animationFrame = requestAnimationFrame(scrollAnimation)
      // è®¾ç½®æ»šåЍ-暂停-滚动的循环效果
      const pauseTimer = setInterval(() => {
        isPaused = !isPaused
      }, pauseTime)
      // æ¸…理定时器
      todoListEl._pauseTimer = pauseTimer
    }
  }
}
// å¾…办事项
const todoInfoS = () => {
  homeTodos().then((res) => {
    todoList.value = res.data
    // åœ¨èŽ·å–åˆ°å¾…åŠžäº‹é¡¹æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
    nextTick(() => {
      initTodoListScroll()
    })
  })
}
onMounted(() => {
  getNum()
  getLedgerNum()
  todoInfoS()
})
onBeforeUnmount(() => {
  // æ¸…理待办事项列表的动画和定时器
  const todoListEl = refTodoList.value
  if (todoListEl) {
    if (todoListEl._animationFrame) {
      cancelAnimationFrame(todoListEl._animationFrame)
      todoListEl._animationFrame = null
    }
    if (todoListEl._pauseTimer) {
      clearInterval(todoListEl._pauseTimer)
      todoListEl._pauseTimer = null
    }
  }
})
</script>
<style scoped>
.stats-cards {
  display: flex;
  gap: 30px;
}
.stat-card {
  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;
  height: 142px;
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.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;
}
.card-label {
  font-weight: 400;
  font-size: 19px;
  color: rgba(208, 231, 255, 0.7);
}
.card-compare {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  color: #d0e7ff;
}
.card-compare > span:first-child {
  font-size: 13px;
  opacity: 0.8;
}
.compare-value {
  font-weight: 600;
}
.compare-icon {
  font-size: 14px;
  position: relative;
  top: -1px; /* è½»å¾®ä¸Šç§»ï¼Œè®©ç®­å¤´ä¸Žæ–‡å­—垂直居中对齐 */
}
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
}
.equipment-stats {
  border: 1px solid #1a58b0;
  padding: 18px;
  height: 240px;
}
.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;
}
.equipment-items {
  display: flex;
  justify-content: space-around;
  gap: 30px;
}
.equipment-item {
  text-align: center;
}
.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;
}
.equipment-label {
  font-weight: 500;
  font-size: 16px;
  color: #fffffe;
}
.event-info {
  background-image: url('@/assets/BI/shijianmingchengbeijing@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  padding: 20px;
  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;
}
.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
  height: 120px; /* æŒ‰ç”¨æˆ·è¦æ±‚调整高度 */
  overflow: hidden;
  font-size: 15px;
}
.todo-list li {
  border-radius: 8px;
  margin-bottom: 12px;
  padding: 12px 40px;
  height: 74px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.todo-title {
  font-weight: 400;
  font-size: 16px;
  color: #fffffe;
  position: relative;
}
.todo-division {
  font-weight: 400;
  font-size: 16px;
  color: #fffffe;
  position: relative;
}
.todo-division::before {
  content: '';
  position: absolute;
  left: -20px;
  top: 50%;
  transform: translateY(-50%);
  width: 6px;
  height: 6px;
  background: #498ceb;
  border-radius: 50%;
}
.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;
}
</style>