add 运营数据

pull/853/head
wersd 2025-08-10 18:41:14 +08:00
parent 3f2f9acc04
commit e69afe7f68
8 changed files with 258 additions and 20 deletions

View File

@ -62,7 +62,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
@ -87,7 +87,7 @@
"source.fixAll.stylelint": "explicit"
},
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
"editor.defaultFormatter": "Vue.volar"
},
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",

46
package-lock.json generated
View File

@ -55,6 +55,7 @@
"video.js": "^7.21.5",
"vue": "3.5.12",
"vue-dompurify-html": "^4.1.4",
"vue-echarts": "^7.0.3",
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",
@ -16879,6 +16880,51 @@
}
}
},
"node_modules/vue-echarts": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-7.0.3.tgz",
"integrity": "sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==",
"license": "MIT",
"dependencies": {
"vue-demi": "^0.13.11"
},
"peerDependencies": {
"@vue/runtime-core": "^3.0.0",
"echarts": "^5.5.1",
"vue": "^2.7.0 || ^3.1.1"
},
"peerDependenciesMeta": {
"@vue/runtime-core": {
"optional": true
}
}
},
"node_modules/vue-echarts/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-eslint-parser": {
"version": "9.4.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",

View File

@ -72,6 +72,7 @@
"video.js": "^7.21.5",
"vue": "3.5.12",
"vue-dompurify-html": "^4.1.4",
"vue-echarts": "^7.0.3",
"vue-i18n": "9.10.2",
"vue-router": "4.4.5",
"vue-types": "^5.1.1",

View File

@ -63,4 +63,14 @@ export const syncProjectMonthlyStat = async (data: ProjectMonthlyStatVO) => {
// 查询项目月度统计图数据
export const getProjectMonthlyStatChartData = async (params: any) => {
return await request.get({ url: `/crm/project-monthly-stat/get-chart-data`, params})
}
// 查询项目对比图数据
export const getProjectCompareChartData = async (data: any) => {
return await request.post({ url: `/crm/project-monthly-stat/get-project-data`, data})
}
// 获取项目列表
export const getProjectSimpleList = async () => {
return await request.get({ url: `/crm/project-monthly-stat/project-simple-list` })
}

View File

@ -0,0 +1,148 @@
<template>
<!-- 搜索工作栏 -->
<el-form :inline="true" :model="queryParams" class="mb-16px">
<el-form-item label="项目名称" prop="projectIds">
<el-select
v-model="queryParams.projectIds"
placeholder="请选择项目名称"
class="!w-240px"
@change="getChartData"
multiple
:multiple-limit="10"
collapse-tags="false"
>
<el-option
v-for="item in projectList"
:key="item.id"
:label="item.deptName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="起始月份" prop="startMonth">
<el-date-picker
v-model="queryParams.startMonth"
type="month"
placeholder="选择月"
:value-format="'YYYYMM'"
clearable
@change="getChartData"
/>
</el-form-item>
<el-form-item label="结束月份" prop="endMonth">
<el-date-picker
v-model="queryParams.endMonth"
type="month"
placeholder="选择月"
:value-format="'YYYYMM'"
clearable
@change="getChartData"
/>
</el-form-item>
</el-form>
<el-card :body-style="{padding: '0'}" shadow="never" style="border:none;box-shadow:none;">
<v-chart :option="chartOption" autoresize style="height: 480px;" />
</el-card>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { use } from 'echarts/core'
import VChart from 'vue-echarts'
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart, LineChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components'
import * as ProjectApi from '@/api/crm/project'
import * as ProjectMonthlyStatApi from '@/api/crm/project/monthly'
import { start } from 'nprogress'
import { format } from 'path'
use([
CanvasRenderer,
BarChart,
LineChart,
GridComponent,
TooltipComponent,
LegendComponent,
TitleComponent
])
const queryParams = reactive({
projectIds: [],
startMonth: undefined,
endMonth: undefined
})
const projectList = ref<any[]>([])
const chartData = ref<any[]>([])
const chartOption = ref({})
const getChartData = async () => {
// >=
if (queryParams.startMonth && queryParams.endMonth && queryParams.endMonth < queryParams.startMonth) {
ElMessage.error('结束月份不能小于起始月份')
return
}
//
chartData.value = await ProjectMonthlyStatApi.getProjectCompareChartData({
projectIds: queryParams.projectIds,
startMonth: queryParams.startMonth,
endMonth: queryParams.endMonth
})
updateChartOption()
}
function updateChartOption() {
// x
const xData = chartData.value.map(item => item.projectName || '-')
const chargingQuantity = chartData.value.map(item => item.chargingQuantity)
const totalIncome = chartData.value.map(item => item.totalIncome)
const grossProfit = chartData.value.map(item => item.grossProfit)
const grossProfitRate = chartData.value.map(item => item.grossProfitRate)
const perGunCharging = chartData.value.map(item => item.perGunCharging)
const perGunProfit = chartData.value.map(item => item.perGunProfit)
const electricityServiceFee = chartData.value.map(item => item.electricityServiceFee)
chartOption.value = {
tooltip: { trigger: 'axis' },
legend: { data: [
'充电量(万度)', '总营收(万元)', '毛利润(万元)', '毛利率(%)',
'单枪充电量(度)', '单枪服务费(元)', '度电服务费(元)'
], top: 30 },
xAxis: [
{ type: 'category', data: xData, axisTick: { alignWithLabel: true }}
],
yAxis: [
{ type: 'value', name: '金额/充电量', position: 'left', min: null, max: null, alignTicks: true },
{ type: 'value', name: '毛利率', position: 'right', min: null, max: null, axisLabel: { formatter: '{value}%' }, alignTicks: true }
],
series: [
{ name: '总营收(万元)', type: 'bar', data: totalIncome, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '毛利润(万元)', type: 'bar', data: grossProfit, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '充电量(万度)', type: 'bar', data: chargingQuantity, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '单枪充电量(度)', type: 'bar', data: perGunCharging, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '单枪服务费(元)', type: 'bar', data: perGunProfit, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '度电服务费(元)', type: 'bar', data: electricityServiceFee, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '毛利率(%)', type: 'line', data: grossProfitRate, yAxisIndex: 1, smooth: true, symbol: 'circle', symbolSize: 8 }
]
}
}
onMounted(async () => {
//
const list = await ProjectMonthlyStatApi.getProjectSimpleList()
projectList.value = list
queryParams.projectIds = []
await getChartData()
})
</script>
<style scoped>
.mb-16px { margin-bottom: 16px; }
::v-deep(.el-card) {
border: none !important;
box-shadow: none !important;
}
</style>

View File

@ -3,7 +3,7 @@
<el-form :inline="true" :model="queryParams" class="mb-16px">
<el-form-item label="项目名称" prop="projectId">
<el-select
v-model="queryParams.projectId"
v-model="queryParams.projectIds"
placeholder="请选择项目名称"
class="!w-240px"
@change="getChartData"
@ -11,7 +11,7 @@
<el-option
v-for="item in projectList"
:key="item.id"
:label="item.name"
:label="item.deptName"
:value="item.id"
/>
</el-select>
@ -38,7 +38,7 @@
</el-form-item>
</el-form>
<el-card>
<el-card :body-style="{padding: '0'}" shadow="never" style="border:none;box-shadow:none;">
<v-chart :option="chartOption" autoresize style="height: 480px;" />
</el-card>
</template>
@ -66,7 +66,7 @@ use([
])
const queryParams = reactive({
projectId: undefined,
projectIds: [],
startMonth: undefined,
endMonth: undefined
@ -77,9 +77,14 @@ const chartData = ref<any[]>([])
const chartOption = ref({})
const getChartData = async () => {
// >=
if (queryParams.startMonth && queryParams.endMonth && queryParams.endMonth < queryParams.startMonth) {
ElMessage.error('结束月份不能小于起始月份')
return
}
//
chartData.value = await ProjectMonthlyStatApi.getProjectMonthlyStatChartData({
projectId: queryParams.projectId,
projectIds: queryParams.projectIds,
startMonth: queryParams.startMonth,
endMonth: queryParams.endMonth
})
@ -98,37 +103,48 @@ function updateChartOption() {
const totalIncome = chartData.value.map(item => item.totalIncome)
const grossProfit = chartData.value.map(item => item.grossProfit)
const grossProfitRate = chartData.value.map(item => item.grossProfitRate)
const perGunCharging = chartData.value.map(item => item.perGunCharging)
const perGunProfit = chartData.value.map(item => item.perGunProfit)
const electricityServiceFee = chartData.value.map(item => item.electricityServiceFee)
chartOption.value = {
title: { text: '项目月度统计(折柱混合)', left: 'center' },
tooltip: { trigger: 'axis' },
legend: { data: ['充电量', '总营收', '毛利润', '毛利率'], top: 30 },
legend: { data: [
'充电量(万度)', '总营收(万元)', '毛利润(万元)', '毛利率(%)',
'单枪充电量(度)', '单枪服务费(元)', '度电服务费(元)'
], top: 30 },
xAxis: [
{ type: 'category', data: xData, axisTick: { alignWithLabel: true }}
],
yAxis: [
{ type: 'value', name: '金额(万元)', position: 'left', min: null, max: null, alignTicks: true }, // 0: /
{ type: 'value', name: '充电量(万度)', position: 'left', offset: 120, min: null, max: null, alignTicks: true }, // 1:
{ type: 'value', name: '毛利率(%)', position: 'right', min: null, max: null, axisLabel: { formatter: '{value}%' }, alignTicks: true } // 2:
{ type: 'value', name: '金额/充电量', position: 'left', min: null, max: null, alignTicks: true }, // 0: /////
{ type: 'value', name: '毛利率', position: 'right', min: null, max: null, axisLabel: { formatter: '{value}%' }, alignTicks: true } // 1:
],
series: [
{ name: '总营收', type: 'bar', data: totalIncome, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '毛利润', type: 'bar', data: grossProfit, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '充电量', type: 'bar', data: chargingQuantity, yAxisIndex: 1, barMaxWidth: 28 },
{ name: '毛利率', type: 'line', data: grossProfitRate, yAxisIndex: 2, smooth: true, symbol: 'circle', symbolSize: 8 }
{ name: '总营收(万元)', type: 'bar', data: totalIncome, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '毛利润(万元)', type: 'bar', data: grossProfit, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '充电量(万度)', type: 'bar', data: chargingQuantity, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '单枪充电量(度)', type: 'bar', data: perGunCharging, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '单枪服务费(元)', type: 'bar', data: perGunProfit, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '度电服务费(元)', type: 'bar', data: electricityServiceFee, yAxisIndex: 0, barMaxWidth: 28 },
{ name: '毛利率(%)', type: 'line', data: grossProfitRate, yAxisIndex: 1, smooth: true, symbol: 'circle', symbolSize: 8 }
]
}
}
onMounted(async () => {
//
const list = await ProjectApi.getProjectSimpleList()
projectList.value = [{ id: undefined, name: '全部' }, ...list]
queryParams.projectId = undefined
const list = await ProjectMonthlyStatApi.getProjectSimpleList()
projectList.value = [{ id: undefined, deptName: '全部' }, ...list]
queryParams.projectIds = []
await getChartData()
})
</script>
<style scoped>
.mb-16px { margin-bottom: 16px; }
::v-deep(.el-card) {
border: none !important;
box-shadow: none !important;
}
</style>

View File

@ -38,6 +38,9 @@
<el-col :span="8">
<el-form-item label="充电量(度)" prop="chargingQuantity">
<el-input
type="number"
:min="0"
:precision="3"
v-model="formData.chargingQuantity"
placeholder="请输入充电量"
/>
@ -54,6 +57,7 @@
<script setup lang="ts">
import * as ProjectMonthlyStatApi from '@/api/crm/project/monthly'
import * as ProjectApi from '@/api/crm/project'
import { min } from 'lodash-es'
/** 项目月度统计 表单 */
defineOptions({ name: 'ProjectMonthlyStatForm' })

View File

@ -107,7 +107,11 @@
</el-button>
<el-button plain type="warning" @click="openSummaryDialog">
<Icon class="mr-5px" icon="ep:view" />
查看汇总
历史轴
</el-button>
<el-button plain type="primary" @click="openCompareDialog">
<Icon class="mr-5px" icon="ep:rank" />
场站PK
</el-button>
</el-form-item>
</el-form>
@ -170,6 +174,9 @@
<Dialog v-model="summaryDialogVisible" title="项目月度汇总图" width="80%" destroy-on-close>
<ProjectMonthlyChart />
</Dialog>
<Dialog v-model="compareDialogVisible" title="场站PK对比图" width="80%" destroy-on-close>
<ProjectCompareChart />
</Dialog>
</template>
<script setup lang="ts">
@ -178,6 +185,12 @@ import * as ProjectMonthlyStatApi from '@/api/crm/project/monthly'
import * as ProjectApi from '@/api/crm/project'
import ProjectMonthlyStatForm from './ProjectMonthlyStatForm.vue'
import ProjectMonthlyChart from '../component/ProjectMonthlyChart.vue'
import ProjectCompareChart from '../component/ProjectCompareChart.vue'
const compareDialogVisible = ref(false)
const openCompareDialog = () => {
compareDialogVisible.value = true
}
const summaryDialogVisible = ref(false)
const openSummaryDialog = () => {
summaryDialogVisible.value = true