From 0796eebe3520fafb0ac5d36ee584af81506d7e9c Mon Sep 17 00:00:00 2001 From: riku <risaku@163.com> Date: 星期六, 20 九月 2025 14:05:52 +0800 Subject: [PATCH] 2025.9.20 数据产品(待完成) --- src/views/fysp/data-product/middle-data-product/ProdProblemTypeSummary.vue | 2 src/views/fysp/data-product/components/ProdQueryOptCompare.vue | 36 ++++- src/utils/echart-util.js | 75 ++++++++++++ src/components/search-option/FYOptionTopTask.vue | 41 +++++- src/views/fysp/data-product/middle-data-product/ProdProblemCountSummary.vue | 166 ++++++++++++++++++++++++++ src/views/fysp/data-product/components/BaseProdProcess.vue | 2 src/components.d.ts | 1 src/views/fysp/data-product/components/ProdDownload.vue | 13 + 8 files changed, 306 insertions(+), 30 deletions(-) diff --git a/src/components.d.ts b/src/components.d.ts index 61329ff..b4c64c6 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -15,6 +15,7 @@ Content: typeof import('./components/core/Content.vue')['default'] copy: typeof import('./components/search-option/FYOptionScene copy.vue')['default'] ElAffix: typeof import('element-plus/es')['ElAffix'] + ElAlert: typeof import('element-plus/es')['ElAlert'] ElAside: typeof import('element-plus/es')['ElAside'] ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElBadge: typeof import('element-plus/es')['ElBadge'] diff --git a/src/components/search-option/FYOptionTopTask.vue b/src/components/search-option/FYOptionTopTask.vue index 291094b..a6cdd0c 100644 --- a/src/components/search-option/FYOptionTopTask.vue +++ b/src/components/search-option/FYOptionTopTask.vue @@ -3,11 +3,11 @@ <el-select :model-value="formatedValue" @update:model-value="handleChange" - placeholder="鎬讳换鍔�" + :placeholder="label" style="width: 260px" > <el-option - v-for="s in topTasks" + v-for="s in filtedBeforeTask" :key="s.value" :label="s.label" :value="s.value" @@ -37,17 +37,44 @@ type: String, default: 'topTaskId' }, + // 閫夐」绛涢�夋潯浠讹紝绛涢�夋煇浠诲姟涔嬪墠鐨勭浉鍚岃鏀垮尯鍒掑唴鐨勪换鍔� + beforeTask: { + type: Object, + default: () => { + return {}; + } + } }, emits: ['update:value'], data() { return { selected: {}, - topTasks: [], + topTasks: [] }; }, computed: { + // 閫夋嫨妗嗕腑浣跨敤椤跺眰浠诲姟id浣滀负閫夐」鍊� formatedValue() { return this.value.tguid; + }, + // 鏌愪换鍔′箣鍓嶇殑鐩稿悓琛屾斂鍖哄垝鍐呯殑浠诲姟 + filtedBeforeTask() { + const filteredTasks = this.topTasks.filter((t) => { + return ( + (!this.beforeTask.provincecode || + this.beforeTask.provincecode == t.data.provincecode) && + (!this.beforeTask.citycode || + this.beforeTask.citycode == t.data.citycode) && + (!this.beforeTask.districtcode || + this.beforeTask.districtcode == t.data.districtcode) && + (!this.beforeTask.starttime || + t.data.starttime < this.beforeTask.starttime) + ); + }); + if (filteredTasks.length > 0) { + this.handleChange(filteredTasks[0]?.value); + } + return filteredTasks; } }, methods: { @@ -69,13 +96,11 @@ }, //鏌ヨ瀛愪换鍔$粺璁′俊鎭� handleChange(value) { - const task = this.topTasks.find( - (t) => t.data.tguid == value - ); - const param = task ? task.data : {} + const task = this.topTasks.find((t) => t.data.tguid == value); + const param = task ? task.data : {}; this.$emit('update:value', param); - }, + } }, mounted() { this.getOptions(); diff --git a/src/utils/echart-util.js b/src/utils/echart-util.js index ad12e65..7f907dd 100644 --- a/src/utils/echart-util.js +++ b/src/utils/echart-util.js @@ -69,6 +69,79 @@ }; } +function barChartOption() { + return { + title: { + text: `鏌辩姸鍥鹃粯璁ゅ悕绉癭, + left: 'center' // 鏍囬灞呬腑鏄剧ず + }, + // 娣诲姞宸ュ叿鏍忛厤缃紝鍖呭惈涓嬭浇鍔熻兘 + toolbox: { + show: true, + feature: { + saveAsImage: { + show: true, + title: '涓嬭浇鍥捐〃', + type: 'png', + pixelRatio: 2 // 鎻愰珮鍥剧墖娓呮櫚搴� + } + } + }, + tooltip: { + trigger: 'axis', // 鏌辩姸鍥句娇鐢╝xis瑙﹀彂tooltip + axisPointer: { + type: 'shadow' // 鏄剧ず闃村奖鎸囩ず鍣� + }, + formatter: '{b}: {c}' // 鏄剧ず鏍煎紡锛氬悕绉�: 鏁伴噺 + }, + legend: { + show: true, + orient: 'horizontal', + bottom: '0%', // 鍥句緥搴曢儴姘村钩鎺掑垪 + }, + grid: { + // left: '3%', + // right: '4%', + bottom: '10%', + top: '15%', + containLabel: true + }, + xAxis: { + name: '鍧愭爣杞�', + type: 'category', + data: ['sample1', 'sample2', 'sample3'], // X杞存暟鎹� + axisTick: { + alignWithLabel: true + }, + axisLabel: { + rotate: 45, + } + }, + yAxis: { + type: 'value', + name: '鏁伴噺', // Y杞村悕绉� + axisLine: { + show: true + }, + axisLabel: { + formatter: '{value}' + } + }, + series: [ + { + name: 'sample', + type: 'bar', // 鍥捐〃绫诲瀷鏀逛负鏌辩姸鍥� + data: [100, 200, 300], // 鏁版嵁鍊� + label: { + show: true, + position: 'top', // 鏍囩鏄剧ず鍦ㄦ煴瀛愰《閮� + formatter: '{c}' // 鏍囩鏍煎紡锛氭暟閲� + } + } + ] + }; +} + // 閫氳繃 ECharts API 涓嬭浇鍥剧墖鐨勫嚱鏁� function downloadChartImage(chart, fileName) { if (!chart) return; // 纭繚鍥捐〃宸插垵濮嬪寲 @@ -92,4 +165,4 @@ document.body.removeChild(link); } -export { pieChartOption, downloadChartImage }; +export { pieChartOption, barChartOption, downloadChartImage }; diff --git a/src/views/fysp/data-product/components/BaseProdProcess.vue b/src/views/fysp/data-product/components/BaseProdProcess.vue index 699d958..cc6d0de 100644 --- a/src/views/fysp/data-product/components/BaseProdProcess.vue +++ b/src/views/fysp/data-product/components/BaseProdProcess.vue @@ -8,7 +8,7 @@ > <div v-show="showStep1Content"> <template v-if="$slots.step1"> - <slot name="step1"></slot> + <slot name="step1" :onSearch="onSearch"></slot> </template> <template v-else> <ProdQueryOpt :loading="loading" @submit="onSearch"> </ProdQueryOpt> diff --git a/src/views/fysp/data-product/components/ProdDownload.vue b/src/views/fysp/data-product/components/ProdDownload.vue index 2ee4fa6..8c073f1 100644 --- a/src/views/fysp/data-product/components/ProdDownload.vue +++ b/src/views/fysp/data-product/components/ProdDownload.vue @@ -5,13 +5,13 @@ </template> <el-form :inline="false" label-position="left" label-width="150px"> <el-form-item label="鍖哄幙"> - <el-text>{{ queryOpt.districtName }}</el-text> + <el-text>{{ opts.districtName }}</el-text> </el-form-item> <el-form-item label="鏃堕棿鑼冨洿"> - <el-text>{{ queryOpt.startTime }} 鑷� {{ queryOpt.endTime }}</el-text> + <el-text>{{ opts.startTime }} 鑷� {{ opts.endTime }}</el-text> </el-form-item> <el-form-item label="鍦烘櫙绫诲瀷"> - <el-text>{{ queryOpt.sceneTypeName }}</el-text> + <el-text>{{ opts.sceneTypeName }}</el-text> </el-form-item> <el-form-item label="浜у搧褰㈠紡"> <el-radio-group v-model="downloadType"> @@ -53,6 +53,13 @@ const emit = defineEmits(['submit']); const downloadType = ref('1'); +const opts = computed(() => { + if (props.queryOpt instanceof Array && props.queryOpt.length > 0) { + return props.queryOpt[0]; + } else { + return props.queryOpt; + } +}); const submit = () => { emit('submit', { diff --git a/src/views/fysp/data-product/components/ProdQueryOptCompare.vue b/src/views/fysp/data-product/components/ProdQueryOptCompare.vue index 0935f07..6f63038 100644 --- a/src/views/fysp/data-product/components/ProdQueryOptCompare.vue +++ b/src/views/fysp/data-product/components/ProdQueryOptCompare.vue @@ -3,17 +3,31 @@ <template #header> <div><el-text tag="b" size="large">浜у搧鐢熸垚閫夐」</el-text></div> </template> - <el-form :inline="true" :model="formSearch"> - <FYOptionTopTask v-model:value="formSearch.topTask"></FYOptionTopTask> - <FYOptionScene - :allOption="false" - :type="2" - v-model:value="formSearch.scenetype" - ></FYOptionScene> - </el-form> - <el-form :inline="true" :model="formSearch2"> - <FYOptionTopTask v-model:value="formSearch2.topTask"></FYOptionTopTask> - </el-form> + <el-space fill> + <el-alert type="info" show-icon :closable="false"> + <p>閫夋嫨鏈湡闇�瑕佺粺璁$殑鎬讳换鍔″拰鍦烘櫙绫诲瀷</p> + </el-alert> + <el-form :inline="true" :model="formSearch"> + <FYOptionTopTask v-model:value="formSearch.topTask"></FYOptionTopTask> + <FYOptionScene + :allOption="false" + :type="2" + v-model:value="formSearch.scenetype" + ></FYOptionScene> + </el-form> + </el-space> + + <el-space fill> + <el-alert type="info" show-icon :closable="false"> + <p>閫夋嫨闇�瑕佽繘琛屽姣旂殑鍘嗗彶鐗堟湰</p> + </el-alert> + <el-form :inline="true" :model="formSearch2"> + <FYOptionTopTask + :beforeTask="formSearch.topTask" + v-model:value="formSearch2.topTask" + ></FYOptionTopTask> + </el-form> + </el-space> <template #footer> <el-row v-show="active" justify="end"> <el-button diff --git a/src/views/fysp/data-product/middle-data-product/ProdProblemCountSummary.vue b/src/views/fysp/data-product/middle-data-product/ProdProblemCountSummary.vue index fe5212f..83aac7e 100644 --- a/src/views/fysp/data-product/middle-data-product/ProdProblemCountSummary.vue +++ b/src/views/fysp/data-product/middle-data-product/ProdProblemCountSummary.vue @@ -1,33 +1,189 @@ <template> <BaseProdProcess v-model:active="active" + @onStep1="onStep1" @onStep2="onStep2" @onStep3="onStep3" :loading="loading" > - <template #step1> - <ProdQueryOptCompare @submit="onStep1"></ProdQueryOptCompare> + <template #step1="{ onSearch }"> + <ProdQueryOptCompare :loading="loading" @submit="onSearch"></ProdQueryOptCompare> + </template> + <template #step2="{ contentHeight }"> + <el-scrollbar :height="contentHeight"> + <el-table + id="prod-problem-count-table" + :data="tableData" + v-loading="loading" + table-layout="fixed" + :show-overflow-tooltip="true" + size="small" + border + > + <el-table-column fixed="left" type="index" label="缂栧彿" width="50"> + </el-table-column> + <el-table-column + fixed="left" + prop="districtName" + label="鍖哄幙" + width="110" + > + </el-table-column> + <el-table-column prop="townName" label="琛楅晣" width="110" /> + <el-table-column + prop="sceneCount" + label="鏈湡鍦烘櫙鏁�" + min-width="70" + /> + <el-table-column + prop="lastSceneCount" + label="涓婃湡鍦烘櫙鏁�" + min-width="70" + /> + <el-table-column + prop="problemCount" + label="鏈湡闂鏁�" + min-width="70" + /> + <el-table-column + prop="lastProblemCount" + label="涓婃湡闂鏁�" + min-width="70" + /> + <el-table-column + prop="ratio" + label="鏈湡闂鏁板潎鍊�" + min-width="70" + :formatter="ratioFormat" + /> + <el-table-column + prop="lastRatio" + label="涓婃湡闂鏁板潎鍊�" + min-width="70" + :formatter="ratioFormat" + /> + </el-table> + <el-row justify="center"> + <div + ref="chartRef" + style="height: 400px; width: 100%; max-width: 800px" + ></div> + </el-row> + </el-scrollbar> </template> </BaseProdProcess> </template> <script setup> -import { ref } from 'vue'; +import { computed, ref } from 'vue'; +import * as echarts from 'echarts'; +import dayjs from 'dayjs'; import BaseProdProcess from '@/views/fysp/data-product/components/BaseProdProcess.vue'; import dataprodmiddleApi from '@/api/fysp/dataprodmiddleApi.js'; import { conversionFromTable } from '@/utils/excel'; import { useProdStepChange } from '@/views/fysp/data-product/prod-step-change.js'; +import { barChartOption, downloadChartImage } from '@/utils/echart-util.js'; import ProdQueryOptCompare from '@/views/fysp/data-product/components/ProdQueryOptCompare.vue'; const { active, changeActive } = useProdStepChange(); const loading = ref(false); +const tableData1 = ref([]); +const tableData2 = ref([]); +const chartRef = ref(null); +let chart; + +const tableData = computed(() => { + return tableData1.value.map((item) => { + const last = tableData2.value.find( + (item2) => item2.townCode === item.townCode + ); + item.ratio = Math.round(item.ratio * 10) / 10 || 0; + return { + ...item, + lastSceneCount: last?.sceneCount || 0, + lastProblemCount: last?.problemCount || 0, + lastRatio: Math.round(item.ratio * 10) / 10 || 0 + }; + }); +}); function onStep1(opts) { - console.log('onStep1', opts); + loading.value = true; + const p1 = dataprodmiddleApi.fetchProblemCountByArea(opts[0]).then((res) => { + if (res.success) { + tableData1.value = res.data; + } + }); + const p2 = dataprodmiddleApi.fetchProblemCountByArea(opts[1]).then((res) => { + if (res.success) { + tableData2.value = res.data; + } + }); + Promise.all([p1, p2]) + .then(() => { + changeActive(); + setTimeout(() => { + genChart(opts[0], opts[1]); + }, 500); + }) + .finally(() => { + loading.value = false; + }); } function onStep2() { changeActive(); } -function onStep3(val) {} +function onStep3(val) { + if (val.downloadType == '1') { + loading.value = true; + conversionFromTable('prod-problem-count-table', '鎵皹姹℃煋闂鏁板潎鍊煎姣�'); + downloadChartImage(chart, '鎵皹姹℃煋闂鏁板潎鍊煎姣�'); + loading.value = false; + } +} + +function genChart(opt1, opt2) { + if (chart == undefined) { + chart = echarts.init(chartRef.value); + } + const year = dayjs(opt1.startTime).year(); + const month1 = dayjs(opt1.startTime).month() + 1; + const month2 = dayjs(opt2.startTime).month() + 1; + const time = `${year}骞�${month1}鏈堛��${month2}鏈坄; + const option = barChartOption(); + option.title.text = `${time}鍚勮閬擄紙闀囷級${opt1.sceneTypeName}鎵皹姹℃煋闂鏁板潎鍊煎姣擿; + + option.xAxis.name = '琛楅亾锛堥晣锛�'; + option.xAxis.data = tableData.value.map((item) => item.townName); + option.yAxis.name = '闂鏁板潎鍊�'; + + option.series = [ + { + name: `${month1}鏈坄, + type: 'bar', // 鍥捐〃绫诲瀷鏀逛负鏌辩姸鍥� + data: tableData1.value.map((item) => item.ratio), + label: { + show: true, + position: 'top', // 鏍囩鏄剧ず鍦ㄦ煴瀛愰《閮� + formatter: '{c}' // 鏍囩鏍煎紡锛氭暟閲� + } + }, + { + name: `${month2}鏈坄, + type: 'bar', // 鍥捐〃绫诲瀷鏀逛负鏌辩姸鍥� + data: tableData1.value.map((item) => item.ratio), + label: { + show: true, + position: 'top', // 鏍囩鏄剧ず鍦ㄦ煴瀛愰《閮� + formatter: '{c}' // 鏍囩鏍煎紡锛氭暟閲� + } + } + ]; + chart.setOption(option); +} + +function ratioFormat(row, column, cellValue, index) { + return Math.round(cellValue * 10) / 10; +} </script> diff --git a/src/views/fysp/data-product/middle-data-product/ProdProblemTypeSummary.vue b/src/views/fysp/data-product/middle-data-product/ProdProblemTypeSummary.vue index 181450a..93c161b 100644 --- a/src/views/fysp/data-product/middle-data-product/ProdProblemTypeSummary.vue +++ b/src/views/fysp/data-product/middle-data-product/ProdProblemTypeSummary.vue @@ -99,7 +99,7 @@ function onStep3(val) { if (val.downloadType == '1') { loading.value = true; - // conversionFromTable('prod-problem-type-table', '鎵皹姹℃煋闂绫诲瀷鍗犳瘮娓呭崟'); + conversionFromTable('prod-problem-type-table', '鎵皹姹℃煋闂绫诲瀷鍗犳瘮娓呭崟'); downloadChartImage(chart, '鎵皹姹℃煋闂绫诲瀷鍗犳瘮'); loading.value = false; } -- Gitblit v1.9.3