diff --git a/src/api/crm/bi/ranking.ts b/src/api/crm/bi/ranking.ts new file mode 100644 index 00000000..53c09e63 --- /dev/null +++ b/src/api/crm/bi/ranking.ts @@ -0,0 +1,34 @@ +import request from '@/config/axios' + +export interface BiContractRanKingRespVO { + price: number + nickname: string + deptName: string +} +export interface BiReceivablesRanKingRespVO { + price: number + nickname: string + deptName: string +} +export interface BiRankReqVO { + deptId: number + type: string +} + +// 排行 API +export const RankingStatisticsApi = { + // 获得合同排行榜 + contractAmountRanking: (params: any) => { + return request.get({ + url: '/bi/ranking/contract-ranking', + params + }) + }, + // 获得回款排行榜 + receivablesAmountRanking: (params: any) => { + return request.get({ + url: '/bi/ranking/receivables-ranking', + params + }) + } +} diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 2c8e47ff..4c490f59 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -203,5 +203,8 @@ export enum DICT_TYPE { CRM_PRODUCT_STATUS = 'crm_product_status', CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 数据权限的级别 CRM_PRODUCT_UNIT = 'crm_product_unit', // 产品单位 - CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type' // 跟进方式 + CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // 跟进方式 + + // ========== BI - 商业智能模块 ========== + BI_ANALYZE_TYPE = 'bi_analyze_type' // 分析类型 } diff --git a/src/views/crm/bi/ranking/components/RankingContractStatistics.vue b/src/views/crm/bi/ranking/components/RankingContractStatistics.vue new file mode 100644 index 00000000..b560131c --- /dev/null +++ b/src/views/crm/bi/ranking/components/RankingContractStatistics.vue @@ -0,0 +1,133 @@ +<template> + <el-card shadow="never"> + <!-- 柱状图 --> + <el-skeleton :loading="trendLoading" animated> + <Echart :height="500" :options="barChartOptions" /> + </el-skeleton> + </el-card> + <el-card shadow="never" class="mt-16px"> + <!-- 排行列表 --> + <el-table v-loading="loading" :data="list"> + <el-table-column label="公司排名" align="center" type="index" width="80" /> + <el-table-column label="签订人" align="center" prop="nickname" min-width="200" /> + <el-table-column label="部门" align="center" prop="deptName" min-width="200" /> + <el-table-column label="合同金额(元)" align="center" prop="price" min-width="200" /> + </el-table> + </el-card> +</template> +<script setup lang="ts"> +import { RankingStatisticsApi, BiContractRanKingRespVO, BiRankReqVO } from '@/api/crm/bi/ranking' +import { EChartsOption } from 'echarts' + +/** 合同金额排行 */ +defineOptions({ name: 'RankingContractStatistics' }) + +const trendLoading = ref(true) // 状态加载中 +const loading = ref(false) // 列表的加载中 +const list = ref<BiContractRanKingRespVO[]>([]) // 列表的数据 +const params = defineProps<{ queryParams: BiRankReqVO }>() // 搜索参数 + +/** 柱状图配置 横向 */ +const barChartOptions = reactive<EChartsOption>({ + dataset: { + dimensions: ['name', 'value'], + source: [] + }, + grid: { + left: 20, + right: 20, + bottom: 20, + top: 80, + containLabel: true + }, + legend: { + top: 50 + }, + series: [ + { + name: '合同金额排行', + type: 'bar', + smooth: true, + itemStyle: { color: '#B37FEB' } + } + ], + toolbox: { + feature: { + // 数据区域缩放 + dataZoom: { + yAxisIndex: false // Y轴不缩放 + }, + brush: { + type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '合同金额排行' } // 保存为图片 + } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + xAxis: { + type: 'value', + name: '合同金额(元)', + nameGap: 30, + nameTextStyle: { + color: '#666', + fontSize: 14 + } + }, + yAxis: { + type: 'category', + name: '签订人', + nameGap: 30, + nameTextStyle: { + color: '#666', + fontSize: 14 + }, + axisLabel: { + formatter: (value: string) => { + return value + } + } + } +}) as EChartsOption + +/** 获取合同金额排行 */ +const getRankingContractStatistics = async () => { + trendLoading.value = true + loading.value = true + const rankingList = await RankingStatisticsApi.contractAmountRanking(params.queryParams) + let source = rankingList.map((item: BiContractRanKingRespVO) => { + return { + name: item.nickname, + value: item.price + } + }) + // 反转数据源 + source = source.reverse() + // 更新 Echarts 数据 + if (barChartOptions.dataset && barChartOptions.dataset['source']) { + barChartOptions.dataset['source'] = source + } + // 更新列表数据 + list.value = rankingList + trendLoading.value = false + loading.value = false +} + +/** 重新加载数据 */ +const reloadData = async () => { + await getRankingContractStatistics() +} +// 暴露 reloadData 函数 +defineExpose({ + reloadData +}) + +onMounted(() => { + getRankingContractStatistics() +}) +</script> +<style scoped lang="scss"></style> diff --git a/src/views/crm/bi/ranking/components/RankingReceivablesStatistics.vue b/src/views/crm/bi/ranking/components/RankingReceivablesStatistics.vue new file mode 100644 index 00000000..80186535 --- /dev/null +++ b/src/views/crm/bi/ranking/components/RankingReceivablesStatistics.vue @@ -0,0 +1,133 @@ +<template> + <el-card shadow="never"> + <!-- 柱状图 --> + <el-skeleton :loading="trendLoading" animated> + <Echart :height="500" :options="barChartOptions" /> + </el-skeleton> + </el-card> + <el-card shadow="never" class="mt-16px"> + <!-- 排行列表 --> + <el-table v-loading="loading" :data="list"> + <el-table-column label="公司排名" align="center" type="index" width="80" /> + <el-table-column label="签订人" align="center" prop="nickname" min-width="200" /> + <el-table-column label="部门" align="center" prop="deptName" min-width="200" /> + <el-table-column label="合同金额(元)" align="center" prop="price" min-width="200" /> + </el-table> + </el-card> +</template> +<script setup lang="ts"> +import { RankingStatisticsApi, BiReceivablesRanKingRespVO } from '@/api/crm/bi/ranking' +import { EChartsOption } from 'echarts' + +/** 回款金额排行 */ +defineOptions({ name: 'RankingReceivablesStatistics' }) + +const trendLoading = ref(true) // 状态加载中 +const loading = ref(false) // 列表的加载中 +const list = ref<BiReceivablesRanKingRespVO[]>([]) // 列表的数据 +const params = defineProps<{ queryParams: any }>() // 搜索参数 + +/** 柱状图配置 横向 */ +const barChartOptions = reactive<EChartsOption>({ + dataset: { + dimensions: ['name', 'value'], + source: [] + }, + grid: { + left: 20, + right: 20, + bottom: 20, + top: 80, + containLabel: true + }, + legend: { + top: 50 + }, + series: [ + { + name: '回款金额排行', + type: 'bar', + smooth: true, + itemStyle: { color: '#B37FEB' } + } + ], + toolbox: { + feature: { + // 数据区域缩放 + dataZoom: { + yAxisIndex: false // Y轴不缩放 + }, + brush: { + type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮 + }, + saveAsImage: { show: true, name: '回款金额排行' } // 保存为图片 + } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + xAxis: { + type: 'value', + name: '回款金额(元)', + nameGap: 30, + nameTextStyle: { + color: '#666', + fontSize: 14 + } + }, + yAxis: { + type: 'category', + name: '签订人', + nameGap: 30, + nameTextStyle: { + color: '#666', + fontSize: 14 + }, + axisLabel: { + formatter: (value: string) => { + return value + } + } + } +}) as EChartsOption + +/** 获取回款金额排行 */ +const getRankingReceivablesStatistics = async () => { + trendLoading.value = true + loading.value = true + const rankingList = await RankingStatisticsApi.receivablesAmountRanking(params.queryParams) + let source = rankingList.map((item: BiReceivablesRanKingRespVO) => { + return { + name: item.nickname, + value: item.price + } + }) + // 反转数据源 + source = source.reverse() + // 更新 Echarts 数据 + if (barChartOptions.dataset && barChartOptions.dataset['source']) { + barChartOptions.dataset['source'] = source + } + // 更新列表数据 + list.value = rankingList + trendLoading.value = false + loading.value = false +} + +/** 重新加载数据 */ +const reloadData = async () => { + await getRankingReceivablesStatistics() +} +// 暴露 reloadData 函数 +defineExpose({ + reloadData +}) + +onMounted(() => { + getRankingReceivablesStatistics() +}) +</script> +<style scoped lang="scss"></style> diff --git a/src/views/crm/bi/ranking/index.vue b/src/views/crm/bi/ranking/index.vue new file mode 100644 index 00000000..fac2b8ba --- /dev/null +++ b/src/views/crm/bi/ranking/index.vue @@ -0,0 +1,91 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <el-form-item label="类型" prop="type"> + <el-select v-model="queryParams.type" placeholder="请选择类型" clearable class="!w-240px"> + <el-option + v-for="dict in getIntDictOptions(DICT_TYPE.BI_ANALYZE_TYPE)" + :key="dict.value" + :label="dict.label" + :value="dict.value" + /> + </el-select> + </el-form-item> + <el-form-item label="归属部门" prop="deptId"> + <el-tree-select + v-model="queryParams.deptId" + :data="deptList" + :props="defaultProps" + check-strictly + node-key="id" + placeholder="请选择归属部门" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> + <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> + </el-form-item> + </el-form> + </ContentWrap> + <el-col> + <el-tabs v-model="activeTab"> + <el-tab-pane label="合同金额排行" name="contractAmountRanking"> + <!-- 合同金额排行 --> + <RankingContractStatistics :queryParams="queryParams" ref="rankingContractStatisticsRef" /> + </el-tab-pane> + <el-tab-pane label="回款金额排行" name="receivablesRanKing" lazy> + <!-- 回款金额排行 --> + <RankingReceivablesStatistics + :queryParams="queryParams" + ref="rankingReceivablesStatisticsRef" + /> + </el-tab-pane> + </el-tabs> + </el-col> +</template> +<script lang="ts" setup> +import RankingContractStatistics from './components/RankingContractStatistics.vue' +import { defaultProps, handleTree } from '@/utils/tree' +import * as DeptApi from '@/api/system/dept' +import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' + +/** 排行榜 */ +defineOptions({ name: 'RankingStatistics' }) + +const queryParams = reactive({ + type: 9, // 将 type 的初始值设置为 9 本年 + deptId: null +}) +const queryFormRef = ref() // 搜索的表单 +const deptList = ref<Tree[]>([]) // 树形结构 +const activeTab = ref('contractAmountRanking') +const rankingContractStatisticsRef = ref() // RankingContractStatistics组件的引用 +const rankingReceivablesStatisticsRef = ref() // RankingReceivablesStatistics组件的引用 + +/** 搜索按钮操作 */ +const handleQuery = () => { + if (activeTab.value === 'contractAmountRanking') { + rankingContractStatisticsRef.value.reloadData() + } else if (activeTab.value === 'receivablesRanKing') { + rankingReceivablesStatisticsRef.value.reloadData() + } +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} +// 加载部门树 +onMounted(async () => { + deptList.value = handleTree(await DeptApi.getSimpleDeptList()) +}) +</script> +<style lang="scss" scoped></style>