| | |
| | | <!-- 顶部统计卡片 --> |
| | | <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> |
| | |
| | | |
| | | <!-- 设备统计 --> |
| | | <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"> |
| | |
| | | </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> |
| | |
| | | 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) |
| | | }) |
| | |
| | | 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 |
| | | }) |
| | | } |
| | | |
| | |
| | | // 待办事项 |
| | | const todoInfoS = () => { |
| | | homeTodos().then((res) => { |
| | | todoList.value = res.data |
| | | todoList.value = res.data || [] |
| | | // 在获取到待办事项数据后,初始化滚动功能 |
| | | nextTick(() => { |
| | | initTodoListScroll() |
| | | }) |
| | | }).catch(err => { |
| | | console.error('获取待办事项失败:', err) |
| | | todoList.value = [] |
| | | }) |
| | | } |
| | | |
| | |
| | | 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 { |
| | |
| | | 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 { |
| | |
| | | |
| | | .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> |