Files
MrleeAdmin/frontend/scanapp/pages/statistics/index.vue
T
2026-08-31 11:33:59 +08:00

616 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="statistics-page" :class="`theme-${themeMode}`">
<view class="hero-card">
<view class="hero-main">
<text class="hero-title">个人统计</text>
<text class="hero-subtitle">查看当前登录用户的扫码与处理数据</text>
</view>
<view class="hero-avatar">
<u-icon name="account-fill" color="#ffffff" size="44" />
</view>
</view>
<view class="profile-card">
<view class="profile-row">
<view class="profile-badge">{{ profile.userCode }}</view>
<view class="profile-meta">
<text class="profile-name">{{ profile.name }}</text>
<text class="profile-desc">岗位:{{ profile.position }} · 班组:{{ profile.team }}</text>
</view>
</view>
<view class="profile-extra">
<view class="profile-chip">{{ profile.department }}</view>
<view class="profile-chip success">{{ profile.level }}</view>
</view>
</view>
<view class="metric-card">
<view class="section-header">
<text class="section-title">核心指标</text>
<text class="section-subtitle">近 30 天累计</text>
</view>
<view class="metric-grid">
<view v-for="item in metrics" :key="item.label" class="metric-item">
<text class="metric-value" :class="item.type">{{ item.value }}</text>
<text class="metric-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="trend-card">
<view class="section-header">
<text class="section-title">个人趋势</text>
<text class="section-subtitle">最近 7 天</text>
</view>
<view class="bar-chart">
<view v-for="item in trendData" :key="item.label" class="bar-item">
<view class="bar-track">
<view class="bar-fill" :style="{ height: item.percent + '%', backgroundColor: item.color }"></view>
</view>
<text class="bar-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="record-card">
<view class="section-header">
<text class="section-title">最近明细</text>
<text class="section-subtitle">最新 5 条</text>
</view>
<view v-for="item in recentRecords" :key="item.id" class="record-item">
<view class="record-top">
<view>
<text class="record-main">{{ item.title }}</text>
<text class="record-sub">{{ item.subtitle }}</text>
</view>
<view class="record-pill" :class="item.statusClass">{{ item.status }}</view>
</view>
<view class="record-bottom">
<text>{{ item.time }}</text>
<text>{{ item.count }}</text>
</view>
</view>
</view>
<view class="split-card">
<view class="split-section">
<view class="section-header compact">
<text class="section-title">扫码类型占比</text>
</view>
<view v-for="item in typeRate" :key="item.label" class="progress-row">
<view class="progress-info">
<text class="progress-label">{{ item.label }}</text>
<text class="progress-value">{{ item.value }}%</text>
</view>
<view class="progress-track">
<view class="progress-fill" :style="{ width: item.value + '%', backgroundColor: item.color }"></view>
</view>
</view>
</view>
<view class="split-section">
<view class="section-header compact">
<text class="section-title">异常统计</text>
</view>
<view v-for="item in exceptionData" :key="item.label" class="progress-row">
<view class="progress-info">
<text class="progress-label">{{ item.label }}</text>
<text class="progress-value">{{ item.value }}%</text>
</view>
<view class="progress-track">
<view class="progress-fill" :style="{ width: item.value + '%', backgroundColor: item.color }"></view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getUserInfo } from '@/api/member.js'
import { getOrderRecentRecords, getOrderStatistics, getProductRecentRecords, getProductStatistics } from '@/api/produce/data-entry.js'
export default {
data() {
return {
profile: {
userCode: 'U001',
name: '-',
position: '-',
team: '-',
department: '-',
level: '-'
},
metrics: [
{ label: '今日扫码', value: '0', type: 'primary' },
{ label: '本月累计', value: '0', type: 'success' },
{ label: '成功率', value: '0%', type: 'success' },
{ label: '异常数', value: '0', type: 'danger' }
],
trendData: [],
recentRecords: [],
typeRate: [
{ label: '订单模式', value: 0, color: '#2f6cf6' },
{ label: '整机模式', value: 0, color: '#35bfa3' },
{ label: '异常记录', value: 0, color: '#f7b733' }
],
exceptionData: [
{ label: '已处理', value: 0, color: '#19be6b' },
{ label: '待复核', value: 0, color: '#f7b733' },
{ label: '待处理', value: 0, color: '#e91f3f' }
]
}
},
onLoad() {
this.loadPageData()
},
methods: {
getCurrentUserCode() {
const user = uni.getStorageSync('scanapp_user') || {}
return user.username || user.nickname || 'U001'
},
async loadPageData() {
const userCode = this.getCurrentUserCode()
try {
const [profile, orderStats, productStats, orderRecent, productRecent] = await Promise.all([
getUserInfo(),
getOrderStatistics({ user_code: userCode }),
getProductStatistics({ user_code: userCode }),
getOrderRecentRecords({ user_code: userCode, limit: 3 }),
getProductRecentRecords({ user_code: userCode, limit: 3 })
])
this.profile = {
userCode,
name: profile.nickname || profile.username || '-',
position: profile.position || profile.role_name || '-',
team: profile.team_name || profile.group_name || '-',
department: profile.department_name || '-',
level: Number(profile.enabled) === 0 ? '已停用' : '稳定输出'
}
const todayTotal = Number(orderStats?.today_total || 0) + Number(productStats?.today_total || 0)
const successTotal = Number(orderStats?.success_total || 0) + Number(productStats?.success_total || 0)
const failedTotal = Number(orderStats?.failed_total || 0) + Number(productStats?.failed_total || 0)
const successRate = todayTotal > 0 ? ((successTotal / todayTotal) * 100).toFixed(1) : '0.0'
this.metrics = [
{ label: '今日扫码', value: String(todayTotal), type: 'primary' },
{ label: '本月累计', value: String(todayTotal), type: 'success' },
{ label: '成功率', value: `${successRate}%`, type: 'success' },
{ label: '异常数', value: String(failedTotal), type: 'danger' }
]
const orderItems = Array.isArray(orderRecent?.items) ? orderRecent.items : []
const productItems = Array.isArray(productRecent?.items) ? productRecent.items : []
this.recentRecords = [...orderItems, ...productItems].slice(0, 5).map((item, index) => ({
id: item.id || index,
title: item.order_no || item.process_order_no || '-',
subtitle: item.process_name && item.step_name ? `${item.process_name} / ${item.step_name}` : '生产记录',
status: Number(item.scan_result) === 0 ? '失败' : '成功',
statusClass: Number(item.scan_result) === 0 ? 'failed' : 'success',
time: item.scan_date || item.created_date || '-',
count: item.quantity ? `+${item.quantity}` : '1'
}))
this.typeRate = [
{ label: '订单模式', value: orderItems.length, color: '#2f6cf6' },
{ label: '整机模式', value: productItems.length, color: '#35bfa3' },
{ label: '异常记录', value: failedTotal, color: '#f7b733' }
]
this.exceptionData = [
{ label: '已处理', value: successTotal, color: '#19be6b' },
{ label: '待复核', value: failedTotal > 0 ? Math.min(failedTotal, 18) : 0, color: '#f7b733' },
{ label: '待处理', value: failedTotal > 18 ? failedTotal - 18 : 0, color: '#e91f3f' }
]
} catch (error) {
console.error('[scanapp] 统计页数据加载失败', error)
}
}
}
}
</script>
<style lang="scss" scoped>
.statistics-page {
min-height: 100vh;
padding: 24rpx 22rpx 40rpx;
box-sizing: border-box;
background: #f5f7fb;
}
.hero-card,
.profile-card,
.metric-card,
.trend-card,
.record-card,
.split-card {
margin-top: 18rpx;
padding: 24rpx;
border-radius: 22rpx;
background: #ffffff;
box-shadow: 0 10rpx 26rpx rgba(23, 35, 61, 0.05);
}
.hero-card {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 0;
padding: 30rpx 26rpx;
background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%);
}
.hero-title,
.hero-subtitle,
.section-title,
.section-subtitle,
.profile-name,
.profile-desc,
.metric-label,
.record-main,
.record-sub,
.record-bottom,
.progress-label,
.progress-value {
display: block;
}
.hero-title {
color: #ffffff;
font-size: 38rpx;
font-weight: 800;
line-height: 1.35;
}
.hero-subtitle {
margin-top: 8rpx;
color: rgba(255, 255, 255, 0.84);
font-size: 22rpx;
line-height: 1.35;
}
.hero-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 84rpx;
height: 84rpx;
border-radius: 26rpx;
background: rgba(255, 255, 255, 0.18);
}
.profile-row {
display: flex;
align-items: center;
gap: 0;
}
.profile-badge {
min-width: 98rpx;
margin-right: 24rpx;
padding: 18rpx 14rpx;
border-radius: 18rpx;
background: rgba(47, 108, 246, 0.1);
color: #2f6cf6;
font-size: 22rpx;
font-weight: 700;
text-align: center;
flex: none;
}
.profile-meta {
flex: 1;
min-width: 0;
}
.profile-name {
color: #17233d;
font-size: 28rpx;
font-weight: 700;
line-height: 1.35;
}
.profile-desc {
margin-top: 6rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.4;
}
.profile-extra {
display: flex;
gap: 0;
margin-top: 16rpx;
flex-wrap: wrap;
}
.profile-chip {
padding: 6rpx 16rpx;
border-radius: 999rpx;
background: #eef3fb;
color: #6b778c;
font-size: 18rpx;
line-height: 1.35;
flex: none;
}
.profile-chip + .profile-chip {
margin-left: 14rpx;
}
.profile-chip.success {
background: rgba(25, 190, 107, 0.12);
color: #19be6b;
}
.section-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 16rpx;
}
.section-header.compact {
margin-bottom: 12rpx;
}
.section-title {
color: #17233d;
font-size: 28rpx;
font-weight: 700;
line-height: 1.35;
}
.section-subtitle {
color: #9aa4b4;
font-size: 20rpx;
line-height: 1.35;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14rpx;
}
.metric-item {
padding: 18rpx;
border-radius: 18rpx;
background: #f8fbff;
}
.metric-value {
color: #17233d;
font-size: 32rpx;
font-weight: 800;
line-height: 1.2;
}
.metric-value.primary {
color: #2f6cf6;
}
.metric-value.success {
color: #19be6b;
}
.metric-value.danger {
color: #e91f3f;
}
.metric-label {
margin-top: 6rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.35;
}
.bar-chart {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 12rpx;
align-items: end;
height: 220rpx;
}
.bar-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
height: 100%;
}
.bar-track {
display: flex;
align-items: end;
justify-content: center;
width: 100%;
height: 170rpx;
border-radius: 16rpx;
background: #f3f6fb;
overflow: hidden;
}
.bar-fill {
width: 72%;
border-radius: 16rpx 16rpx 0 0;
}
.bar-label {
margin-top: 10rpx;
color: #9aa4b4;
font-size: 18rpx;
line-height: 1.35;
}
.record-item {
padding: 18rpx 0;
border-bottom: 1rpx solid #eef1f6;
}
.record-item:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.record-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14rpx;
}
.record-main {
color: #17233d;
font-size: 28rpx;
font-weight: 700;
line-height: 1.35;
}
.record-sub {
margin-top: 5rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.35;
}
.record-pill {
padding: 4rpx 14rpx;
border-radius: 999rpx;
font-size: 18rpx;
line-height: 1.35;
flex: none;
}
.record-pill.success {
background: rgba(25, 190, 107, 0.12);
color: #19be6b;
}
.record-pill.failed {
background: rgba(233, 31, 63, 0.12);
color: #e91f3f;
}
.record-bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10rpx;
color: #9aa4b4;
font-size: 18rpx;
line-height: 1.35;
}
.split-card {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 18rpx;
}
.split-section {
padding: 20rpx;
border-radius: 18rpx;
background: #f8fbff;
}
.progress-row {
margin-top: 14rpx;
}
.progress-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
}
.progress-label,
.progress-value {
color: #7d8798;
font-size: 20rpx;
line-height: 1.35;
}
.progress-value {
color: #17233d;
font-weight: 700;
}
.progress-track {
height: 14rpx;
border-radius: 999rpx;
background: #e9edf4;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 999rpx;
}
.theme-dark.statistics-page {
background: #151923;
}
.theme-dark .hero-card,
.theme-dark .profile-card,
.theme-dark .metric-card,
.theme-dark .trend-card,
.theme-dark .record-card,
.theme-dark .split-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .metric-item,
.theme-dark .split-section,
.theme-dark .profile-chip {
background: #1c2431;
}
.theme-dark .profile-name,
.theme-dark .section-title,
.theme-dark .metric-value,
.theme-dark .record-main,
.theme-dark .progress-value {
color: #f2f5fa;
}
.theme-dark .profile-desc,
.theme-dark .section-subtitle,
.theme-dark .metric-label,
.theme-dark .bar-label,
.theme-dark .record-sub,
.theme-dark .record-bottom,
.theme-dark .progress-label,
.theme-dark .profile-chip {
color: #aeb9ca;
}
.theme-dark .record-item,
.theme-dark .progress-track {
border-color: #364152;
}
.theme-dark .bar-track,
.theme-dark .progress-track {
background: #1c2431;
}
.theme-dark .profile-badge {
background: rgba(47, 108, 246, 0.2);
}
.theme-dark .record-pill.success,
.theme-dark .record-pill.failed {
background: rgba(25, 190, 107, 0.18);
}
.theme-dark .record-pill.failed {
background: rgba(233, 31, 63, 0.18);
}
</style>