|
@@ -1,28 +1,317 @@ |
|
|
import React from 'react'; |
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from 'react'; |
|
|
import { PageContainer } from '@ant-design/pro-layout'; |
|
|
import { PageContainer } from '@ant-design/pro-layout'; |
|
|
import { Button, Card, DatePicker } from 'antd'; |
|
|
|
|
|
|
|
|
import { Button, Card, DatePicker, Col, Row, TreeSelect, Spin, message } from 'antd'; |
|
|
import styles from './index.less'; |
|
|
import styles from './index.less'; |
|
|
|
|
|
import moment from 'moment'; |
|
|
|
|
|
import marketAPI from "./service"; |
|
|
|
|
|
import * as echarts from 'echarts'; |
|
|
const { RangePicker } = DatePicker; |
|
|
const { RangePicker } = DatePicker; |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 会员报表 |
|
|
* 会员报表 |
|
|
* @returns |
|
|
* @returns |
|
|
*/ |
|
|
*/ |
|
|
export default function Index() { |
|
|
export default function Index() { |
|
|
|
|
|
|
|
|
|
|
|
//日期选择下标:0:今天、1:昨天、2:近7天、3、近30天 |
|
|
|
|
|
const [searchDayIndex, setSearchDayIndex] = useState(0); |
|
|
|
|
|
//日期选择器 |
|
|
|
|
|
const [timeRange, setTimeRange] = useState([ |
|
|
|
|
|
moment(moment(new Date(Date.now())).format('YYYY-MM-DD 00:00:00')), |
|
|
|
|
|
moment(moment(new Date(Date.now())).format('YYYY-MM-DD 23:59:59')) |
|
|
|
|
|
]); |
|
|
|
|
|
const [orgTree, setOrgTree] = useState([]); |
|
|
|
|
|
const [showLoading, setShowLoading] = useState(false); |
|
|
|
|
|
const [currentOrg, setCurrentOrg] = useState(""); |
|
|
|
|
|
|
|
|
|
|
|
//会员图表 |
|
|
|
|
|
const [memberReport, setMemberReport] = useState({ |
|
|
|
|
|
userCount: 0, |
|
|
|
|
|
newAddUser: [], |
|
|
|
|
|
repurChase: [] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
//新增会员图表实例 |
|
|
|
|
|
let newMemberObj = null; |
|
|
|
|
|
//复购图表实例 |
|
|
|
|
|
let repurChaseObj = null; |
|
|
|
|
|
|
|
|
|
|
|
//切换时间 |
|
|
|
|
|
const onChangeTime = (dayIndex) => { |
|
|
|
|
|
setSearchDayIndex(dayIndex); |
|
|
|
|
|
let tempDate = []; |
|
|
|
|
|
switch (dayIndex) { |
|
|
|
|
|
case 0: |
|
|
|
|
|
tempDate = [ |
|
|
|
|
|
moment(moment(new Date(Date.now())).format('YYYY-MM-DD 00:00:00')), |
|
|
|
|
|
moment(moment(new Date(Date.now())).format('YYYY-MM-DD 23:59:59')) |
|
|
|
|
|
] |
|
|
|
|
|
break; |
|
|
|
|
|
case 1: |
|
|
|
|
|
tempDate = [ |
|
|
|
|
|
moment(moment(new Date(Date.now() - 24 * 60 * 60 * 1000)).format('YYYY-MM-DD 00:00:00')), |
|
|
|
|
|
moment(moment(new Date(Date.now() - 24 * 60 * 60 * 1000)).format('YYYY-MM-DD 23:59:59')) |
|
|
|
|
|
] |
|
|
|
|
|
break; |
|
|
|
|
|
case 2: |
|
|
|
|
|
tempDate = [ |
|
|
|
|
|
moment(moment(new Date(Date.now() - 24 * 60 * 60 * 1000 * 7)).format('YYYY-MM-DD 00:00:00')), |
|
|
|
|
|
moment(moment(new Date(Date.now())).format('YYYY-MM-DD 23:59:59')), |
|
|
|
|
|
] |
|
|
|
|
|
break; |
|
|
|
|
|
case 3: |
|
|
|
|
|
tempDate = [ |
|
|
|
|
|
moment(moment(new Date(Date.now() - 24 * 60 * 60 * 1000 * 30)).format('YYYY-MM-DD 00:00:00')), |
|
|
|
|
|
moment(moment(new Date(Date.now())).format('YYYY-MM-DD 23:59:59')), |
|
|
|
|
|
] |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
setTimeRange(tempDate); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//获取组织树 |
|
|
|
|
|
const onGetOrgTree = async () => { |
|
|
|
|
|
setShowLoading(true); |
|
|
|
|
|
const response = await marketAPI.getOrgTree(); |
|
|
|
|
|
setShowLoading(false); |
|
|
|
|
|
if (response.statusCode === 200) { |
|
|
|
|
|
const originTree = response.data; |
|
|
|
|
|
onSetOrgTreeStatus(originTree); |
|
|
|
|
|
setOrgTree(originTree); |
|
|
|
|
|
} else { |
|
|
|
|
|
message.error(response.errors || '获取组织树出错'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//设置组织树不可选择状态 |
|
|
|
|
|
const onSetOrgTreeStatus = (originTree) => { |
|
|
|
|
|
originTree.forEach(treeItem => { |
|
|
|
|
|
if (treeItem.children && treeItem.children.length > 0) { |
|
|
|
|
|
onSetOrgTreeStatus(treeItem.children); |
|
|
|
|
|
} else { |
|
|
|
|
|
if (treeItem.type === 2 || treeItem.type === 3) { |
|
|
|
|
|
treeItem.disabled = false; |
|
|
|
|
|
} else { |
|
|
|
|
|
treeItem.disabled = true; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const LoadingCard = () => { |
|
|
|
|
|
return ( |
|
|
|
|
|
<div className={styles['loading-card']}> |
|
|
|
|
|
<Spin size="large" /> |
|
|
|
|
|
</div> |
|
|
|
|
|
) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//初始化会员报表数据 |
|
|
|
|
|
const initMemberReport = async () => { |
|
|
|
|
|
const jsonData = { |
|
|
|
|
|
"startTime": timeRange[0], |
|
|
|
|
|
"endTime": timeRange[1] |
|
|
|
|
|
} |
|
|
|
|
|
setShowLoading(true); |
|
|
|
|
|
const response = await marketAPI.getMemberReport(jsonData); |
|
|
|
|
|
setShowLoading(false); |
|
|
|
|
|
if (response.statusCode === 200) { |
|
|
|
|
|
setMemberReport(response.data); |
|
|
|
|
|
} else { |
|
|
|
|
|
message.error(response.errors || '获取会员报表失败'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//初始化新增会员图表 |
|
|
|
|
|
const initNewMemberCharts = () => { |
|
|
|
|
|
const chartDom = document.getElementById('new-member'); |
|
|
|
|
|
if (!newMemberObj) { |
|
|
|
|
|
newMemberObj = echarts.init(chartDom); |
|
|
|
|
|
} |
|
|
|
|
|
const xAxisData = []; |
|
|
|
|
|
const seriesData = []; |
|
|
|
|
|
memberReport.newAddUser.forEach(item => { |
|
|
|
|
|
const date = new Date(item.userKey); |
|
|
|
|
|
xAxisData.push(date.toLocaleString()); |
|
|
|
|
|
seriesData.push(item.userValue); |
|
|
|
|
|
}); |
|
|
|
|
|
const option = { |
|
|
|
|
|
title: { |
|
|
|
|
|
text: '新增会员' |
|
|
|
|
|
}, |
|
|
|
|
|
xAxis: { |
|
|
|
|
|
type: 'category', |
|
|
|
|
|
data: xAxisData |
|
|
|
|
|
}, |
|
|
|
|
|
yAxis: { |
|
|
|
|
|
type: 'value' |
|
|
|
|
|
}, |
|
|
|
|
|
series: [ |
|
|
|
|
|
{ |
|
|
|
|
|
data: seriesData, |
|
|
|
|
|
type: 'bar' |
|
|
|
|
|
} |
|
|
|
|
|
] |
|
|
|
|
|
}; |
|
|
|
|
|
newMemberObj.setOption(option); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//复购次数图表 |
|
|
|
|
|
const initRepurChaseCharts = () => { |
|
|
|
|
|
const chartDom = document.getElementById('repur-chase'); |
|
|
|
|
|
if (!repurChaseObj) { |
|
|
|
|
|
repurChaseObj = echarts.init(chartDom); |
|
|
|
|
|
} |
|
|
|
|
|
const xAxisData = []; |
|
|
|
|
|
const seriesData = []; |
|
|
|
|
|
memberReport.repurChase.forEach(item => { |
|
|
|
|
|
xAxisData.push(item.userKey); |
|
|
|
|
|
seriesData.push(item.userValue); |
|
|
|
|
|
}); |
|
|
|
|
|
const option = { |
|
|
|
|
|
title: { |
|
|
|
|
|
text: '复购次数' |
|
|
|
|
|
}, |
|
|
|
|
|
xAxis: { |
|
|
|
|
|
type: 'category', |
|
|
|
|
|
data: xAxisData |
|
|
|
|
|
}, |
|
|
|
|
|
yAxis: { |
|
|
|
|
|
type: 'value' |
|
|
|
|
|
}, |
|
|
|
|
|
series: [ |
|
|
|
|
|
{ |
|
|
|
|
|
data: seriesData, |
|
|
|
|
|
type: 'line', |
|
|
|
|
|
smooth: true |
|
|
|
|
|
} |
|
|
|
|
|
] |
|
|
|
|
|
}; |
|
|
|
|
|
repurChaseObj.setOption(option); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//重置 |
|
|
|
|
|
const onResetSearch = () => { |
|
|
|
|
|
setCurrentOrg(""); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
onGetOrgTree(); |
|
|
|
|
|
initMemberReport(); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
initNewMemberCharts(); |
|
|
|
|
|
initRepurChaseCharts(); |
|
|
|
|
|
window.onresize = () => { |
|
|
|
|
|
if (newMemberObj && repurChaseObj) { |
|
|
|
|
|
newMemberObj.resize(); |
|
|
|
|
|
repurChaseObj.resize(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}, [memberReport]); |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<PageContainer> |
|
|
<PageContainer> |
|
|
|
|
|
{showLoading ? <LoadingCard></LoadingCard> : null} |
|
|
<Card className={styles['data-search-card']}> |
|
|
<Card className={styles['data-search-card']}> |
|
|
<div className={styles['data-search-box']}> |
|
|
|
|
|
<RangePicker /> |
|
|
|
|
|
<div className={styles['data-search-btns']}> |
|
|
|
|
|
<Button className={styles['search-btn-item']}>重置</Button> |
|
|
|
|
|
<Button className={styles['search-btn-item']} type="primary">查询</Button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</Card> |
|
|
|
|
|
<Card className={styles['table-card']}> |
|
|
|
|
|
会员报表 |
|
|
|
|
|
|
|
|
<Row gutter={20} className={styles['data-search-row']}> |
|
|
|
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={6} className={styles['data-search-item']}> |
|
|
|
|
|
<RangePicker size='middle' className={styles['data-search-sufixx']} value={timeRange} onChange={(date, dateStrings) => { |
|
|
|
|
|
let tempDate = [ |
|
|
|
|
|
moment(moment(new Date(dateStrings[0])).format('YYYY-MM-DD 00:00:00')), |
|
|
|
|
|
moment(moment(new Date(dateStrings[1])).format('YYYY-MM-DD 23:59:59')), |
|
|
|
|
|
] |
|
|
|
|
|
setTimeRange(tempDate); |
|
|
|
|
|
}} /> |
|
|
|
|
|
</Col> |
|
|
|
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={6} className={styles['data-search-item']}> |
|
|
|
|
|
<div className={searchDayIndex === 0 ? `${styles['data-search-day']} ${styles['search-day-selected']}` : `${styles['data-search-day']}`} onClick={() => onChangeTime(0)}> |
|
|
|
|
|
今天 |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className={searchDayIndex === 1 ? `${styles['data-search-day']} ${styles['search-day-selected']}` : `${styles['data-search-day']}`} onClick={() => onChangeTime(1)}> |
|
|
|
|
|
昨天 |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className={searchDayIndex === 2 ? `${styles['data-search-day']} ${styles['search-day-selected']}` : `${styles['data-search-day']}`} onClick={() => onChangeTime(2)}> |
|
|
|
|
|
近7天 |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className={searchDayIndex === 3 ? `${styles['data-search-day']} ${styles['search-day-selected']}` : `${styles['data-search-day']}`} onClick={() => onChangeTime(3)}> |
|
|
|
|
|
近30天 |
|
|
|
|
|
</div> |
|
|
|
|
|
</Col> |
|
|
|
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={6} className={styles['data-search-item']}> |
|
|
|
|
|
<TreeSelect |
|
|
|
|
|
className={styles['data-search-sufixx']} |
|
|
|
|
|
dropdownStyle={{ |
|
|
|
|
|
maxHeight: 400, |
|
|
|
|
|
overflow: 'auto', |
|
|
|
|
|
}} |
|
|
|
|
|
value={currentOrg.title} |
|
|
|
|
|
treeData={orgTree} |
|
|
|
|
|
onSelect={(value, node) => { |
|
|
|
|
|
if (node.type === 2 || node === 3) { |
|
|
|
|
|
setCurrentOrg(node); |
|
|
|
|
|
} else { |
|
|
|
|
|
setCurrentOrg(""); |
|
|
|
|
|
} |
|
|
|
|
|
}} |
|
|
|
|
|
placeholder="请选择组织架构" |
|
|
|
|
|
treeDefaultExpandAll |
|
|
|
|
|
/> |
|
|
|
|
|
</Col> |
|
|
|
|
|
<Col xs={24} sm={24} md={12} lg={12} xl={6} className={styles['data-search-item']}> |
|
|
|
|
|
<div className={styles['data-search-btns']}> |
|
|
|
|
|
<Button className={styles['search-btn-item']} onClick={onResetSearch}>重置</Button> |
|
|
|
|
|
<Button className={styles['search-btn-item']} type="primary" onClick={initMemberReport}>查询</Button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</Col> |
|
|
|
|
|
</Row> |
|
|
</Card> |
|
|
</Card> |
|
|
|
|
|
<Row gutter={10} className={styles['member-row-common']}> |
|
|
|
|
|
<Col xs={24} sm={24} md={24} lg={8} xl={6}> |
|
|
|
|
|
<Card className={styles['member-card']}> |
|
|
|
|
|
<div className={styles['member-card-box']}> |
|
|
|
|
|
<div className={styles['member-card-prefix']}> |
|
|
|
|
|
会员总量 |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className={styles['member-card-sufixx']}> |
|
|
|
|
|
{memberReport.userCount} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className={styles['member-card-box']}> |
|
|
|
|
|
<div className={styles['member-card-prefix']}> |
|
|
|
|
|
新增会员 |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className={styles['member-card-sufixx']}> |
|
|
|
|
|
{memberReport.newAddUser.length} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className={styles['member-card-box']}> |
|
|
|
|
|
<div className={styles['member-card-prefix']}> |
|
|
|
|
|
复购数量 |
|
|
|
|
|
</div> |
|
|
|
|
|
<div className={styles['member-card-sufixx']}> |
|
|
|
|
|
{memberReport.repurChase.length} |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</Card> |
|
|
|
|
|
</Col> |
|
|
|
|
|
<Col xs={24} sm={24} md={24} lg={16} xl={18}> |
|
|
|
|
|
<Card> |
|
|
|
|
|
{/* 新增会员图表 */} |
|
|
|
|
|
<div id="new-member" className={styles['new-member']}></div> |
|
|
|
|
|
</Card> |
|
|
|
|
|
</Col> |
|
|
|
|
|
</Row> |
|
|
|
|
|
<Row> |
|
|
|
|
|
<Col xs={24} sm={24} md={24} lg={24} xl={24} > |
|
|
|
|
|
<Card> |
|
|
|
|
|
{/* 复购次数图表 */} |
|
|
|
|
|
<div id="repur-chase" className={styles['repur-chase']}></div> |
|
|
|
|
|
</Card> |
|
|
|
|
|
</Col> |
|
|
|
|
|
</Row> |
|
|
</PageContainer> |
|
|
</PageContainer> |
|
|
) |
|
|
) |
|
|
} |
|
|
} |