@@ -587,7 +587,7 @@ export default [ | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '营销报表', | |||
name: '营收报表', | |||
path: '/order/order-market-report', | |||
component: './order/order-market-report', | |||
access: 'k31', | |||
@@ -605,7 +605,7 @@ export default [ | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '营收报表', | |||
name: '营销报表', | |||
path: '/order/order-revenue-statement', | |||
component: './order/order-revenue-statement', | |||
access: 'k31', | |||
@@ -63,6 +63,7 @@ | |||
"braft-editor": "^2.3.9", | |||
"classnames": "^2.2.6", | |||
"cos-js-sdk-v5": "^1.3.5", | |||
"echarts": "^5.3.3", | |||
"js-export-excel": "^1.1.4", | |||
"linq": "^4.0.0", | |||
"lodash": "^4.17.11", | |||
@@ -571,7 +571,7 @@ export async function getInitialState() { | |||
access: 'k32', | |||
}, | |||
{ | |||
name: '营销报表', | |||
name: '营收报表', | |||
path: '/order/order-market-report', | |||
component: './order/order-market-report', | |||
access: 'k31', | |||
@@ -583,13 +583,7 @@ export async function getInitialState() { | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '订单报表详情', | |||
path: '/order/order-report/order-report-detail', | |||
component: './order/order-report-detail', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '营收报表', | |||
name: '营销报表', | |||
path: '/order/order-revenue-statement', | |||
component: './order/order-revenue-statement', | |||
access: 'k31', | |||
@@ -0,0 +1,79 @@ | |||
import React, { useEffect } from 'react'; | |||
import { Button, Card, DatePicker, Col, Row, TreeSelect, Spin } from 'antd'; | |||
import styles from './index.less'; | |||
import moment from 'moment'; | |||
const { RangePicker } = DatePicker; | |||
/** | |||
* 条件查询通用头部 | |||
* @returns | |||
*/ | |||
export default function Index(props) { | |||
const LoadingCard = () => { | |||
return ( | |||
<div className={styles['loading-card']}> | |||
<Spin size="large" /> | |||
</div> | |||
) | |||
} | |||
return ( | |||
<div> | |||
{props.showLoading ? <LoadingCard></LoadingCard> : null} | |||
<Card className={styles['data-search-card']}> | |||
<Row gutter={20} className={styles['data-search-row']}> | |||
<Col xs={24} sm={24} md={24} lg={12} xl={6} className={styles['data-search-item']}> | |||
<RangePicker size='middle' className={styles['data-search-sufixx']} value={props.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')), | |||
] | |||
props.onTimePickerChange(tempDate); | |||
}} /> | |||
</Col> | |||
<Col xs={24} sm={24} md={24} lg={12} xl={6} className={styles['data-search-item']}> | |||
<div className={props.searchDayIndex === 0 ? `${styles['data-search-day']} ${styles['search-day-selected']}` : `${styles['data-search-day']}`} onClick={() => props.onChangeTimeIndex(0)}> | |||
今天 | |||
</div> | |||
<div className={props.searchDayIndex === 1 ? `${styles['data-search-day']} ${styles['search-day-selected']}` : `${styles['data-search-day']}`} onClick={() => props.onChangeTimeIndex(1)}> | |||
昨天 | |||
</div> | |||
<div className={props.searchDayIndex === 2 ? `${styles['data-search-day']} ${styles['search-day-selected']}` : `${styles['data-search-day']}`} onClick={() => props.onChangeTimeIndex(2)}> | |||
近7天 | |||
</div> | |||
<div className={props.searchDayIndex === 3 ? `${styles['data-search-day']} ${styles['search-day-selected']}` : `${styles['data-search-day']}`} onClick={() => props.onChangeTimeIndex(3)}> | |||
近30天 | |||
</div> | |||
</Col> | |||
<Col xs={24} sm={24} md={24} lg={12} xl={6} className={styles['data-search-item']}> | |||
<TreeSelect | |||
className={styles['data-search-sufixx']} | |||
dropdownStyle={{ | |||
maxHeight: 400, | |||
overflow: 'auto', | |||
}} | |||
value={props.currentOrg.title} | |||
treeData={props.orgTree} | |||
onSelect={(value, node) => { | |||
if (node.type === 2 || node === 3) { | |||
props.onCurrentOrgChange(node); | |||
} else { | |||
props.onCurrentOrgChange(""); | |||
} | |||
}} | |||
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={props.onResetSearch}>重置</Button> | |||
<Button className={styles['search-btn-item']} type="primary" onClick={props.onQueryBtn}>查询</Button> | |||
</div> | |||
</Col> | |||
</Row> | |||
</Card> | |||
</div> | |||
) | |||
} |
@@ -0,0 +1,93 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-row { | |||
margin-bottom: 10px; | |||
} | |||
.data-search-item { | |||
display: flex; | |||
align-items: center; | |||
margin-bottom: 10px; | |||
} | |||
.data-search-prefix { | |||
margin-right: 10px; | |||
font-size: 18px; | |||
} | |||
.search-btn-item { | |||
margin-right: 20px; | |||
} | |||
.data-search-day { | |||
padding: 5px 20px; | |||
border: 1px solid #dedede; | |||
cursor: pointer; | |||
} | |||
.data-search-day:nth-child(1), .data-search-day:nth-child(2), .data-search-day:nth-child(3) { | |||
border-right: none; | |||
} | |||
.search-day-selected { | |||
border: 1px solid #FA541C !important; | |||
color: #FA541C; | |||
} | |||
.data-search-sufixx { | |||
width: 100%; | |||
} | |||
// 加载中 | |||
.loading-card { | |||
position: fixed; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
width: 100vw; | |||
height: 100vh; | |||
top: 0; | |||
left: 0; | |||
right: 0; | |||
bottom: 0; | |||
z-index: 999; | |||
background-color: rgba(0, 0, 0, 0.5); | |||
} | |||
.member-card-box { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
margin-bottom: 5px; | |||
} | |||
.member-card-prefix { | |||
font-size: 20px; | |||
font-weight: 700; | |||
color: #999; | |||
font-family: '楷体'; | |||
} | |||
.member-card-sufixx { | |||
font-size: 22px; | |||
} | |||
.new-member { | |||
width: 100%; | |||
height: 250px; | |||
} | |||
.repur-chase { | |||
width: 100%; | |||
height: 250px; | |||
} | |||
.member-card { | |||
height: 300px; | |||
} | |||
.member-row-common { | |||
margin-bottom: 10px; | |||
} |
@@ -33,6 +33,7 @@ export default function Index() { | |||
//门店 | |||
const [storeIdArray, setStoreIdArray] = useState(""); | |||
const [storeSelect, setStoreSelect] = useState([]); | |||
const [storeList, setStoreList] = useState([]); | |||
//商品 | |||
const [goodsIdArray, setGoodsIdArray] = useState([]); | |||
const [goodsIdSelect, setGoodsIdSelect] = useState([]); | |||
@@ -52,9 +53,9 @@ export default function Index() { | |||
dataIndex: 'storeId', | |||
key: 'storeId', | |||
render: (text) => { | |||
const findGoods = storeSelect.find(item => item.id === text); | |||
const findGoods = storeList.find(item => item.key === text); | |||
if (findGoods) { | |||
return <span>{findGoods.name}</span> | |||
return <span>{findGoods.title}</span> | |||
} else { | |||
return <span>暂无门店名称</span> | |||
} | |||
@@ -124,10 +125,11 @@ export default function Index() { | |||
*/ | |||
const onQueryStoreList = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.gettree({}); | |||
const response = await costSalesAPI.gettree(); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setStoreSelect(response.data); | |||
setStoreList(treeArrayToFlat(response.data)); | |||
} else { | |||
message.error('查询店铺列表失败'); | |||
} | |||
@@ -136,7 +138,7 @@ export default function Index() { | |||
//查询商品列表 | |||
const onQueryGoodsList = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.goodsList({}); | |||
const response = await costSalesAPI.goodsList(); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setGoodsIdSelect(response.data); | |||
@@ -148,7 +150,7 @@ export default function Index() { | |||
//查询商品分类类型列表 | |||
const onQueryGoodsType = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.goodsTypeList({}); | |||
const response = await costSalesAPI.goodsTypeList(); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setGoodsTypeSelect(response.data); | |||
@@ -168,6 +170,18 @@ export default function Index() { | |||
]); | |||
} | |||
//树形数据扁平化 | |||
const treeArrayToFlat = (tree, arr = []) => { | |||
tree.forEach(item => { | |||
const { children, ...props } = item; | |||
arr.push(props); | |||
if (children && children.length > 0) { | |||
treeArrayToFlat(children, arr); | |||
} | |||
}); | |||
return arr; | |||
} | |||
useEffect(() => { | |||
onQueryReportSalescost(); | |||
onQueryStoreList(); | |||
@@ -35,6 +35,7 @@ export default function Index() { | |||
//门店 | |||
const [storeIdArray, setStoreIdArray] = useState([]); | |||
const [storeSelect, setStoreSelect] = useState([]); | |||
const [storeList, setStoreList] = useState([]); | |||
const columns = [ | |||
{ | |||
@@ -42,13 +43,12 @@ export default function Index() { | |||
dataIndex: 'storeId', | |||
key: 'storeId', | |||
render: (text) => { | |||
const findGoods = storeSelect.find(item => item.id === text); | |||
const findGoods = storeList.find(item => item.key === text); | |||
if (findGoods) { | |||
return <span>{findGoods.name}</span> | |||
return <span>{findGoods.title}</span> | |||
} else { | |||
return <span>暂无门店名称</span> | |||
} | |||
}, | |||
}, | |||
{ | |||
@@ -92,6 +92,7 @@ export default function Index() { | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setStoreSelect(response.data); | |||
setStoreList(treeArrayToFlat(response.data)); | |||
} else { | |||
message.error('查询店铺列表失败'); | |||
} | |||
@@ -106,6 +107,18 @@ export default function Index() { | |||
]); | |||
} | |||
//树形数据扁平化 | |||
const treeArrayToFlat = (tree, arr = []) => { | |||
tree.forEach(item => { | |||
const { children, ...props } = item; | |||
arr.push(props); | |||
if (children && children.length > 0) { | |||
treeArrayToFlat(children, arr); | |||
} | |||
}); | |||
return arr; | |||
} | |||
useEffect(() => { | |||
onQueryReportSalescost(); | |||
onQueryStoreList(); | |||
@@ -391,7 +391,7 @@ const LoadingCard = () => { | |||
} | |||
/** | |||
* 营销报表 | |||
* 营收报表 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
@@ -1,28 +1,318 @@ | |||
import React from 'react'; | |||
import React, { useState, useEffect } from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker } from 'antd'; | |||
import { Card, Col, Row, message } from 'antd'; | |||
import ConditionQuery from "../../../components/ConditionQuery"; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
import moment from 'moment'; | |||
import marketAPI from "./service"; | |||
import * as echarts from 'echarts'; | |||
/** | |||
* 会员报表 | |||
* @returns | |||
*/ | |||
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: [] | |||
}); | |||
//新增会员数 | |||
const [newMemberNum, setNewMemberNum] = useState(0); | |||
//复购次数 | |||
const [repurChaseNum, setRepurChaseNum] = useState(0); | |||
//新增会员图表实例 | |||
let newMemberObj = null; | |||
//复购图表实例 | |||
let repurChaseObj = null; | |||
//切换时间 | |||
const onChangeTimeIndex = (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 initMemberReport = async () => { | |||
const jsonData = { | |||
"shopIds": [ | |||
currentOrg.key | |||
], | |||
"startTime": timeRange[0], | |||
"endTime": timeRange[1] | |||
} | |||
setShowLoading(true); | |||
const response = await marketAPI.getMemberReport(jsonData); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
let sumNewMember = 0; | |||
let sumRepurChase = 0; | |||
response.data.newAddUser.forEach(item => { | |||
sumNewMember += item.userValue | |||
}); | |||
response.data.repurChase.forEach(item => { | |||
sumRepurChase += item.userValue | |||
}); | |||
setNewMemberNum(sumNewMember); | |||
setRepurChaseNum(sumRepurChase); | |||
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.toLocaleDateString()); | |||
seriesData.push(item.userValue); | |||
}); | |||
const option = { | |||
title: { | |||
text: '新增会员' | |||
}, | |||
tooltip: { | |||
trigger: 'axis' | |||
}, | |||
toolbox: { | |||
feature: { | |||
saveAsImage: {} | |||
} | |||
}, | |||
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: '复购次数' | |||
}, | |||
tooltip: { | |||
trigger: 'axis', | |||
axisPointer: { | |||
label: { | |||
show: true, | |||
formatter: '复购{value}次' | |||
} | |||
} | |||
}, | |||
toolbox: { | |||
feature: { | |||
saveAsImage: {} | |||
} | |||
}, | |||
xAxis: { | |||
type: 'category', | |||
data: xAxisData | |||
}, | |||
yAxis: { | |||
type: 'value' | |||
}, | |||
series: [ | |||
{ | |||
data: seriesData, | |||
type: 'line', | |||
smooth: true | |||
} | |||
] | |||
}; | |||
repurChaseObj.setOption(option); | |||
} | |||
//重置 | |||
const onResetSearch = () => { | |||
setCurrentOrg(""); | |||
} | |||
//子组件切换组织架构 | |||
const onCurrentOrgChange = (curOrg) => { | |||
setCurrentOrg(curOrg) | |||
} | |||
//子组件时间改变 | |||
const onTimePickerChange = (nowTimeRange) => { | |||
setTimeRange(nowTimeRange); | |||
} | |||
useEffect(() => { | |||
onGetOrgTree(); | |||
initMemberReport(); | |||
}, []); | |||
useEffect(() => { | |||
initNewMemberCharts(); | |||
initRepurChaseCharts(); | |||
window.onresize = () => { | |||
if (newMemberObj && repurChaseObj) { | |||
newMemberObj.resize(); | |||
repurChaseObj.resize(); | |||
} | |||
} | |||
}, [memberReport]); | |||
return ( | |||
<PageContainer> | |||
<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']}> | |||
会员报表 | |||
</Card> | |||
<ConditionQuery | |||
orgTree={orgTree} | |||
timeRange={timeRange} | |||
searchDayIndex={searchDayIndex} | |||
currentOrg={currentOrg} | |||
showLoading={showLoading} | |||
onTimePickerChange={onTimePickerChange} | |||
onChangeTimeIndex={onChangeTimeIndex} | |||
onCurrentOrgChange={onCurrentOrgChange} | |||
onResetSearch={onResetSearch} | |||
onQueryBtn={initMemberReport} | |||
> | |||
</ConditionQuery> | |||
<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']}> | |||
{newMemberNum} | |||
</div> | |||
</div> | |||
<div className={styles['member-card-box']}> | |||
<div className={styles['member-card-prefix']}> | |||
复购数量 | |||
</div> | |||
<div className={styles['member-card-sufixx']}> | |||
{repurChaseNum} | |||
</div> | |||
</div> | |||
</Card> | |||
</Col> | |||
<Col xs={24} sm={24} md={24} lg={16} xl={18}> | |||
<Card> | |||
{/* 复购次数图表 */} | |||
<div id="repur-chase" className={styles['repur-chase']}></div> | |||
</Card> | |||
</Col> | |||
</Row> | |||
<Row> | |||
<Col xs={24} sm={24} md={24} lg={24} xl={24} > | |||
<Card> | |||
{/* 新增会员图表 */} | |||
<div id="new-member" className={styles['new-member']}></div> | |||
</Card> | |||
</Col> | |||
</Row> | |||
</PageContainer> | |||
) | |||
} |
@@ -2,15 +2,92 @@ | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
.data-search-row { | |||
margin-bottom: 10px; | |||
} | |||
.data-search-item { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
margin-bottom: 10px; | |||
} | |||
.data-search-prefix { | |||
margin-right: 10px; | |||
font-size: 18px; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
margin-right: 20px; | |||
} | |||
.data-search-day { | |||
padding: 5px 20px; | |||
border: 1px solid #dedede; | |||
cursor: pointer; | |||
} | |||
.data-search-day:nth-child(1), .data-search-day:nth-child(2), .data-search-day:nth-child(3) { | |||
border-right: none; | |||
} | |||
.search-day-selected { | |||
border: 1px solid #FA541C !important; | |||
color: #FA541C; | |||
} | |||
.data-search-sufixx { | |||
width: 100%; | |||
} | |||
// 加载中 | |||
.loading-card { | |||
position: fixed; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
width: 100vw; | |||
height: 100vh; | |||
top: 0; | |||
left: 0; | |||
right: 0; | |||
bottom: 0; | |||
z-index: 999; | |||
background-color: rgba(0, 0, 0, 0.5); | |||
} | |||
.member-card-box { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
margin-bottom: 5px; | |||
} | |||
.member-card-prefix { | |||
font-size: 20px; | |||
font-weight: 700; | |||
color: #999; | |||
font-family: '楷体'; | |||
} | |||
.member-card-sufixx { | |||
font-size: 22px; | |||
} | |||
.new-member { | |||
width: 100%; | |||
height: 250px; | |||
} | |||
.repur-chase { | |||
width: 100%; | |||
height: 250px; | |||
} | |||
.member-card { | |||
height: 300px; | |||
} | |||
.table-card { | |||
.member-row-common { | |||
margin-bottom: 10px; | |||
} |
@@ -0,0 +1,18 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
//获取组织架构 | |||
getOrgTree() { | |||
return request(`/kitchen/api/report-statistics/org-tree`, { | |||
method: 'GET', | |||
}); | |||
}, | |||
//获取会员报表 | |||
getMemberReport(data) { | |||
return request(`/kitchen/api/report-statistics/user-report`, { | |||
method: 'POST', | |||
data | |||
}); | |||
} | |||
}; |
@@ -1,28 +1,269 @@ | |||
import React from 'react'; | |||
import React, { useState, useEffect } from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker } from 'antd'; | |||
import { Card, Col, Row, message } from 'antd'; | |||
import ConditionQuery from "../../../components/ConditionQuery"; | |||
import * as echarts from 'echarts'; | |||
import moment from 'moment'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
import productAPI from "./service"; | |||
/** | |||
* 产品报表 | |||
* @returns | |||
*/ | |||
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(""); | |||
//产品Top | |||
const [topGoods, setTopGoods] = useState([]); | |||
//热销商品折线图实例 | |||
let topGoodsLine = null; | |||
//热销商品柱状图实例 | |||
let topGoodsBar = null; | |||
//切换时间 | |||
const onChangeTimeIndex = (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 productAPI.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 onResetSearch = () => { | |||
setCurrentOrg(""); | |||
} | |||
//子组件切换组织架构 | |||
const onCurrentOrgChange = (curOrg) => { | |||
setCurrentOrg(curOrg) | |||
} | |||
//子组件时间改变 | |||
const onTimePickerChange = (nowTimeRange) => { | |||
setTimeRange(nowTimeRange); | |||
} | |||
//获取热销产品数据 | |||
const onGetTopGoods = async () => { | |||
const jsonData = { | |||
"top": 10, | |||
"shopIds": [ | |||
currentOrg.key | |||
], | |||
"startTime": timeRange[0], | |||
"endTime": timeRange[1] | |||
} | |||
setShowLoading(true); | |||
const response = await productAPI.getProductEcharts(jsonData); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setTopGoods(response.data.topGoods); | |||
} else { | |||
message.error(response.errors || '获取热销产品数据出错') | |||
} | |||
} | |||
//初始化热销商品柱状图 | |||
const initHotTopGoods = () => { | |||
const chartDom = document.getElementById('top-goods'); | |||
topGoodsBar = echarts.init(chartDom); | |||
const xData = []; | |||
const seriesData = []; | |||
topGoods.forEach(item => { | |||
xData.push(item.name); | |||
seriesData.push(item.count); | |||
}); | |||
const option = { | |||
title: { | |||
show: true, | |||
text: '热销商品' | |||
}, | |||
tooltip: { | |||
trigger: 'axis', | |||
axisPointer: { | |||
label: { | |||
show: true, | |||
formatter: '热销商品' | |||
} | |||
} | |||
}, | |||
toolbox: { | |||
feature: { | |||
saveAsImage: {} | |||
} | |||
}, | |||
xAxis: { | |||
type: 'category', | |||
data: xData | |||
}, | |||
yAxis: { | |||
type: 'value' | |||
}, | |||
series: [ | |||
{ | |||
data: seriesData, | |||
type: 'bar', | |||
showBackground: true, | |||
backgroundStyle: { | |||
color: 'rgba(180, 180, 180, 0.2)' | |||
} | |||
} | |||
] | |||
}; | |||
option && topGoodsBar.setOption(option); | |||
} | |||
//初始化热销商品折线图 | |||
const initHotTopLine = () => { | |||
const chartDom = document.getElementById('top-goods-line'); | |||
topGoodsLine = echarts.init(chartDom); | |||
const option = { | |||
title: { | |||
show: true, | |||
text: '热销商品' | |||
}, | |||
toolbox: { | |||
feature: { | |||
saveAsImage: {} | |||
} | |||
}, | |||
tooltip: { | |||
trigger: 'axis', | |||
axisPointer: { | |||
label: { | |||
show: true, | |||
formatter: '热销商品' | |||
} | |||
} | |||
}, | |||
xAxis: { | |||
type: 'category', | |||
data: ['00:00', '01:00', '02:00', '03:00', '04:00', '05:00', '06:00'] | |||
}, | |||
yAxis: { | |||
type: 'value' | |||
}, | |||
series: [ | |||
{ | |||
data: [150, 230, 224, 218, 135, 147, 260], | |||
type: 'line' | |||
} | |||
] | |||
}; | |||
option && topGoodsLine.setOption(option); | |||
} | |||
useEffect(() => { | |||
onGetOrgTree(); | |||
onGetTopGoods(); | |||
}, []); | |||
useEffect(() => { | |||
initHotTopGoods(); | |||
initHotTopLine(); | |||
window.onresize = () => { | |||
if (topGoodsLine && topGoodsBar) { | |||
topGoodsLine.resize(); | |||
topGoodsBar.resize(); | |||
} | |||
} | |||
}, [topGoods]); | |||
return ( | |||
<PageContainer> | |||
<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']}> | |||
产品报表 | |||
</Card> | |||
<ConditionQuery | |||
orgTree={orgTree} | |||
timeRange={timeRange} | |||
searchDayIndex={searchDayIndex} | |||
currentOrg={currentOrg} | |||
showLoading={showLoading} | |||
onTimePickerChange={onTimePickerChange} | |||
onChangeTimeIndex={onChangeTimeIndex} | |||
onCurrentOrgChange={onCurrentOrgChange} | |||
onResetSearch={onResetSearch} | |||
onQueryBtn={onGetTopGoods} | |||
> | |||
</ConditionQuery> | |||
<Row gutter={10}> | |||
<Col xs={24} sm={24} md={24} lg={8} xl={8}> | |||
<Card> | |||
<div id="top-goods" className={styles['top-goods']}></div> | |||
</Card> | |||
</Col> | |||
<Col xs={24} sm={24} md={24} lg={16} xl={16}> | |||
<Card> | |||
<div id="top-goods-line" className={styles['top-goods-line']}></div> | |||
</Card> | |||
</Col> | |||
</Row> | |||
</PageContainer> | |||
) | |||
} |
@@ -1,16 +1,4 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
.top-goods , .top-goods-line{ | |||
width: 100%; | |||
height: 300px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-card { | |||
} |
@@ -0,0 +1,18 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
//获取组织架构 | |||
getOrgTree() { | |||
return request(`/kitchen/api/report-statistics/org-tree`, { | |||
method: 'GET', | |||
}); | |||
}, | |||
//获取产品报表 | |||
getProductEcharts(data) { | |||
return request(`/kitchen/api/report-statistics/productc`, { | |||
method: 'POST', | |||
data | |||
}); | |||
} | |||
}; |
@@ -1,28 +1,285 @@ | |||
import React from 'react'; | |||
import React, { useState, useEffect } from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker } from 'antd'; | |||
import { Card, Col, Row, message } from 'antd'; | |||
import ConditionQuery from "../../../components/ConditionQuery"; | |||
import * as echarts from 'echarts'; | |||
import moment from 'moment'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
import reportAPI from "./service"; | |||
/** | |||
* 营收报表 | |||
* 营销报表 | |||
* @returns | |||
*/ | |||
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 [reportActivity, setReportActivity] = useState([]); | |||
//营销优惠券 | |||
const [reportCoupon, setReportCoupon] = useState([]); | |||
//活动报表图表实例 | |||
let activityEcharts = null; | |||
//优惠券报表图表实例 | |||
let couponEcharts = null; | |||
//切换时间 | |||
const onChangeTimeIndex = (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 reportAPI.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 onResetSearch = () => { | |||
setCurrentOrg(""); | |||
} | |||
//子组件切换组织架构 | |||
const onCurrentOrgChange = (curOrg) => { | |||
setCurrentOrg(curOrg) | |||
} | |||
//子组件时间改变 | |||
const onTimePickerChange = (nowTimeRange) => { | |||
setTimeRange(nowTimeRange); | |||
} | |||
//获取营销报表列表 | |||
const onQueryMarketing = async () => { | |||
setShowLoading(true); | |||
const response = await reportAPI.getReportStatisticsMarketing(); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setReportActivity(response.data.activity); | |||
setReportCoupon(response.data.coupon); | |||
} else { | |||
message.error(response.errors || '获取营销报表列表出错'); | |||
} | |||
} | |||
//生成营销报表活动信息柱状图 | |||
const initActivityEcharts = () => { | |||
const chartDom = document.getElementById('activity-echarts'); | |||
activityEcharts = echarts.init(chartDom); | |||
const xAxisData = []; | |||
const seriesData = []; | |||
reportActivity.forEach(item => { | |||
xAxisData.push(item.activityName); | |||
seriesData.push(item.numberParticipants); | |||
}); | |||
const option = { | |||
title: { | |||
text: '活动报表' | |||
}, | |||
tooltip: { | |||
trigger: 'axis' | |||
}, | |||
toolbox: { | |||
feature: { | |||
saveAsImage: {} | |||
} | |||
}, | |||
xAxis: { | |||
type: 'category', | |||
data: xAxisData | |||
}, | |||
yAxis: { | |||
type: 'value' | |||
}, | |||
series: [ | |||
{ | |||
data: seriesData, | |||
type: 'bar', | |||
showBackground: true, | |||
backgroundStyle: { | |||
color: 'rgba(180, 180, 180, 0.2)' | |||
} | |||
} | |||
] | |||
}; | |||
option && activityEcharts.setOption(option); | |||
} | |||
//生成优惠券报表折线图 | |||
const initCouponEcharts = () => { | |||
const chartDom = document.getElementById('coupon-ehcarts'); | |||
couponEcharts = echarts.init(chartDom); | |||
const xAxisData = []; | |||
const seriesPreferentialAmount = []; | |||
const seriesSend = []; | |||
const seriesGet = []; | |||
reportCoupon.forEach(item => { | |||
xAxisData.push(item.couponName); | |||
seriesPreferentialAmount.push(item.preferentialAmount); | |||
seriesSend.push(item.send); | |||
seriesGet.push(item.get); | |||
}); | |||
const option = { | |||
title: { | |||
text: '优惠券报表' | |||
}, | |||
tooltip: { | |||
trigger: 'axis' | |||
}, | |||
legend: { | |||
data: ['优惠金额', '发放数量', '领取数量'] | |||
}, | |||
grid: { | |||
left: '3%', | |||
right: '4%', | |||
bottom: '3%', | |||
containLabel: true | |||
}, | |||
toolbox: { | |||
feature: { | |||
saveAsImage: {} | |||
} | |||
}, | |||
xAxis: { | |||
type: 'category', | |||
boundaryGap: false, | |||
data: xAxisData | |||
}, | |||
yAxis: { | |||
type: 'value' | |||
}, | |||
series: [ | |||
{ | |||
name: '优惠金额', | |||
type: 'line', | |||
stack: 'Total', | |||
data: seriesPreferentialAmount | |||
}, | |||
{ | |||
name: '发放数量', | |||
type: 'line', | |||
stack: 'Total', | |||
data: seriesSend | |||
}, | |||
{ | |||
name: '领取数量', | |||
type: 'line', | |||
stack: 'Total', | |||
data: seriesGet | |||
} | |||
] | |||
}; | |||
option && couponEcharts.setOption(option); | |||
} | |||
useEffect(() => { | |||
onGetOrgTree(); | |||
onQueryMarketing(); | |||
}, []); | |||
useEffect(() => { | |||
initActivityEcharts(); | |||
initCouponEcharts(); | |||
window.onresize = () => { | |||
if (activityEcharts && couponEcharts) { | |||
activityEcharts.resize(); | |||
couponEcharts.resize(); | |||
} | |||
} | |||
}, [reportActivity, reportCoupon]); | |||
return ( | |||
<PageContainer> | |||
<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']}> | |||
营收报表 | |||
</Card> | |||
<ConditionQuery | |||
orgTree={orgTree} | |||
timeRange={timeRange} | |||
searchDayIndex={searchDayIndex} | |||
currentOrg={currentOrg} | |||
showLoading={showLoading} | |||
onTimePickerChange={onTimePickerChange} | |||
onChangeTimeIndex={onChangeTimeIndex} | |||
onCurrentOrgChange={onCurrentOrgChange} | |||
onResetSearch={onResetSearch} | |||
onQueryBtn={onQueryMarketing} | |||
> | |||
</ConditionQuery> | |||
<Row gutter={10}> | |||
<Col xs={24} sm={24} md={24} lg={8} xl={8}> | |||
<Card> | |||
<div id="activity-echarts" className={styles['activity-echarts']}></div> | |||
</Card> | |||
</Col> | |||
<Col xs={24} sm={24} md={24} lg={16} xl={16}> | |||
<Card> | |||
<div id="coupon-ehcarts" className={styles['coupon-ehcarts']}></div> | |||
</Card> | |||
</Col> | |||
</Row> | |||
</PageContainer> | |||
) | |||
} |
@@ -12,5 +12,7 @@ | |||
margin-left: 20px; | |||
} | |||
.table-card { | |||
.activity-echarts, .coupon-ehcarts { | |||
width: 100%; | |||
height: 300px; | |||
} |
@@ -0,0 +1,17 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
//获取组织架构 | |||
getOrgTree() { | |||
return request(`/kitchen/api/report-statistics/org-tree`, { | |||
method: 'GET', | |||
}); | |||
}, | |||
//获取营销报表 | |||
getReportStatisticsMarketing() { | |||
return request(`/kitchen/api/report-statistics/marketing`, { | |||
method: 'POST', | |||
}); | |||
} | |||
}; |
@@ -33,6 +33,7 @@ export default function Index() { | |||
//门店 | |||
const [storeIdArray, setStoreIdArray] = useState(""); | |||
const [storeSelect, setStoreSelect] = useState([]); | |||
const [storeList, setStoreList] = useState([]); | |||
//商品 | |||
const [goodsIdArray, setGoodsIdArray] = useState([]); | |||
const [goodsIdSelect, setGoodsIdSelect] = useState([]); | |||
@@ -46,13 +47,12 @@ export default function Index() { | |||
dataIndex: 'storeId', | |||
key: 'storeId', | |||
render: (text) => { | |||
const findGoods = storeSelect.find(item => item.id === text); | |||
const findGoods = storeList.find(item => item.key === text); | |||
if (findGoods) { | |||
return <span>{findGoods.name}</span> | |||
return <span>{findGoods.title}</span> | |||
} else { | |||
return <span>暂无门店名称</span> | |||
} | |||
}, | |||
}, | |||
{ | |||
@@ -112,6 +112,7 @@ export default function Index() { | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setStoreSelect(response.data); | |||
setStoreList(treeArrayToFlat(response.data)); | |||
} else { | |||
message.error('查询店铺列表失败'); | |||
} | |||
@@ -152,6 +153,18 @@ export default function Index() { | |||
]); | |||
} | |||
//树形数据扁平化 | |||
const treeArrayToFlat = (tree, arr = []) => { | |||
tree.forEach(item => { | |||
const { children, ...props } = item; | |||
arr.push(props); | |||
if (children && children.length > 0) { | |||
treeArrayToFlat(children, arr); | |||
} | |||
}); | |||
return arr; | |||
} | |||
useEffect(() => { | |||
onQueryReportSalescost(); | |||
onQueryStoreList(); | |||