| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">åæ¾å£åº¦ï¼</span> |
| | | <el-select |
| | | style="width: 200px" |
| | | v-model="searchForm.season" |
| | | placeholder="è¯·éæ©" |
| | | @change="handleQuery" |
| | | @clear="clearSeason" |
| | | clearable |
| | | :disabled="!!searchForm.issueDate" |
| | | > |
| | | <el-option |
| | | v-for="item in jidu" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | |
| | | <span class="search_title ml10">åæ¾æä»½ï¼</span> |
| | | <el-date-picker |
| | | style="width: 200px" |
| | | v-model="searchForm.issueDate" |
| | | type="month" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM" |
| | | placeholder="è¯·éæ©æä»½" |
| | | clearable |
| | | @change="handleQuery" |
| | | @clear="clearIssueDaten" |
| | | :disabled="!!searchForm.season" |
| | | /> |
| | | |
| | | <el-button type="primary" style="margin-left: 10px" @click="handleQuery"> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button style="margin-left: 10px" @click="resetHandleQuery"> |
| | | éç½® |
| | | </el-button> |
| | | </div> |
| | | <div> |
| | | <el-button @click="handleOut" icon="download">导åº</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- åæ¾è¿åº¦ï¼å¾è¡¨æ¨¡å¼ï¼ --> |
| | | <el-row :gutter="20" class="progress-cards"> |
| | | <el-col :span="6"> |
| | | <el-card class="progress-card"> |
| | | <div class="pc-title">åæ¾æ»æ°é</div> |
| | | <div class="pc-value">{{ totalNum }}</div> |
| | | <div class="pc-sub">å·²é¢ + æªé¢ï¼å«è¶
æ¶ï¼</div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-card class="progress-card success"> |
| | | <div class="pc-title">å·²é¢å</div> |
| | | <div class="pc-value">{{ adoptedNum }}</div> |
| | | <div class="pc-sub">å«è¶
æ¶å·²é¢å</div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-card class="progress-card warning"> |
| | | <div class="pc-title">æªé¢å</div> |
| | | <div class="pc-value">{{ unAdoptedNum }}</div> |
| | | <div class="pc-sub">å«è¶
æ¶æªé¢å</div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-card class="progress-card info"> |
| | | <div class="pc-title">é¢å宿ç</div> |
| | | <div class="pc-value">{{ progressPercentVal }}%</div> |
| | | <el-progress |
| | | :percentage="progressPercentVal" |
| | | :stroke-width="10" |
| | | status="success" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20" class="charts-section"> |
| | | <el-col :span="12"> |
| | | <el-card class="chart-card" v-loading="statsLoading"> |
| | | <template #header> |
| | | <div class="card-header">é¢åè¿åº¦å æ¯</div> |
| | | </template> |
| | | <div ref="pieChartRef" class="chart"></div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card class="chart-card" v-loading="statsLoading"> |
| | | <template #header> |
| | | <div class="card-header">è¿åº¦åå¸ï¼å«è¶
æ¶ï¼</div> |
| | | </template> |
| | | <div ref="barChartRef" class="chart"></div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, onMounted, onUnmounted, computed, nextTick, getCurrentInstance } from 'vue' |
| | | import dayjs from 'dayjs' |
| | | import * as echarts from 'echarts' |
| | | import { progressTotal, progressPercent, progressDistribution } from '@/api/lavorissce/ledger' |
| | | import { ElMessageBox, ElMessage } from 'element-plus' |
| | | import { getCurrentMonth } from '@/utils/util' |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | // æ¥è¯¢æ¡ä»¶ |
| | | const data = reactive({ |
| | | searchForm: { |
| | | season: getCurrentMonth(), |
| | | issueDate: '', |
| | | }, |
| | | }) |
| | | const { searchForm } = toRefs(data) |
| | | |
| | | // å£åº¦é项 |
| | | const jidu = ref([ |
| | | { value: '1', label: '第ä¸å£åº¦' }, |
| | | { value: '2', label: '第äºå£åº¦' }, |
| | | { value: '3', label: '第ä¸å£åº¦' }, |
| | | { value: '4', label: '第åå£åº¦' }, |
| | | ]) |
| | | |
| | | // è¿åº¦ç»è®¡ï¼å¾è¡¨æ°æ®ï¼ |
| | | const statsLoading = ref(false) |
| | | // åæ¾è¿åº¦-æ»è®¡ï¼æ¥èª progressTotal æ¥å£ï¼ |
| | | const totalData = ref({ |
| | | totalNum: 0, |
| | | adoptedNum: 0, |
| | | unAdoptedNum: 0, |
| | | progressPercent: 0, |
| | | }) |
| | | // é¢åè¿åº¦å æ¯ï¼æ¥èª progressPercent æ¥å£ï¼é¥¼å¾ï¼ |
| | | const percentData = ref([]) |
| | | // è¿åº¦åå¸ï¼æ¥èª progressDistribution æ¥å£ï¼æ±ç¶å¾ï¼ |
| | | const distributionData = ref({ |
| | | ylqNum: 0, |
| | | wlqNum: 0, |
| | | csylqNum: 0, |
| | | cswlqNum: 0, |
| | | }) |
| | | |
| | | const totalNum = computed(() => Number(totalData.value.totalNum || 0)) |
| | | const adoptedNum = computed(() => Number(totalData.value.adoptedNum || 0)) |
| | | const unAdoptedNum = computed(() => Number(totalData.value.unAdoptedNum || 0)) |
| | | const progressPercentVal = computed(() => Number(totalData.value.progressPercent ?? 0)) |
| | | |
| | | const pieChartRef = ref(null) |
| | | const barChartRef = ref(null) |
| | | let pieChart = null |
| | | let barChart = null |
| | | |
| | | const resizeCharts = () => { |
| | | pieChart?.resize() |
| | | barChart?.resize() |
| | | } |
| | | |
| | | const initChartsIfNeeded = async () => { |
| | | await nextTick() |
| | | if (pieChartRef.value && !pieChart) pieChart = echarts.init(pieChartRef.value) |
| | | if (barChartRef.value && !barChart) barChart = echarts.init(barChartRef.value) |
| | | renderCharts() |
| | | window.addEventListener('resize', resizeCharts) |
| | | } |
| | | |
| | | const renderCharts = () => { |
| | | const s = distributionData.value |
| | | const timelyAdopted = Number(s.ylqNum || 0) |
| | | const timelyUnAdopted = Number(s.wlqNum || 0) |
| | | const overtimeAdopted = Number(s.csylqNum || 0) |
| | | const overtimeUnAdopted = Number(s.cswlqNum || 0) |
| | | |
| | | const colors = ['#67C23A', '#E6A23C'] |
| | | const pieData = percentData.value.length |
| | | ? percentData.value.map((it, i) => ({ |
| | | ...it, |
| | | itemStyle: it.itemStyle || { color: colors[i % colors.length] }, |
| | | })) |
| | | : [ |
| | | { name: 'å·²é¢å', value: adoptedNum.value, itemStyle: { color: '#67C23A' } }, |
| | | { name: 'æªé¢å', value: unAdoptedNum.value, itemStyle: { color: '#E6A23C' } }, |
| | | ] |
| | | |
| | | if (pieChart) { |
| | | pieChart.setOption( |
| | | { |
| | | tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, |
| | | legend: { orient: 'vertical', left: 'left', top: 'middle' }, |
| | | series: [ |
| | | { |
| | | name: 'é¢åè¿åº¦', |
| | | type: 'pie', |
| | | radius: ['45%', '72%'], |
| | | center: ['60%', '50%'], |
| | | label: { formatter: '{b}\n{c}' }, |
| | | data: pieData, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: 'rgba(0, 0, 0, 0.3)', |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | true, |
| | | ) |
| | | } |
| | | |
| | | if (barChart) { |
| | | barChart.setOption( |
| | | { |
| | | tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, |
| | | grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, |
| | | xAxis: { type: 'category', data: ['åæ¶', 'è¶
æ¶'] }, |
| | | yAxis: { type: 'value' }, |
| | | legend: { data: ['å·²é¢å', 'æªé¢å'] }, |
| | | series: [ |
| | | { |
| | | name: 'å·²é¢å', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 42, |
| | | itemStyle: { color: '#67C23A' }, |
| | | data: [timelyAdopted, overtimeAdopted], |
| | | }, |
| | | { |
| | | name: 'æªé¢å', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | itemStyle: { color: '#F56C6C' }, |
| | | data: [timelyUnAdopted, overtimeUnAdopted], |
| | | }, |
| | | ], |
| | | }, |
| | | true, |
| | | ) |
| | | } |
| | | } |
| | | |
| | | const clearSeason = () => { |
| | | searchForm.value.season = '' |
| | | searchForm.value.issueDate = dayjs().format('YYYY-MM-DD') |
| | | } |
| | | |
| | | const clearIssueDaten = () => { |
| | | searchForm.value.issueDate = '' |
| | | searchForm.value.season = getCurrentMonth() |
| | | } |
| | | |
| | | const resetHandleQuery = () => { |
| | | searchForm.value.issueDate = '' |
| | | searchForm.value.season = getCurrentMonth() |
| | | handleQuery() |
| | | } |
| | | |
| | | // æ¥è¯¢ |
| | | const handleQuery = async () => { |
| | | await getStatistics() |
| | | } |
| | | |
| | | const getStatistics = async () => { |
| | | statsLoading.value = true |
| | | const params = { ...searchForm.value } |
| | | try { |
| | | const [totalRes, percentRes, distRes] = await Promise.all([ |
| | | progressTotal(params), |
| | | progressPercent(params), |
| | | progressDistribution(params), |
| | | ]) |
| | | |
| | | const d = totalRes?.data || {} |
| | | totalData.value = { |
| | | totalNum: d.total ?? 0, |
| | | adoptedNum: d.adopted ?? 0, |
| | | unAdoptedNum: d.notAdopted ?? 0, |
| | | progressPercent: d.adoptedPercent ?? 0, |
| | | } |
| | | |
| | | const p = percentRes?.data || {} |
| | | if (Array.isArray(p)) { |
| | | percentData.value = p |
| | | } else if (p.data && Array.isArray(p.data)) { |
| | | percentData.value = p.data |
| | | } else if (p.adopted != null || p.notAdopted != null) { |
| | | percentData.value = [ |
| | | { name: 'å·²é¢å', value: Number(p.adopted || 0), itemStyle: { color: '#67C23A' } }, |
| | | { name: 'æªé¢å', value: Number(p.notAdopted || 0), itemStyle: { color: '#E6A23C' } }, |
| | | ] |
| | | } else { |
| | | percentData.value = [] |
| | | } |
| | | |
| | | const dist = distRes?.data || {} |
| | | // å端è¿å示ä¾ï¼{ series: [[2,14],[0,0]] } |
| | | // 约å®ï¼series[0] = [åæ¶å·²é¢, åæ¶æªé¢]ï¼series[1] = [è¶
æ¶å·²é¢, è¶
æ¶æªé¢] |
| | | if ( |
| | | Array.isArray(dist.series) && |
| | | dist.series.length >= 2 && |
| | | Array.isArray(dist.series[0]) && |
| | | Array.isArray(dist.series[1]) && |
| | | dist.series[0].length >= 2 && |
| | | dist.series[1].length >= 2 |
| | | ) { |
| | | distributionData.value = { |
| | | ylqNum: Number(dist.series[0][0] ?? 0), |
| | | wlqNum: Number(dist.series[0][1] ?? 0), |
| | | csylqNum: Number(dist.series[1][0] ?? 0), |
| | | cswlqNum: Number(dist.series[1][1] ?? 0), |
| | | } |
| | | } else { |
| | | // å
¼å®¹å
¶å®è¿ååæ®µå½å |
| | | distributionData.value = { |
| | | ylqNum: Number(dist.ylqNum ?? dist.timelyAdopted ?? 0), |
| | | wlqNum: Number(dist.wlqNum ?? dist.timelyUnAdopted ?? 0), |
| | | csylqNum: Number(dist.csylqNum ?? dist.overtimeAdopted ?? 0), |
| | | cswlqNum: Number(dist.cswlqNum ?? dist.overtimeUnAdopted ?? 0), |
| | | } |
| | | } |
| | | |
| | | await initChartsIfNeeded() |
| | | } catch (e) { |
| | | totalData.value = { totalNum: 0, adoptedNum: 0, unAdoptedNum: 0, progressPercent: 0 } |
| | | percentData.value = [] |
| | | distributionData.value = { ylqNum: 0, wlqNum: 0, csylqNum: 0, cswlqNum: 0 } |
| | | renderCharts() |
| | | } finally { |
| | | statsLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm('éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼', '导åº', { |
| | | confirmButtonText: '确认', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning', |
| | | }) |
| | | .then(() => { |
| | | proxy.download( |
| | | '/lavorIssue/exportCopy', |
| | | { |
| | | season: searchForm.value.season, |
| | | issueDate: searchForm.value.issueDate, |
| | | }, |
| | | 'å³ä¿åæ¾æ¥è¡¨.xlsx', |
| | | ) |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.info('已忶') |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | handleQuery() |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | window.removeEventListener('resize', resizeCharts) |
| | | if (pieChart) { |
| | | pieChart.dispose() |
| | | pieChart = null |
| | | } |
| | | if (barChart) { |
| | | barChart.dispose() |
| | | barChart = null |
| | | } |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .progress-cards { |
| | | margin: 14px 0 18px; |
| | | } |
| | | .progress-card :deep(.el-card__body) { |
| | | padding: 16px; |
| | | } |
| | | .pc-title { |
| | | color: #606266; |
| | | font-size: 13px; |
| | | margin-bottom: 8px; |
| | | } |
| | | .pc-value { |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | color: #303133; |
| | | line-height: 1.2; |
| | | margin-bottom: 6px; |
| | | } |
| | | .pc-sub { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .charts-section { |
| | | margin-bottom: 12px; |
| | | } |
| | | .chart-card :deep(.el-card__body) { |
| | | padding: 12px 16px 16px; |
| | | } |
| | | .card-header { |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | .chart { |
| | | height: 320px; |
| | | width: 100%; |
| | | } |
| | | .dynamic-table-container { |
| | | width: 100%; |
| | | } |
| | | |
| | | .pagination-container { |
| | | margin-top: 20px; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | ::deep(.el-table .el-table__header-wrapper th) { |
| | | background-color: #f0f1f5 !important; |
| | | color: #333333; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | ::deep(.el-table .el-table__body-wrapper td) { |
| | | padding: 8px 0; |
| | | } |
| | | |
| | | ::deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | |
| | | ::deep(.el-input) { |
| | | width: 100%; |
| | | } |
| | | </style> |
| | | |