diff --git a/src/api/home/reportChannel.js b/src/api/home/reportChannel.js new file mode 100644 index 0000000..0e1d268 --- /dev/null +++ b/src/api/home/reportChannel.js @@ -0,0 +1,4 @@ +import request from '@/config/axios' +export const getList = async (data) => { + return await request.post({ url: '/admin-api/crm/sch-clue/clueQuality/report', data }) +} diff --git a/src/api/home/reportSignDetail.js b/src/api/home/reportSignDetail.js new file mode 100644 index 0000000..71b6667 --- /dev/null +++ b/src/api/home/reportSignDetail.js @@ -0,0 +1,8 @@ +import request from '@/config/axios' +export const getInfo = async (data) => { + return await request.post({ url: '/admin-api/crm/sch-clue/signData/report/one', data }) +} + +export const getList = async (data) => { + return await request.post({ url: '/admin-api/crm/sch-clue/signData/report/two', data }) +} diff --git a/src/api/home/reportSignRate.js b/src/api/home/reportSignRate.js new file mode 100644 index 0000000..0b515fe --- /dev/null +++ b/src/api/home/reportSignRate.js @@ -0,0 +1,4 @@ +import request from '@/config/axios' +export const getList = async (data) => { + return await request.post({ url: '/admin-api/crm/sch-clue/signRate/report', data }) +} diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts index a6b5036..13f762a 100644 --- a/src/router/modules/remaining.ts +++ b/src/router/modules/remaining.ts @@ -134,7 +134,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ // path: '/Basic', // component: Layout, // name: 'Basic', - // meta: {}, + // meta: { title: '菜单管理' }, // redirect: '/Basic/menu', // children: [ // { diff --git a/src/views/Home/ChannelReport.vue b/src/views/Home/ChannelReport.vue new file mode 100644 index 0000000..d541869 --- /dev/null +++ b/src/views/Home/ChannelReport.vue @@ -0,0 +1,35 @@ +<template> + <div> + <el-tabs v-model="tabName" type="border-card" tab-position="top"> + <el-tab-pane label="月度统计" name="monthly"> + <ChannelMonthly :sourceOptions="sourceOptions" /> + </el-tab-pane> + <el-tab-pane label="周期统计" name="period"> + <ChannelPeriod :sourceOptions="sourceOptions" /> + </el-tab-pane> + </el-tabs> + </div> +</template> + +<script setup name="ChannelReport"> +import ChannelMonthly from './Comp/ChannelMonthly.vue' +import ChannelPeriod from './Comp/ChannelPeriod.vue' +import { getSimpleSourceList } from '@/api/clue/source' +import { handleTree } from '@/utils/tree' + +const tabName = ref('monthly') + +onMounted(() => { + getOptions() +}) + +const sourceOptions = ref([]) + +function getOptions() { + getSimpleSourceList().then((data) => { + sourceOptions.value = handleTree(data, 'sourceId') + }) +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/views/Home/CloseRate.vue b/src/views/Home/CloseRate.vue index c8876d0..aa9aa68 100644 --- a/src/views/Home/CloseRate.vue +++ b/src/views/Home/CloseRate.vue @@ -1,7 +1,114 @@ <template> - <div> 成交率 </div> + <div> + <ContentWrap> + <el-form :model="searchForm" label-width="0" inline> + <el-form-item> + <el-date-picker + v-model="searchForm.year" + type="year" + format="YYYY" + value-format="YYYY" + placeholder="选择年" + :clearable="false" + /> + </el-form-item> + <el-form-item> + <el-input v-model="searchForm.nickname" placeholder="销售姓名" clearable /> + </el-form-item> + <el-form-item> + <el-button @click="handleSearch">查询</el-button> + <el-button @click="handleReset">重置</el-button> + </el-form-item> + </el-form> + + <el-table v-loading="loading" :data="tableList" border stripe> + <el-table-column type="index" width="50" fixed="left" /> + <el-table-column prop="nickname" label="姓名" width="80" /> + <el-table-column + v-for="(item, index) in new Date().getMonth() + 1" + :key="index" + :label="item + '月'" + sortable + min-width="150" + :sort-method="(pre, cur) => monthSort(pre, cur, index)" + > + <template #default="{ row }"> + <span>{{ (row.monthClueSignRateReportList[index].rate * 100).toFixed(2) }}%</span> + <span>({{ row.monthClueSignRateReportList[index].clueSignNum }}/</span> + <span>{{ row.monthClueSignRateReportList[index].followClueNum }})</span> + </template> + </el-table-column> + <el-table-column + label="总成交率" + prop="totalRate" + min-width="150" + fixed="right" + sortable + :sort-method="totalSort" + /> + </el-table> + </ContentWrap> + </div> </template> -<script setup name="CloseRate"></script> +<script setup name="CloseRate"> +import * as reportApi from '@/api/home/reportSignRate' +import { removeNullField } from '@/utils' + +onMounted(() => { + handleReset() + handleSearch() +}) + +const searchForm = ref({}) + +function handleReset() { + searchForm.value = { + nickname: undefined, + year: new Date().getFullYear() + '' + } +} + +const loading = ref(false) +const tableList = ref([]) +async function handleSearch() { + loading.value = true + try { + const data = await reportApi.getList(removeNullField(searchForm.value)) + tableList.value = data.map((item) => { + const count = item.monthClueSignRateReportList.reduce( + (pre, cur) => { + return { + signCount: pre.signCount + cur.clueSignNum, + clueCount: pre.clueCount + cur.followClueNum + } + }, + { signCount: 0, clueCount: 0 } + ) + const rate = count.clueCount > 0 ? ((count.signCount * 100) / count.clueCount).toFixed(2) : 0 + return { + ...item, + totalRate: `${rate}%(${count.signCount}/${count.clueCount})`, + totalRateNum: rate + } + }) + } finally { + loading.value = false + } +} + +function totalSort(pre, cur) { + return Number(pre.totalRateNum) - Number(cur.totalRateNum) +} + +function monthSort(pre, cur, idx) { + console.log(idx) + + return ( + Number(pre.monthClueSignRateReportList[idx].rate) - + Number(cur.monthClueSignRateReportList[idx].rate) + ) +} +</script> <style lang="scss" scoped></style> diff --git a/src/views/Home/Comp/ChannelMonthly.vue b/src/views/Home/Comp/ChannelMonthly.vue new file mode 100644 index 0000000..6411d27 --- /dev/null +++ b/src/views/Home/Comp/ChannelMonthly.vue @@ -0,0 +1,147 @@ +<template> + <ContentWrap> + <el-form :model="searchForm" label-width="0" inline> + <el-form-item> + <el-date-picker + v-model="searchForm.period" + type="month" + format="YYYY-MM" + value-format="YYYY-MM" + placeholder="选择年月" + :clearable="false" + /> + </el-form-item> + <el-form-item> + <el-tree-select + v-model="searchForm.sourceId" + :data="props.sourceOptions" + :props="defaultProps" + check-strictly + node-key="sourceId" + placeholder="请选择渠道" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleSearch">查询</el-button> + <el-button @click="handleReset">重置</el-button> + </el-form-item> + </el-form> + + <el-table + v-loading="loading" + :data="tableList" + border + stripe + :summary-method="getSummaries" + show-summary + > + <el-table-column prop="sourceName" label="渠道名称" /> + <el-table-column prop="newClueSignNum" label="新线索当月成交数" sortable> + <template #header> + <Tooltip message="当月咨询且当月成交的线索数" /> <span>新线索当月成交数</span> + </template> + </el-table-column> + <el-table-column prop="newClueNum" label="新线索总数" sortable> + <template #header> + <Tooltip message="当月咨询的线索数" /> <span>新线索总数</span> + </template> + </el-table-column> + <el-table-column prop="rate" label="新线索当月成交率" sortable :formatter="parseRate" /> + <el-table-column prop="reallyClueSignNum" sortable> + <template #header> + <Tooltip message="成交日期在本月的线索数" /> <span>当月总成交数</span> + </template> + </el-table-column> + </el-table> + </ContentWrap> +</template> + +<script setup name="ChannelMonthly"> +import * as reportApi from '@/api/home/reportChannel' +import { removeNullField } from '@/utils' +import { formatDate } from '@/utils/formatTime' + +const props = defineProps({ + sourceOptions: { + type: Array, + default: () => [] + } +}) + +const defaultProps = { + children: 'children', + label: 'sourceName', + value: 'sourceId', + isLeaf: 'leaf' +} + +onMounted(() => { + getOptions() + handleReset() + handleSearch() +}) + +const searchForm = ref({}) + +function handleReset() { + searchForm.value = { + sourceId: undefined, + period: formatDate(new Date(), 'YYYY-MM') + } +} + +function getOptions() {} + +const loading = ref(false) +const tableList = ref([]) +async function handleSearch() { + loading.value = true + try { + const data = await reportApi.getList(removeNullField(searchForm.value)) + tableList.value = data + } finally { + loading.value = false + } +} + +function parseRate(row) { + return Number(row.rate * 100).toFixed(2) + '%' +} + +function getSummaries({ columns, data }) { + let sums = [] + columns.forEach((column, index) => { + if (index == 0) { + sums[index] = '合计' + return + } + const values = data.map((item) => Number(item[column.property])) + if (!values.every((value) => Number.isNaN(value))) { + if (column.property == 'rate') { + const sum = data.reduce( + (pre, cur) => ({ + clueNum: pre.clueNum + cur.newClueNum, + signNum: pre.signNum + cur.newClueSignNum + }), + { clueNum: 0, signNum: 0 } + ) + sums[index] = sum.clueNum > 0 ? ((sum.signNum * 100) / sum.clueNum).toFixed(2) + '%' : 0 + } else { + sums[index] = values.reduce((prev, curr) => { + const value = Number(curr) + if (!Number.isNaN(value)) { + return prev + curr + } else { + return prev + } + }, 0) + } + } else { + sums[index] = '' + } + }) + return sums +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/views/Home/Comp/ChannelPeriod.vue b/src/views/Home/Comp/ChannelPeriod.vue new file mode 100644 index 0000000..7da3810 --- /dev/null +++ b/src/views/Home/Comp/ChannelPeriod.vue @@ -0,0 +1,160 @@ +<template> + <ContentWrap> + <el-form ref="searchRef" :model="searchForm" :rules="searchRules" label-width="100px" inline> + <el-form-item label="咨询日期" prop="consultDate"> + <el-date-picker + v-model="searchForm.consultDate" + type="daterange" + format="YYYY-MM-DD" + value-format="YYYY-MM-DD" + start-placeholder="选择日期" + end-placeholder="选择日期" + /> + </el-form-item> + <el-form-item label="成交周期" prop="period"> + <el-input-number + v-model="searchForm.period" + :min="1" + :controls="false" + style="width: 100%" + /> + </el-form-item> + <el-form-item label="渠道来源" prop="sourceId"> + <el-tree-select + v-model="searchForm.sourceId" + :data="props.sourceOptions" + :props="defaultProps" + check-strictly + node-key="sourceId" + placeholder="请选择渠道" + style="width: 100%" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleSearch">查询</el-button> + <el-button @click="handleReset">重置</el-button> + </el-form-item> + </el-form> + + <el-table + v-loading="loading" + :data="tableList" + border + stripe + :summary-method="getSummaries" + show-summary + > + <el-table-column prop="sourceName" label="渠道名称" /> + <el-table-column prop="newClueSignNum" label="新线索成交数" sortable> + <template #header> + <Tooltip message="咨询日期在查询范围内,且在所查成交周期内成交的线索数" /> + <span>新线索成交数</span> + </template> + </el-table-column> + <el-table-column prop="newClueNum" label="新线索总数" sortable> + <template #header> + <Tooltip message="咨询日期在查询范围内,咨询的线索数" /> <span>新线索总数</span> + </template> + </el-table-column> + <el-table-column prop="rate" label="新线索成交率" sortable :formatter="parseRate" /> + </el-table> + </ContentWrap> +</template> + +<script setup name="ChannelPeriod"> +import * as reportApi from '@/api/home/reportChannel' +import { removeNullField } from '@/utils' + +const props = defineProps({ + sourceOptions: { + type: Array, + default: () => [] + } +}) + +const defaultProps = { + children: 'children', + label: 'sourceName', + value: 'sourceId', + isLeaf: 'leaf' +} + +const searchRef = ref() + +const searchRules = { + consultDate: { required: true, type: 'array', message: '请选择咨询日期', trigger: 'change,blur' }, + period: { required: true, message: '请输入成交周期', trigger: 'blur' } +} + +onMounted(() => { + getOptions() + handleReset() +}) + +const searchForm = ref({}) + +function handleReset() { + searchForm.value = { + sourceId: undefined, + period: 30, + consultDate: [] + } +} + +function getOptions() {} + +const loading = ref(false) +const tableList = ref([]) +async function handleSearch() { + try { + const valid = await searchRef.value.validate() + if (!valid) return + loading.value = true + const data = await reportApi.getList(removeNullField(searchForm.value)) + tableList.value = data + } finally { + loading.value = false + } +} + +function parseRate(row) { + return Number(row.rate * 100).toFixed(2) + '%' +} + +function getSummaries({ columns, data }) { + let sums = [] + columns.forEach((column, index) => { + if (index == 0) { + sums[index] = '合计' + return + } + const values = data.map((item) => Number(item[column.property])) + if (!values.every((value) => Number.isNaN(value))) { + if (column.property == 'rate') { + const sum = data.reduce( + (pre, cur) => ({ + clueNum: pre.clueNum + cur.newClueNum, + signNum: pre.signNum + cur.newClueSignNum + }), + { clueNum: 0, signNum: 0 } + ) + sums[index] = sum.clueNum > 0 ? ((sum.signNum * 100) / sum.clueNum).toFixed(2) + '%' : 0 + } else { + sums[index] = values.reduce((prev, curr) => { + const value = Number(curr) + if (!Number.isNaN(value)) { + return prev + curr + } else { + return prev + } + }, 0) + } + } else { + sums[index] = '' + } + }) + return sums +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/views/Home/Comp/SignListCard.vue b/src/views/Home/Comp/SignListCard.vue new file mode 100644 index 0000000..fb9e77d --- /dev/null +++ b/src/views/Home/Comp/SignListCard.vue @@ -0,0 +1,105 @@ +<template> + <ContentWrap> + <el-form :model="searchForm" label-width="0" inline> + <el-form-item> + <el-select v-model="searchForm.detailType" :clearable="false" filterable> + <el-option + v-for="item in detailTypeOptions" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + </el-form-item> + <el-form-item v-if="searchForm.detailType == 1"> + <el-date-picker + v-model="searchForm.month" + type="month" + format="YYYY-MM" + value-format="YYYY-MM" + placeholder="选择年月" + :clearable="false" + /> + </el-form-item> + <el-form-item v-if="[2, 3].includes(searchForm.detailType)"> + <el-date-picker + v-model="searchForm.year" + type="year" + format="YYYY" + value-format="YYYY" + placeholder="选择年" + :clearable="false" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleSearch">查询</el-button> + <el-button @click="handleReset">重置</el-button> + </el-form-item> + </el-form> + <el-table :data="tableList" border stripe> + <el-table-column label="周期" prop="period" width="110px" /> + <el-table-column prop="signNumber" label="成交数" /> + <el-table-column prop="signPrice" label="成交额" /> + <el-table-column prop="signPriceSequentialGrowth" label="成交额环比增长" /> + <el-table-column prop="signPriceSamePeriodCompareGrowth" label="成交额同比增长" /> + <el-table-column prop="profit" label="利润" /> + <el-table-column prop="profitSequentialGrowth" label="利润环比增长" /> + <el-table-column prop="profitSamePeriodCompareGrowth" label="利润同比增长" /> + <el-table-column prop="notReceivedMoney" label="未到账金额" /> + <el-table-column prop="receivedMoney" label="已到账金额" /> + <el-table-column prop="waitSettleMoney" label="待结算金额" /> + <el-table-column prop="settledMoney" label="已结算金额" /> + <el-table-column prop="refundAmount" label="已退款金额" /> + <el-table-column prop="remainMoney" label="结余" /> + </el-table> + </ContentWrap> +</template> + +<script setup name="SignListCard"> +import { getList } from '@/api/home/reportSignDetail' +import { formatDate } from '@/utils/formatTime' +import { removeNullField } from '@/utils' + +const detailTypeOptions = [ + { label: '日明细', value: 1 }, + { label: '月明细', value: 2 }, + { label: '季明细', value: 3 }, + { label: '年明细', value: 4 } +] + +onMounted(() => { + handleReset() + handleSearch() +}) + +const searchForm = ref({}) + +function handleReset() { + searchForm.value = { + detailType: 2, + month: formatDate(new Date(), 'YYYY-MM'), + year: new Date().getFullYear() + '' + } +} +const loading = ref(false) +const tableList = ref([]) +async function handleSearch() { + loading.value = true + try { + let params = { detailType: searchForm.value.detailType } + if (params.detailType == 1) { + const d = new Date(searchForm.value.month) + params.month = d.getMonth() + 1 + params.year = d.getFullYear() + } else if ([2, 3].includes(params.detailType)) { + params.year = searchForm.value.year + } + const data = await getList(removeNullField(params)) + tableList.value = data + } finally { + loading.value = false + } +} +</script> + +<style lang="scss" scoped></style> diff --git a/src/views/Home/Comp/SignTotalCard.vue b/src/views/Home/Comp/SignTotalCard.vue new file mode 100644 index 0000000..c7d3bcb --- /dev/null +++ b/src/views/Home/Comp/SignTotalCard.vue @@ -0,0 +1,195 @@ +<template> + <ContentWrap> + <div class="flex items-center"> + <span class="mr-20px">成交概况</span> + <el-dropdown> + <el-button size="small" + >{{ periodTypeOptions[periodType] }} <Icon icon="ep:arrow-down" + /></el-button> + <template #dropdown> + <el-dropdown-menu> + <el-dropdown-item + v-for="(item, index) in periodTypeOptions" + :key="index" + @click="handleSearchTotalInfo(index)" + > + {{ item }} + </el-dropdown-item> + </el-dropdown-menu> + </template> + </el-dropdown> + </div> + <el-row :gutter="20" v-loading="infoLoading"> + <el-col :span="8" :offset="0"> + <div class="statistic-card"> + <el-statistic :value="totalData.signNumber" title="成交数" /> + <div + class="statistic-footer" + :class="periodType != 4 ? 'justify-between' : 'justify-center'" + > + <div class="footer-item"> + <span>环比</span> + <span :class="getColor(totalData.signNumberSequentialGrowth)"> + {{ totalData.signNumberSequentialGrowth }}% + <Icon class="ml-2px" :icon="getIcon(totalData.signNumberSequentialGrowth)" /> + </span> + </div> + <div class="footer-item" v-if="periodType != 4"> + <span>同比</span> + <span :class="getColor(totalData.signNumberPeriodCompareGrowth)"> + {{ totalData.signNumberPeriodCompareGrowth }}% + <Icon class="ml-2px" :icon="getIcon(totalData.signNumberPeriodCompareGrowth)" /> + </span> + </div> + </div> + </div> + </el-col> + <el-col :span="8" :offset="0"> + <div class="statistic-card"> + <el-statistic :value="totalData.signPrice" title="成交额" :precision="2" /> + <div + class="statistic-footer" + :class="periodType != 4 ? 'justify-between' : 'justify-center'" + > + <div class="footer-item"> + <span>环比</span> + <span :class="getColor(totalData.signPriceSequentialGrowth)"> + {{ totalData.signPriceSequentialGrowth }}% + <Icon class="ml-2px" :icon="getIcon(totalData.signPriceSequentialGrowth)" /> + </span> + </div> + <div class="footer-item" v-if="periodType != 4"> + <span>同比</span> + <span :class="getColor(totalData.signPriceSamePeriodCompareGrowth)"> + {{ totalData.signPriceSamePeriodCompareGrowth }}% + <Icon class="ml-2px" :icon="getIcon(totalData.signPriceSamePeriodCompareGrowth)" /> + </span> + </div> + </div> + </div> + </el-col> + <el-col :span="8" :offset="0"> + <div class="statistic-card"> + <el-statistic :value="totalData.profit" title="利润" :precision="2" /> + <div + class="statistic-footer" + :class="periodType != 4 ? 'justify-between' : 'justify-center'" + > + <div class="footer-item"> + <span>环比</span> + <span :class="getColor(totalData.profitSequentialGrowth)"> + {{ totalData.profitSequentialGrowth }}% + <Icon class="ml-2px" :icon="getIcon(totalData.profitSequentialGrowth)" /> + </span> + </div> + <div class="footer-item" v-if="periodType != 4"> + <span>同比</span> + <span :class="getColor(totalData.profitSamePeriodCompareGrowth)"> + {{ totalData.profitSamePeriodCompareGrowth }}% + <Icon class="ml-2px" :icon="getIcon(totalData.profitSamePeriodCompareGrowth)" /> + </span> + </div> + </div> + </div> + </el-col> + </el-row> + </ContentWrap> +</template> + +<script setup name="SignTotalCard"> +import { getInfo } from '@/api/home/reportSignDetail' + +const periodTypeOptions = ['本日', '本周', '本月', '本季', '本年'] + +const periodType = ref(2) +const totalData = ref({}) + +onMounted(() => { + handleSearchTotalInfo(2) +}) + +const infoLoading = ref(false) +async function handleSearchTotalInfo(index) { + periodType.value = index + infoLoading.value = true + try { + const data = await getInfo({ periodType: periodType.value + 1 }) + totalData.value = data + } finally { + infoLoading.value = false + } +} + +function getIcon(val) { + if (val > 0) { + return 'ep:caret-top' + } else if (val < 0) { + return 'ep:caret-bottom' + } else { + return 'ep:minus' + } +} + +function getColor(val) { + if (val > 0) { + return 'green' + } else if (val < 0) { + return 'red' + } else { + return 'gley' + } +} +</script> + +<style lang="scss" scoped> +.el-col { + text-align: center; +} +:global(h2#card-usage ~ .example .example-showcase) { + background-color: var(--el-fill-color) !important; +} + +.el-statistic { + --el-statistic-content-font-size: 28px; +} + +.statistic-card { + height: 100%; + // padding: 20px; + border-radius: 4px; + background-color: var(--el-bg-color-overlay); +} + +.statistic-footer { + margin: 16px auto 0; + display: flex; + // justify-content: space-between; + align-items: center; + flex-wrap: wrap; + font-size: 12px; + color: var(--el-text-color-regular); + width: 300px; +} + +.statistic-footer .footer-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.statistic-footer .footer-item span:last-child { + display: inline-flex; + align-items: center; + margin-left: 4px; +} + +.green { + color: var(--el-color-success); +} +.gley { + color: var(--el-color-info); +} +.red { + color: var(--el-color-error); +} +</style> diff --git a/src/views/Home/SignReport.vue b/src/views/Home/SignReport.vue new file mode 100644 index 0000000..c65aab5 --- /dev/null +++ b/src/views/Home/SignReport.vue @@ -0,0 +1,13 @@ +<template> + <div> + <SignTotalCard /> + <SignListCard /> + </div> +</template> + +<script setup name="SignReport"> +import SignTotalCard from './Comp/SignTotalCard.vue' +import SignListCard from './Comp/SignListCard.vue' +</script> + +<style lang="scss" scoped></style> diff --git a/src/views/Home/channel.vue b/src/views/Home/channel.vue deleted file mode 100644 index 082b0bf..0000000 --- a/src/views/Home/channel.vue +++ /dev/null @@ -1,7 +0,0 @@ -<template> - <div> </div> -</template> - -<script setup></script> - -<style lang="scss" scoped></style>