diff --git a/config/routes.js b/config/routes.js index a5c011c..64dac7e 100644 --- a/config/routes.js +++ b/config/routes.js @@ -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', diff --git a/package.json b/package.json index 883d2d0..23ce52f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app.jsx b/src/app.jsx index 8ca8868..5855dc4 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -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', diff --git a/src/components/ConditionQuery/index.jsx b/src/components/ConditionQuery/index.jsx new file mode 100644 index 0000000..7535263 --- /dev/null +++ b/src/components/ConditionQuery/index.jsx @@ -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 ( +
+ +
+ ) + } + + return ( +
+ {props.showLoading ? : null} + + + + { + 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); + }} /> + + +
props.onChangeTimeIndex(0)}> + 今天 +
+
props.onChangeTimeIndex(1)}> + 昨天 +
+
props.onChangeTimeIndex(2)}> + 近7天 +
+
props.onChangeTimeIndex(3)}> + 近30天 +
+ + + { + if (node.type === 2 || node === 3) { + props.onCurrentOrgChange(node); + } else { + props.onCurrentOrgChange(""); + } + }} + placeholder="请选择组织架构" + treeDefaultExpandAll + /> + + +
+ + +
+ +
+
+
+ ) +} diff --git a/src/components/ConditionQuery/index.less b/src/components/ConditionQuery/index.less new file mode 100644 index 0000000..5b5dbc1 --- /dev/null +++ b/src/components/ConditionQuery/index.less @@ -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; +} \ No newline at end of file diff --git a/src/pages/order/cost-of-sales/index.jsx b/src/pages/order/cost-of-sales/index.jsx index ec3fead..5602ae3 100644 --- a/src/pages/order/cost-of-sales/index.jsx +++ b/src/pages/order/cost-of-sales/index.jsx @@ -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 {findGoods.name} + return {findGoods.title} } else { return 暂无门店名称 } @@ -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(); diff --git a/src/pages/order/gross-profit-store-sales/index.jsx b/src/pages/order/gross-profit-store-sales/index.jsx index 2797e12..7ced9be 100644 --- a/src/pages/order/gross-profit-store-sales/index.jsx +++ b/src/pages/order/gross-profit-store-sales/index.jsx @@ -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 {findGoods.name} + return {findGoods.title} } else { return 暂无门店名称 } - }, }, { @@ -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(); diff --git a/src/pages/order/order-market-report/index.jsx b/src/pages/order/order-market-report/index.jsx index b9403b5..ea52c2a 100644 --- a/src/pages/order/order-market-report/index.jsx +++ b/src/pages/order/order-market-report/index.jsx @@ -391,7 +391,7 @@ const LoadingCard = () => { } /** - * 营销报表 + * 营收报表 * @returns */ export default function Index() { diff --git a/src/pages/order/order-member-statement/index.jsx b/src/pages/order/order-member-statement/index.jsx index 8c62138..c0ca33d 100644 --- a/src/pages/order/order-member-statement/index.jsx +++ b/src/pages/order/order-member-statement/index.jsx @@ -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 ( - -
- -
- - -
-
-
- - 会员报表 - + + + + + +
+
+ 会员总量 +
+
+ {memberReport.userCount} +
+
+
+
+ 新增会员 +
+
+ {newMemberNum} +
+
+
+
+ 复购数量 +
+
+ {repurChaseNum} +
+
+
+ + + + {/* 复购次数图表 */} +
+
+ + +
+ + + + {/* 新增会员图表 */} +
+
+ +
) } diff --git a/src/pages/order/order-member-statement/index.less b/src/pages/order/order-member-statement/index.less index 74d0c92..5b5dbc1 100644 --- a/src/pages/order/order-member-statement/index.less +++ b/src/pages/order/order-member-statement/index.less @@ -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; } \ No newline at end of file diff --git a/src/pages/order/order-member-statement/service.js b/src/pages/order/order-member-statement/service.js new file mode 100644 index 0000000..063d1c6 --- /dev/null +++ b/src/pages/order/order-member-statement/service.js @@ -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 + }); + } +}; diff --git a/src/pages/order/order-product-report/index.jsx b/src/pages/order/order-product-report/index.jsx index 253aa3b..17a3110 100644 --- a/src/pages/order/order-product-report/index.jsx +++ b/src/pages/order/order-product-report/index.jsx @@ -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 ( - -
- -
- - -
-
-
- - 产品报表 - + + + + + +
+
+ + + +
+
+ +
) } diff --git a/src/pages/order/order-product-report/index.less b/src/pages/order/order-product-report/index.less index 74d0c92..37f49a0 100644 --- a/src/pages/order/order-product-report/index.less +++ b/src/pages/order/order-product-report/index.less @@ -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 { -} \ No newline at end of file diff --git a/src/pages/order/order-product-report/service.js b/src/pages/order/order-product-report/service.js new file mode 100644 index 0000000..33db2fd --- /dev/null +++ b/src/pages/order/order-product-report/service.js @@ -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 + }); + } +}; diff --git a/src/pages/order/order-revenue-statement/index.jsx b/src/pages/order/order-revenue-statement/index.jsx index c2e0ebd..1d325ba 100644 --- a/src/pages/order/order-revenue-statement/index.jsx +++ b/src/pages/order/order-revenue-statement/index.jsx @@ -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 ( - -
- -
- - -
-
-
- - 营收报表 - + + + + + +
+
+ + + +
+
+ +
) } diff --git a/src/pages/order/order-revenue-statement/index.less b/src/pages/order/order-revenue-statement/index.less index 74d0c92..1fcef07 100644 --- a/src/pages/order/order-revenue-statement/index.less +++ b/src/pages/order/order-revenue-statement/index.less @@ -12,5 +12,7 @@ margin-left: 20px; } -.table-card { +.activity-echarts, .coupon-ehcarts { + width: 100%; + height: 300px; } \ No newline at end of file diff --git a/src/pages/order/order-revenue-statement/service.js b/src/pages/order/order-revenue-statement/service.js new file mode 100644 index 0000000..a079fa0 --- /dev/null +++ b/src/pages/order/order-revenue-statement/service.js @@ -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', + }); + } +}; diff --git a/src/pages/order/sales-gross-profit/index.jsx b/src/pages/order/sales-gross-profit/index.jsx index 4eb682b..42c9305 100644 --- a/src/pages/order/sales-gross-profit/index.jsx +++ b/src/pages/order/sales-gross-profit/index.jsx @@ -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 {findGoods.name} + return {findGoods.title} } else { return 暂无门店名称 } - }, }, { @@ -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();