Commit 7a52d2bd by 黑潮

Merge branch 'feature/销售线索' of http://115.159.76.241/marketing-web/marketing into feature/销售线索

# Conflicts:
#	src/views/ecm/list.vue
parents 1b9aa362 c39feeb1
......@@ -10,8 +10,11 @@
<el-breadcrumb-item :class="{ 'no-link': !v.path }" v-for="(v, i) in breadcrumb" :key="i" :to="{ path: v.path }">{{ v.name }}</el-breadcrumb-item>
</el-breadcrumb>
<h3>
<span>{{ contentTitle }}</span>
<div v-html="layoutTips" class="layout--tips--wrap"></div>
<div>
<span>{{ contentTitle }}</span>
<div v-html="layoutTips" class="layout--tips--wrap"></div>
</div>
<div v-if="$route.path.indexOf('ecm') >= 0 && xsxsFlag == 1" class="intro_wrap"><img :src="require('@/assets/img/introlIcon.png')" class="introlIcon" alt="" /><span class="intro" @click="() => (drawer = true)">指标说明</span></div>
</h3>
</div>
<div class="layout-content__wrap">
......@@ -22,20 +25,27 @@
<vue-gic-footer></vue-gic-footer>
</div>
</div>
<description :drawer.sync="drawer" :direction="direction" :contentTitle="contentTitle" />
</div>
</template>
<script>
import asideMenu from '../aside-menu';
import description from '@/views/ecm/touch-components/description.vue';
import { getXsxsFalg } from '@/service/api/ecmApi.js';
export default {
components: {
asideMenu
asideMenu,
description
},
data() {
return {
collapseFlag: false,
projectName: 'marketing',
leftModulesName: '公众号配置',
bodyHeight: 0
bodyHeight: 0,
drawer: false,
direction: 'rtl',
xsxsFlag: 0
};
},
mounted() {
......@@ -71,6 +81,22 @@ export default {
// 折叠事件
collapseTagHandler(val) {
this.collapseFlag = val;
},
setXsxsFlag() {
getXsxsFalg().then(res => {
this.xsxsFlag = res.result;
});
}
},
watch: {
$route: {
handler: function(to) {
if (to.path.indexOf('ecm') >= 0) {
this.setXsxsFlag();
}
this.drawer = false;
},
immediate: true
}
}
};
......@@ -127,6 +153,28 @@ export default {
font-size: 20px;
padding: 24px 0;
font-weight: 500;
display: flex;
justify-content: space-between;
.intro_wrap {
display: flex;
align-items: center;
.introlIcon {
width: 20px;
height: 20px;
margin-right: 8px;
}
.intro {
width: 64px;
height: 22px;
font-size: 16px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #1890ff;
line-height: 22px;
cursor: pointer;
padding-right: 30px;
}
}
}
}
&-content__wrap {
......
......@@ -65,6 +65,24 @@ export default {
path: '/ecm/list',
type: 'currrent'
}
},
{
path: 'touch/:id',
name: '触达效果',
component: () => import(/* webpackChunkName: "ecm" */ '../../views/ecm/touch-effect.vue'),
meta: {
path: '/ecm/list',
type: 'info'
}
},
{
path: 'clue',
name: '线索页',
component: () => import(/* webpackChunkName: "ecm" */ '../../views/ecm/touch-clue.vue'),
meta: {
path: '/ecm/list',
type: 'info'
}
}
]
};
......
......@@ -43,6 +43,20 @@ export const exportBatchSendDetails = config.api + PREFIX + 'export-batch-send-d
// 智能营销--实时人员列表--导出csv
export const exportCurrentSendDetails = config.api + PREFIX + 'export-current-send-details';
// 智能营销--触达效果
export const ecmTouchEffectColumnDiagram = params => requests(PREFIX + 'ecmTouchEffectColumnDiagram', params);
export const ecmTouchEffectTable = params => requests(PREFIX + 'ecmTouchEffectTable', params);
export const ecmTouchEffectFunnelChart = params => requests(PREFIX + 'ecmTouchEffectFunnelChart', params);
// 智能营销--导购线索
export const ecmGuideCluesColumnDiagram = params => requests(PREFIX + 'ecmGuideCluesColumnDiagram', params);
export const ecmGuideCluesTable = params => requests(PREFIX + 'ecmGuideCluesTable', params);
export const ecmGuideCluesTaskTable = params => requests(PREFIX + 'ecmGuideCluesTaskTable', params); // 任务完成情况表格
// 智能营销--后台线索
export const ecmHeadCluesColumnDiagram = params => requests(PREFIX + 'ecmHeadCluesColumnDiagram', params);
export const ecmHeadGuideCluesTable = params => requests(PREFIX + 'ecmHeadGuideCluesTable', params);
export const ecmHeadCluesTaskTab = params => requests(PREFIX + 'ecmHeadCluesTaskTab', params); // 表格
export const ecmHeadCluesTaskTabHead = params => requests(PREFIX + 'ecmHeadCluesTaskTabHead', params); //表头
export const getUseStoredFalg = params => requests(PREFIX + 'get-ecm-store-flag', params);
export const getXsxsFalg = params => requests(PREFIX + 'get-xsxs-open-flag', params);
......@@ -23,7 +23,7 @@
</template>
</el-table-column>
<el-table-column min-width="100" align="left" prop="creatorName" label="创建人" v-if="$store.state.marketing.openFlag"></el-table-column>
<el-table-column label="操作" align="left" width="220" fixed="right">
<el-table-column label="操作" align="left" width="260" fixed="right">
<template slot-scope="scope">
<template v-if="scope.row.canEdit !== false">
<el-button type="text" v-if="scope.row.effectType == 0 || scope.row.effectType == 1" @click="editData(scope.row)">编辑</el-button>
......@@ -36,6 +36,7 @@
<el-button type="text">删除</el-button>
</dm-delete>
<el-button type="text" @click="toRecord(scope.row)">记录</el-button>
<el-button type="text" @click="toTouch(scope.row)" v-if="xsxsFlag && isOpenFlag(scope.row.analyseJson)">触达效果</el-button>
</template>
</el-table-column>
</el-table>
......@@ -51,8 +52,18 @@ export default {
name: 'ecm',
data() {
return {
effectTypeOption: [{ value: '', label: '所有时效' }, { value: 0, label: '触点' }, { value: 1, label: '重复' }, { value: 2, label: '单次' }], // eslint-disable-line
onlineOptions: [{ value: '', label: '所有上线状态' }, { value: 0, label: '待上线' }, { value: 1, label: '已上线' }, { value: 2, label: '已下线' }], // eslint-disable-line
effectTypeOption: [
{ value: '', label: '所有时效' },
{ value: 0, label: '触点' },
{ value: 1, label: '重复' },
{ value: 2, label: '单次' }
], // eslint-disable-line
onlineOptions: [
{ value: '', label: '所有上线状态' },
{ value: 0, label: '待上线' },
{ value: 1, label: '已上线' },
{ value: 2, label: '已下线' }
], // eslint-disable-line
marketingTypeOptions,
listParams: {
effectType: '', // 全部失效的默认值是-1,提交时判断''改为-1
......@@ -64,6 +75,7 @@ export default {
pageSize: 20,
showSelfFlag: ''
},
xsxsFlag: 0, // 销售线索开关 1 开 0 关
total: 0,
loading: false,
tableHeader: [
......@@ -121,6 +133,50 @@ export default {
}
return result;
}
},
{
label: '计划触达人数',
prop: 'ecmMemberNum',
minWidth: '120',
align: 'left',
formatter(row) {
let openFlag = JSON.parse(row.analyseJson);
if (!openFlag) openFlag = {};
return openFlag.open_flag == 0 || (!row.ecmMemberNum && row.ecmMemberNum != 0) ? '--' : row.ecmMemberNum;
}
},
{
label: '触达人数',
prop: 'ecmSuccessNum',
minWidth: '120',
align: 'left',
formatter(row) {
let openFlag = JSON.parse(row.analyseJson);
if (!openFlag) openFlag = {};
return openFlag.open_flag == 0 || (!row.ecmSuccessNum && row.ecmSuccessNum != 0) ? '--' : row.ecmSuccessNum;
}
},
{
label: '转换人数',
prop: 'ecmTranNum',
minWidth: '120',
align: 'left',
formatter(row) {
let openFlag = JSON.parse(row.analyseJson);
if (!openFlag) openFlag = {};
return openFlag.open_flag == 0 || (!row.ecmTranNum && row.ecmTranNum != 0) ? '--' : row.ecmTranNum;
}
},
{
label: '线索转化收益',
prop: 'ecmTranIncome',
minWidth: '120',
align: 'left',
formatter(row) {
let openFlag = JSON.parse(row.analyseJson);
if (!openFlag) openFlag = {};
return openFlag.open_flag == 0 || (!row.ecmTranIncome && row.ecmTranIncome != 0) ? '--' : row.ecmTranIncome;
}
}
],
tableList: []
......@@ -130,12 +186,6 @@ export default {
activitySelect
},
created() {
getXsxsFalg().then(res => {
this.xsxsFlag = res.result;
if (this.xsxsFlag) {
this.marketingTypeOptions.find(el => el.value == 'qywx').visible = true;
}
});
getUseStoredFalg().then(res => {
if (res.result.useStoredFlag) {
this.marketingTypeOptions.find(el => el.value == 'grade').visible = true;
......@@ -143,12 +193,11 @@ export default {
});
this.loadEcmList();
this.$store.commit('aside_handler', false);
this.$store.commit('mutations_breadcrumb', [{ name: '营销管理', path: '' }, { name: '智能营销', path: '/ecm' }]); // eslint-disable-line
},
computed: {
showMarketingTypeOptions() {
return this.marketingTypeOptions.filter(el => el.visible == true);
}
this.$store.commit('mutations_breadcrumb', [
{ name: '营销管理', path: '' },
{ name: '智能营销', path: '/ecm' }
]); // eslint-disable-line
this.setXsxsFlag();
},
methods: {
search() {
......@@ -170,7 +219,6 @@ export default {
params.effectType = -1; // 全部时效的默认值是-1
}
let res = await loadEcmList(params);
console.log(res);
this.tableList = res.result.result || [];
this.total = res.result.totalCount;
this.loading = false;
......@@ -184,6 +232,10 @@ export default {
const prefix = row.effectType == 1 ? 'batchlist' : row.effectType == 2 ? 'oncelist' : 'currentlist';
this.$router.push({ path: `/ecm/${prefix}/${row.ecmPlanId}`, query: { name: row.ecmPlanName } });
},
// 触达效果
toTouch(row) {
this.$router.push({ path: `/ecm/touch/${row.ecmPlanId}`, query: { name: row.ecmPlanName } });
},
// 删除
async delData(row) {
try {
......@@ -214,6 +266,40 @@ export default {
} catch (err) {
this.$tips({ type: 'error', message: '下线失败!' });
}
},
setXsxsFlag() {
// 是否开启销售线索
getXsxsFalg().then(res => {
this.xsxsFlag = res.result;
if (this.xsxsFlag == 0) {
let arr = [];
this.tableHeader.forEach(item => {
if (item.label != '线索转化收益' && item.label != '转换人数' && item.label != '触达人数' && item.label != '计划触达人数') {
arr.push(item);
}
});
this.tableHeader = arr;
}
if (this.xsxsFlag) {
this.marketingTypeOptions.find(el => el.value == 'qywx').visible = true;
}
});
}
},
computed: {
isOpenFlag(item) {
return item => {
item = JSON.parse(item);
if (!item) item = {};
if (item.open_flag == 0) {
return false;
} else {
return true;
}
};
},
showMarketingTypeOptions() {
return this.marketingTypeOptions.filter(el => el.visible == true);
}
}
};
......
<template>
<div class="funnel_wrap">
<div :id="nodeName"></div>
<div class="funnelDesc">
<p>
<span>{{ data[0].action }}</span>
<span class="num">{{ data[0].value }}</span>
</p>
<p>
<span class="per">{{ data[1].action }}</span>
<span class="num">{{ data[1].value }}</span>
<span class="rate">
<span>/</span>
<span>{{ data[1].rateAction }}</span>
<span class="rateNum">{{ data[1].rate }}</span>
</span>
</p>
<p>
<span class="per">{{ data[2].action }}</span>
<span class="num">{{ data[2].value }}</span>
<span class="rate">
<span>/</span>
<span>{{ data[2].rateAction }}</span>
<span class="rateNum">{{ data[2].rate }}</span>
</span>
</p>
</div>
</div>
</template>
<script>
import * as G2 from '@antv/g2';
export default {
name: 'funnel',
props: {
nodeName: String,
colorArr: Array,
data: {
type: Array
}
},
mounted() {
this.funnel();
},
methods: {
funnel() {
var chart = new G2.Chart({
container: this.nodeName,
forceFit: true,
height: 144,
padding: [0, 0, 0]
});
chart.source(this.data);
chart.axis(false);
chart.legend(false);
chart.tooltip({
showTitle: false,
itemTpl: '<li data-index={index} style="display:flex;">' + '<span style="background-color:{color};margin-top:8px;" class="g2-tooltip-marker"></span>' + '<div><p>{name} {value}</p><p>{rateAction} {rate}</p></div>' + '</li>'
});
chart
.coord('rect')
.transpose()
.scale(1, -1);
chart
.intervalSymmetric()
.position('action*value')
.shape('funnel')
.color('action', this.colorArr)
.tooltip('action*value*rate*rateAction', function(action, value, rate, rateAction) {
return {
name: action,
rate: rate,
value: value,
rateAction: rateAction
};
});
chart.render();
}
}
};
</script>
<style scoped lang="scss">
.funnel_wrap {
display: flex;
#funnel1,
#funnel2,
#funnel3 {
width: 219px;
height: 144px;
}
.funnelDesc {
padding: 14px 0 13px 10px;
p {
display: flex;
align-items: center;
height: 20px;
line-height: 20px;
&:nth-child(1) {
// margin: 0 0 23px 15px;
margin-bottom: 23px;
// margin-left: 15px;
}
&:nth-child(2) {
// margin: 0 0 33px -14px;
margin-bottom: 33px;
// margin-left: -14px;
}
&:nth-child(3) {
// margin: 0 0 0 -44px;
}
}
span {
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #303133;
white-space: nowrap;
}
.rate {
height: 22px;
display: inline-block;
display: flex;
justify-content: space-between;
span:nth-child(2) {
margin: 0 8px;
}
}
.num {
font-size: 18px;
font-family: DINAlternate-Bold, DINAlternate;
font-weight: bold;
color: #303133;
line-height: 21px;
margin: 0 8px 0 15px;
}
.rateNum {
font-size: 18px;
font-family: DINAlternate-Bold, DINAlternate;
font-weight: bold;
}
}
}
</style>
<template>
<div class="content">
<div class="top" v-if="!isCluePage">
<div class="left">计划:踏青特惠</div>
<div class="right" v-if="isRepeat">批次合计:3 / 最新批次时间:2021-03-25:12:22:33</div>
</div>
<div class="middle" v-for="(item, index) in data" :key="index">
<template v-if="item.isSales == 0">
<div class="left" v-if="isReference && !isCluePage">
<img :src="require('@/assets/img/experimentIcon.png')" class="icon" />
<span class="title">实验组</span>
<span>-计划触达</span>
</div>
</template>
<template v-else>
<div class="left" v-if="isReference && !isCluePage">
<img :src="require('@/assets/img/referenceIcon.png')" class="icon" />
<span class="title">参照组</span>
<span>-计划非触达</span>
</div>
</template>
<!--非线索页列表-->
<div class="right" :class="[isCluePage ? 'cluePage' : '']">
<div v-if="!isCluePage">
<p>计划人次</p>
<p>{{ item.planMbrTimes.toLocaleString() }}</p>
</div>
<div>
<p>{{ isCluePage ? '计划触达人数' : '计划人数' }}</p>
<p>{{ item.planMbrNum.toLocaleString() }}</p>
</div>
<div v-if="isCluePage && item.flag">
<p>
任务完成率<span>(任务总数 {{ item.taskCnt.toLocaleString() }})</span>
</p>
<p>{{ item.taskRate ? item.taskRate.toFixed(2) + '%' : '-' }}</p>
</div>
<div>
<p>
触达人数<span>(触达率 {{ item.touchRate + '%' }})</span>
</p>
<p>{{ item.touchMbrNum.toLocaleString() }}</p>
</div>
<div>
<p>
转化人数<span :class="{ active: item.isSales == 0 && data[0].transformRate < data[1].transformRate }">(转化率 {{ item.transformRate + '%' }})</span>
</p>
<p>{{ item.convMbrNum.toLocaleString() }}</p>
</div>
<div>
<p>转化订单数</p>
<p>{{ item.convOrderCnt.toLocaleString() }}</p>
</div>
<div>
<p>转化收益</p>
<p>{{ convSalesAmt(item.convSalesAmt) }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'market-list',
props: {
// 是否是线索页面
isCluePage: {
type: Boolean,
default: false
},
// 是否重复营销
isRepeat: {
type: Boolean,
default: true
},
// 是否勾选参照组
isReference: {
type: Boolean,
default: true
},
// 数据
data: {
type: Array,
default: () => []
}
},
computed: {
convSalesAmt(num) {
return num => {
if (num) {
num = parseFloat(num).toFixed(2);
let i = num.indexOf('.');
let before = parseInt(num.slice(0, i)).toLocaleString();
let last = num.slice(i);
return before + last;
} else {
return '0';
}
};
}
}
};
</script>
<style lang="scss" scoped>
.content {
font-family: PingFangSC-Medium, PingFang SC;
margin-left: 8px;
margin-right: 13px;
.top {
color: #303133;
height: 50px;
background: #f5f7fa;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
margin-top: 10px;
.left {
font-size: 16px;
font-weight: 600;
line-height: 22px;
padding-left: 27px;
}
.right {
font-family: PingFangSC-Regular, PingFang SC;
font-size: 14px;
font-weight: 400;
line-height: 20px;
padding-right: 16px;
}
}
.middle {
margin-bottom: 4px;
height: 100px;
background: #f0f5ff;
border-radius: 6px;
display: flex;
align-items: center;
.left {
width: 226px;
height: 55px;
box-sizing: border-box;
display: flex;
align-items: center;
padding: 0 35px 0 25px;
border-right: 1px solid#E4E7ED;
span {
font-size: 14px;
font-weight: 400;
color: #606266;
line-height: 20px;
}
.title {
font-weight: 600;
color: #303133;
margin-right: 4px;
}
.icon {
width: 40px;
height: 40px;
background: rgba(144, 188, 255, 0.21);
border-radius: 4px;
margin-right: 15px;
}
}
.right {
padding: 0 28px 0 32px;
height: 58px;
flex: 1;
display: flex;
justify-content: space-between;
div {
p {
&:nth-child(1) {
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #606266;
line-height: 20px;
padding-top: 4px;
margin-bottom: 6px;
}
&:nth-child(2) {
font-size: 24px;
font-family: DINAlternate-Bold, DINAlternate;
font-weight: bold;
color: #303133;
line-height: 28px;
}
.active {
color: #f5222d;
margin-left: 4px;
}
}
}
}
.cluePage {
padding-left: 65px;
}
&:nth-of-type(3) {
background: #f5f7fa;
margin-bottom: 7px;
.left {
padding-right: 21px;
}
}
}
}
</style>
<template>
<div class="draw">
<div id="draw_g2"></div>
</div>
</template>
<script>
import * as G2 from '@antv/g2';
import { ecmTouchEffectColumnDiagram, ecmGuideCluesColumnDiagram, ecmHeadGuideCluesTable } from '@/service/api/ecmApi.js';
export default {
name: 'touch-charts',
props: {
type: String // 0是触达效果 1导购线索 2后台线索
},
data() {
return {
chartData: []
};
},
mounted() {
this.getChartData();
},
methods: {
draw() {
let valueArr = this.chartData.filter(item => item.value >= 0);
let rateArr = this.chartData.filter(item => item.rate >= 0);
let valueFlag = valueArr.every(item => item.value == 0); // value是否出现全为0的情况
let rateFlag = rateArr.every(item => item.rate == 0); // rate是否出现全为0的情况
const chart = new G2.Chart({
container: 'draw_g2',
forceFit: true,
height: 376,
padding: [55, 90, 46, 68]
});
chart.source(this.chartData);
chart.tooltip({
showMarkers: false,
shared: true
});
chart.legend({
position: 'top',
offsetY: -20
});
chart.axis('value', {
grid: {
type: 'line',
lineStyle: {
stroke: '#d9d9d9',
lineWidth: 1,
lineDash: [-1, -1]
}
}
});
chart.scale({
date: {
dataKey: 'date',
type: 'cat'
},
value: {
min: 0,
ticks: valueFlag ? [0, 2, 4, 6, 8] : '',
tickCount: 5
},
rate: {
min: 0,
ticks: rateFlag ? [0, 2, 4, 6, 8] : '',
tickCount: 5
}
});
chart.axis('rate', {
label: {
formatter: text => {
let num = Number(text);
if (num > 10000) {
return num / 10000 + '万';
}
return num + '元';
}
},
grid: null
});
chart
.interval()
.position('date*value')
.color('name', ['rgba(91, 143, 249, 0.85)', 'rgba(90, 216, 166, 0.85)', '#FF9F40'])
.adjust([
{
type: 'dodge',
marginRatio: 0.1
}
]); // eslint-disable-next-line
chart
.line()
.position('date*rate')
.color('name', ['#FF9F40']);
chart.render();
},
getChartData() {
let meth;
if (this.type == 0) meth = ecmTouchEffectColumnDiagram;
else if (this.type == 1) meth = ecmGuideCluesColumnDiagram;
else meth = ecmHeadGuideCluesTable;
meth({ ecmPlanId: 1 }).then(res => {
this.chartData = res.result.map(item => {
if (item.name == '线索转化收益') {
item.rate = item.vaule * 1;
delete item.vaule;
} else {
item.value = item.vaule * 1;
delete item.vaule;
}
return item;
});
this.draw();
});
}
}
};
</script>
<style lang="scss" scoped>
.draw {
.title {
padding-left: 12px;
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 700;
color: #303133;
line-height: 22px;
margin-bottom: 36px;
}
#draw_g2 {
height: 376px;
margin: 0 auto;
position: relative;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: rgba(0, 0, 0, 0.45);
line-height: 17px;
&::before {
content: '人数';
position: absolute;
top: 10px;
left: 41px;
}
&::after {
content: '金额';
position: absolute;
top: 10px;
right: 57px;
}
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment