| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="process-statistics"> |
| | | <PageHeader title="å·¥åºç产å®åµ" |
| | | @back="goBack" /> |
| | | <!-- æç´¢åºå --> |
| | | <view class="search-section"> |
| | | <view class="date-picker-container" |
| | | @click="showCalendar = true"> |
| | | <view class="date-input"> |
| | | <up-icon name="calendar" |
| | | size="20" |
| | | color="#999"></up-icon> |
| | | <text class="date-text" |
| | | :class="{ 'placeholder': !searchForm.startDate }">{{ dateRangeText }}</text> |
| | | <view v-if="searchForm.startDate" |
| | | class="clear-icon-wrapper" |
| | | @click.stop="handleClearDate"> |
| | | <up-icon name="close-circle-fill" |
| | | size="18" |
| | | color="#c0c4cc"></up-icon> |
| | | </view> |
| | | </view> |
| | | <view class="search-btn-wrapper"> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="æç´¢" |
| | | @click.stop="handleQuery"></up-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- ç»è®¡å¡çå表 --> |
| | | <scroll-view scroll-y |
| | | class="stats-list"> |
| | | <view v-if="loading" |
| | | class="loading-box"> |
| | | <up-loading-icon text="å è½½ä¸..."></up-loading-icon> |
| | | </view> |
| | | <view v-else-if="statsData.length > 0" |
| | | class="card-grid"> |
| | | <view v-for="(item, index) in statsData" |
| | | :key="index" |
| | | class="stats-card"> |
| | | <view class="card-header"> |
| | | <text class="process-tag">{{ item.name }}</text> |
| | | <view class="header-details"> |
| | | <view class="detail-row"> |
| | | <text class="label">è®¡åæ°</text> |
| | | <text class="value">{{ item.planned }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">è¯åæ°</text> |
| | | <text class="value good">{{ item.good }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="label">ä¸è¯å</text> |
| | | <text class="value bad">{{ item.bad }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view class="card-body"> |
| | | <view class="main-stat"> |
| | | <text class="big-number">{{ item.total }}</text> |
| | | <text class="sub-label">çäº§ä»»å¡æ°</text> |
| | | </view> |
| | | </view> |
| | | <view class="card-footer"> |
| | | <view class="progress-section"> |
| | | <view class="progress-header"> |
| | | <text class="progress-label">ç产è¿åº¦</text> |
| | | <text class="percentage-text">{{ item.percentage }}%</text> |
| | | </view> |
| | | <up-line-progress :percentage="Math.min(item.percentage, 100)" |
| | | :activeColor="getProgressColor(item.percentage)" |
| | | :show-text="false" |
| | | height="8"></up-line-progress> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <up-empty mode="data" |
| | | text="ææ å·¥åºç»è®¡æ°æ®"></up-empty> |
| | | </view> |
| | | </scroll-view> |
| | | <!-- æ¥åéæ©å¨ --> |
| | | <up-calendar :show="showCalendar" |
| | | mode="range" |
| | | :maxDate="maxDate" |
| | | minDate="2026-01-01" |
| | | :monthNum="monthNum" |
| | | @confirm="onDateConfirm" |
| | | @close="showCalendar = false"></up-calendar> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, computed } from "vue"; |
| | | import { getOperationStatistics } from "@/api/productionManagement/workOrder.js"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const loading = ref(false); |
| | | const showCalendar = ref(false); |
| | | const dateRange = ref([]); |
| | | const maxDate = dayjs().format("YYYY-MM-DD"); |
| | | const monthNum = computed(() => { |
| | | const min = dayjs("2022-02-01"); |
| | | const max = dayjs(maxDate); |
| | | return max.diff(min, "month") + 1; |
| | | }); |
| | | // const minDate = dayjs().subtract(7, "day").format("YYYY-MM-DD"); |
| | | |
| | | const searchForm = reactive({ |
| | | startDate: "", |
| | | endDate: "", |
| | | }); |
| | | |
| | | const statsData = ref([]); |
| | | |
| | | const dateRangeText = computed(() => { |
| | | if (searchForm.startDate && searchForm.endDate) { |
| | | return `${searchForm.startDate} è³ ${searchForm.endDate}`; |
| | | } |
| | | return "è¯·éæ©æ¥æåºé´"; |
| | | }); |
| | | |
| | | const goBack = () => { |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | const getProgressColor = percentage => { |
| | | if (percentage >= 100) return "#67c23a"; |
| | | if (percentage >= 50) return "#3c9cff"; |
| | | if (percentage >= 25) return "#e6a23c"; |
| | | return "#f56c6c"; |
| | | }; |
| | | |
| | | const onDateConfirm = e => { |
| | | searchForm.startDate = e[0]; |
| | | searchForm.endDate = e[e.length - 1]; |
| | | showCalendar.value = false; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | loading.value = true; |
| | | const params = { |
| | | startDate: searchForm.startDate, |
| | | endDate: searchForm.endDate, |
| | | }; |
| | | getOperationStatistics(params) |
| | | .then(res => { |
| | | statsData.value = (res.data || []).map(item => ({ |
| | | name: item.operationName || "-", |
| | | total: item.productionTaskCount || 0, |
| | | planned: item.planQuantity || 0, |
| | | good: item.goodQuantity || 0, |
| | | bad: item.scrapQty || 0, |
| | | percentage: Number(item.completionStatus || 0), |
| | | })); |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | getList(); |
| | | }; |
| | | |
| | | const handleClearDate = () => { |
| | | searchForm.startDate = ""; |
| | | searchForm.endDate = ""; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | // é»è®¤æ¶é´ç½®ç©º |
| | | searchForm.startDate = ""; |
| | | searchForm.endDate = ""; |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/styles/procurement-common.scss"; |
| | | |
| | | .process-statistics { |
| | | min-height: 100vh; |
| | | background-color: #f5f7fa; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .search-section { |
| | | background-color: #fff; |
| | | padding: 24rpx 30rpx; |
| | | margin-bottom: 20rpx; |
| | | box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02); |
| | | } |
| | | |
| | | .date-picker-container { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 100%; |
| | | |
| | | .date-input { |
| | | flex: 1; |
| | | height: 80rpx; |
| | | background-color: #f5f7fa; |
| | | border: 1rpx solid #e4e7ed; |
| | | border-radius: 12rpx; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 24rpx; |
| | | margin-right: 20rpx; |
| | | transition: all 0.3s; |
| | | |
| | | &:active { |
| | | background-color: #ebedf0; |
| | | } |
| | | |
| | | .date-text { |
| | | font-size: 28rpx; |
| | | color: #303133; |
| | | margin-left: 16rpx; |
| | | flex: 1; |
| | | |
| | | &.placeholder { |
| | | color: #c0c4cc; |
| | | } |
| | | } |
| | | |
| | | .clear-icon { |
| | | padding: 10rpx; |
| | | margin-right: -10rpx; |
| | | } |
| | | } |
| | | |
| | | .search-btn-wrapper { |
| | | width: 140rpx; |
| | | } |
| | | } |
| | | |
| | | .stats-list { |
| | | flex: 1; |
| | | height: 0; |
| | | padding: 0 24rpx 40rpx; |
| | | } |
| | | |
| | | .loading-box { |
| | | display: flex; |
| | | justify-content: center; |
| | | padding-top: 100rpx; |
| | | } |
| | | |
| | | .card-grid { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 24rpx; |
| | | } |
| | | |
| | | .stats-card { |
| | | background: #fff; |
| | | border-radius: 16rpx; |
| | | padding: 24rpx; |
| | | box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05); |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: flex-start; |
| | | margin-bottom: 30rpx; |
| | | |
| | | .process-tag { |
| | | background-color: #e6f7ff; |
| | | color: #1890ff; |
| | | padding: 6rpx 16rpx; |
| | | border-radius: 8rpx; |
| | | font-size: 26rpx; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .header-details { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4rpx; |
| | | |
| | | .detail-row { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | gap: 12rpx; |
| | | |
| | | .label { |
| | | font-size: 22rpx; |
| | | color: #999; |
| | | } |
| | | |
| | | .value { |
| | | font-size: 24rpx; |
| | | color: #333; |
| | | font-weight: bold; |
| | | min-width: 60rpx; |
| | | text-align: right; |
| | | |
| | | &.good { |
| | | color: #52c41a; |
| | | } |
| | | &.bad { |
| | | color: #f56c6c; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .card-body { |
| | | padding-bottom: 30rpx; |
| | | border-bottom: 1rpx solid #f0f0f0; |
| | | |
| | | .main-stat { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | |
| | | .big-number { |
| | | font-size: 56rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .sub-label { |
| | | font-size: 26rpx; |
| | | color: #666; |
| | | margin-top: 12rpx; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .card-footer { |
| | | padding-top: 24rpx; |
| | | |
| | | .progress-section { |
| | | .progress-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | margin-bottom: 12rpx; |
| | | |
| | | .progress-label { |
| | | font-size: 24rpx; |
| | | color: #999; |
| | | } |
| | | |
| | | .percentage-text { |
| | | font-size: 24rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .no-data { |
| | | padding-top: 100rpx; |
| | | } |
| | | </style> |