<template>
|
<div>
|
<!-- 顶部统计卡片 -->
|
<div class="stats-cards">
|
<div class="stat-card">
|
<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 v-if="!isZero(staffYoY)" class="compare-icon">{{ staffYoY > 0 ? '↑' : '↓' }}</span>
|
</div>
|
</div>
|
</div>
|
<div class="stat-card">
|
<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 v-if="!isZero(customersYoY)" class="compare-icon">{{ customersYoY > 0 ? '↑' : '↓' }}</span>
|
</div>
|
</div>
|
</div>
|
<div class="stat-card">
|
<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 v-if="!isZero(suppliersYoY)" class="compare-icon">{{ suppliersYoY > 0 ? '↑' : '↓' }}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 设备统计 -->
|
<div class="equipment-stats">
|
<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">
|
<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="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 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 class="event-empty">暂无数据</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 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) => {
|
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)
|
})
|
}
|
|
// 获取设备相关数量
|
const getLedgerNum = () => {
|
const params = {
|
pageNum: -1,
|
pageSize: -1,
|
}
|
getLedgerPage(params).then((res) => {
|
equipmentNum.value = Number(res.data?.total) || 0
|
})
|
getRepairPage({ ...params, status: 0 }).then((res) => {
|
equipmentRepair.value = Number(res.data?.total) || 0
|
})
|
getUpkeepPage({ ...params, status: 0 }).then((res) => {
|
equipmentMaintain.value = Number(res.data?.total) || 0
|
})
|
measuringInstrumentListPage(params).then((res) => {
|
totalMeasuring.value = Number(res.data?.total) || 0
|
})
|
}
|
|
// 初始化待办事项列表滚动功能
|
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()
|
})
|
}).catch(err => {
|
console.error('获取待办事项失败:', err)
|
todoList.value = []
|
})
|
}
|
|
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: 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: 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: 8px;
|
min-width: 0;
|
}
|
|
.card-value {
|
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: var(--bi-text);
|
}
|
|
.card-compare {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
font-size: 15px;
|
}
|
|
.card-compare > span:first-child {
|
font-size: 13px;
|
color: var(--bi-text);
|
}
|
|
.compare-value {
|
font-weight: 600;
|
color: var(--bi-text);
|
font-variant-numeric: tabular-nums;
|
}
|
|
.compare-icon {
|
font-size: 14px;
|
position: relative;
|
top: -1px; /* 轻微上移,让箭头与文字垂直居中对齐 */
|
}
|
|
.compare-up .compare-value,
|
.compare-up .compare-icon {
|
color: var(--bi-up, #34c77b);
|
}
|
|
.compare-down .compare-value,
|
.compare-down .compare-icon {
|
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 {
|
height: 240px;
|
margin-top: 20px;
|
box-sizing: border-box;
|
}
|
|
.event-info {
|
height: 186px;
|
margin-top: 20px;
|
box-sizing: border-box;
|
}
|
|
.section-header {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
color: var(--bi-accent-bright);
|
border-bottom: 1px solid rgba(143, 163, 200, 0.15);
|
padding-bottom: 10px;
|
}
|
|
.section-icon {
|
flex-shrink: 0;
|
}
|
|
.section-title {
|
font-weight: 500;
|
font-size: 16px;
|
color: var(--bi-text-strong);
|
letter-spacing: 1px;
|
}
|
|
.equipment-items {
|
display: flex;
|
justify-content: space-around;
|
gap: 20px;
|
padding-top: 22px;
|
}
|
|
.equipment-item {
|
text-align: center;
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.equipment-value {
|
font-weight: 600;
|
font-size: 36px;
|
color: var(--bi-text-strong);
|
font-variant-numeric: tabular-nums;
|
line-height: 1.1;
|
}
|
|
.equipment-label {
|
font-weight: 400;
|
font-size: 14px;
|
color: var(--bi-text);
|
}
|
|
.event-content {
|
padding-top: 8px;
|
}
|
|
.todo-list {
|
list-style: none;
|
padding: 0;
|
margin: 0;
|
height: 96px;
|
overflow: hidden;
|
font-size: 14px;
|
}
|
|
.todo-list li {
|
margin-bottom: 8px;
|
}
|
|
.todo-row {
|
display: flex;
|
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-size: 14px;
|
color: var(--bi-text);
|
white-space: nowrap;
|
}
|
|
.todo-division {
|
font-size: 14px;
|
color: var(--bi-text);
|
white-space: nowrap;
|
}
|
|
.todo-reason {
|
color: var(--bi-text-strong);
|
overflow: hidden;
|
text-overflow: ellipsis;
|
min-width: 0;
|
flex: 1;
|
}
|
|
.todo-time {
|
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>
|