@@ -586,6 +586,60 @@ export default [ | |||
component: './order/exOrder', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '营销报表', | |||
path: '/order/order-market-report', | |||
component: './order/order-market-report', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '订单报表', | |||
path: '/order/order-report', | |||
component: './order/order-report', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '订单报表详情', | |||
path: '/order/order-report/order-report-detail', | |||
component: './order/order-report-detail', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '营收报表', | |||
path: '/order/order-revenue-statement', | |||
component: './order/order-revenue-statement', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '产品报表', | |||
path: '/order/order-product-report', | |||
component: './order/order-product-report', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '会员报表', | |||
path: '/order/order-member-statement', | |||
component: './order/order-member-statement', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '销售成本分析', | |||
path: '/order/cost-of-sales', | |||
component: './order/cost-of-sales', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '销售毛利分析', | |||
path: '/order/sales-gross-profit', | |||
component: './order/sales-gross-profit', | |||
access: 'k31', | |||
}, | |||
{ | |||
name: '门店销售毛利分析', | |||
path: '/order/gross-profit-store-sales', | |||
component: './order/gross-profit-store-sales', | |||
access: 'k31', | |||
}, | |||
], | |||
}, | |||
{ | |||
@@ -0,0 +1,245 @@ | |||
import React, { useState, useEffect } from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker, Table, Pagination, Spin, Select, message } from 'antd'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
import costSalesAPI from "./service"; | |||
import moment from 'moment'; | |||
const columns = [ | |||
{ | |||
title: '门店名称', | |||
dataIndex: 'storeId', | |||
key: 'storeId', | |||
}, | |||
{ | |||
title: '商品名称', | |||
dataIndex: 'goodsId', | |||
key: 'goodsId', | |||
}, | |||
{ | |||
title: '销售收入', | |||
dataIndex: 'salesPrice', | |||
key: 'salesPrice', | |||
}, | |||
{ | |||
title: '销售成本', | |||
dataIndex: 'costPrice', | |||
key: 'costPrice', | |||
}, | |||
{ | |||
title: '销售数量', | |||
dataIndex: 'salesNum', | |||
key: 'salesNum', | |||
}, | |||
{ | |||
title: '成本率', | |||
dataIndex: 'costRate', | |||
key: 'costRate', | |||
} | |||
]; | |||
const LoadingCard = () => { | |||
return ( | |||
<div className={styles['loading-card']}> | |||
<Spin size="large" /> | |||
</div> | |||
) | |||
} | |||
/** | |||
* 销售成本分析 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
//订单报表列表 | |||
const [costSalesData, setCostSalesData] = useState([]); | |||
const [current, setCurrent] = useState(1); | |||
const [pageSize, setPageSize] = useState(10); | |||
const [total, setTotal] = useState(0); | |||
const [showLoading, setShowLoading] = useState(false); | |||
const [timeRange, setTimeRange] = useState([ | |||
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')), | |||
]); //日期选择器 | |||
//门店 | |||
const [storeIdArray, setStoreIdArray] = useState([]); | |||
const [storeSelect, setStoreSelect] = useState([]); | |||
//商品 | |||
const [goodsIdArray, setGoodsIdArray] = useState([]); | |||
const [goodsIdSelect, setGoodsIdSelect] = useState([]); | |||
//商品类型 | |||
const [goodsTypeArray, setGoodsTypeArray] = useState([]); | |||
const [goodsTypeSelect, setGoodsTypeSelect] = useState([]); | |||
//页码变化 | |||
const onPageChange = (current, pageSize) => { | |||
setCurrent(current); | |||
setPageSize(pageSize); | |||
} | |||
//查询 | |||
const onQueryReportSalescost = async () => { | |||
const jsonData = { | |||
"storeId": storeIdArray, | |||
"goodsId": goodsIdArray, | |||
"goodsTypeId": goodsTypeArray, | |||
"begintime": timeRange[0], | |||
"endtime": timeRange[1], | |||
current, | |||
pageSize | |||
} | |||
setShowLoading(true); | |||
const response = await costSalesAPI.getReportSalescost({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setCostSalesData(response.data.data); | |||
} else { | |||
message.error(response.errors || '获取销售成本失败'); | |||
} | |||
} | |||
/** | |||
* 查询店铺列表 | |||
*/ | |||
const onQueryStoreList = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.gettree({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
const storeList = []; | |||
response.data.forEach(item => { | |||
if (item.type === 2 || item.type === 3) { | |||
storeList.push(item) | |||
} | |||
}) | |||
setStoreSelect(storeList); | |||
} else { | |||
message.error('查询店铺列表失败'); | |||
} | |||
} | |||
//查询商品列表 | |||
const onQueryGoodsList = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.goodsList({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setGoodsIdSelect(response.data); | |||
} else { | |||
message.error('查询商品列表失败'); | |||
} | |||
} | |||
//查询商品分类类型列表 | |||
const onQueryGoodsType = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.goodsTypeList({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setGoodsTypeSelect(response.data); | |||
} else { | |||
message.error('查询商品分类列表失败'); | |||
} | |||
} | |||
useEffect(() => { | |||
onQueryReportSalescost(); | |||
onQueryStoreList(); | |||
onQueryGoodsList(); | |||
onQueryGoodsType(); | |||
}, []); | |||
return ( | |||
<PageContainer> | |||
{showLoading ? <LoadingCard></LoadingCard> : null} | |||
<Card className={styles['data-search-card']}> | |||
<div className={styles['data-search-box']}> | |||
<div className={styles['data-search-left']}> | |||
<Select | |||
showSearch | |||
mode="tags" | |||
size="middle" | |||
allowClear | |||
placeholder="请选择门店" | |||
onChange={(values) => setStoreIdArray(values)} | |||
filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())} | |||
style={{ | |||
width: '300px', | |||
marginRight: '50px' | |||
}} | |||
> | |||
{ | |||
storeSelect.map( (item) => { | |||
return ( | |||
<Option key={item.key}>{item.title}</Option> | |||
) | |||
}) | |||
} | |||
</Select> | |||
<Select | |||
showSearch | |||
mode="tags" | |||
size="middle" | |||
allowClear | |||
placeholder="请选择商品" | |||
onChange={(values) => setGoodsIdArray(values)} | |||
filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())} | |||
style={{ | |||
width: '300px', | |||
marginRight: '50px' | |||
}} | |||
> | |||
{ | |||
goodsIdSelect.map( (item, index) => { | |||
return ( | |||
<Option key={item.id}>{item.name}</Option> | |||
) | |||
}) | |||
} | |||
</Select> | |||
<Select | |||
showSearch | |||
mode="tags" | |||
size="middle" | |||
allowClear | |||
placeholder="请选择商品类别" | |||
onChange={(values) => setGoodsTypeArray(values)} | |||
filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())} | |||
style={{ | |||
width: '300px', | |||
marginRight: '50px' | |||
}} | |||
> | |||
{ | |||
goodsTypeSelect.map( (item) => { | |||
return ( | |||
<Option key={item.id}>{item.name}</Option> | |||
) | |||
}) | |||
} | |||
</Select> | |||
<RangePicker size='middle' className={styles['my-range-picker']} 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); | |||
}} /> | |||
</div> | |||
<div className={styles['data-search-btns']}> | |||
<Button className={styles['search-btn-item']}>重置</Button> | |||
<Button className={styles['search-btn-item']} type="primary" onClick={onQueryReportSalescost}>查询</Button> | |||
</div> | |||
</div> | |||
</Card> | |||
<Card className={styles['table-card']}> | |||
<Table dataSource={costSalesData} columns={columns} pagination={false} /> | |||
<div className={styles['table-page']}> | |||
<Pagination current={current} pageSize={pageSize} total={total} onChange={onPageChange} /> | |||
</div> | |||
</Card> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,36 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-page { | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
height: 50px; | |||
} | |||
// 加载中 | |||
.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); | |||
} |
@@ -0,0 +1,40 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
getReportSalescost(data) { | |||
return request(`/kitchen/api/report/salescost`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
/** | |||
* 查询商品信息 | |||
*/ | |||
goodsList(data) { | |||
return request(`/kitchen/api/goodes/list`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
/** | |||
* 查询商品类型 | |||
*/ | |||
goodsTypeList(data) { | |||
return request(`/kitchen/api/goodstype/list`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
/** | |||
* 查询门店列表 | |||
*/ | |||
gettree(params) { | |||
return request('/kitchen/api/sysOrg/tree', { | |||
data: { | |||
...params, | |||
}, | |||
}); | |||
} | |||
}; |
@@ -0,0 +1,149 @@ | |||
import React, { useState, useEffect } from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker, Table, Pagination, Spin, Select, message } from 'antd'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
import costSalesAPI from "./service"; | |||
import moment from 'moment'; | |||
const columns = [ | |||
{ | |||
title: '门店名称', | |||
dataIndex: 'storeId', | |||
key: 'storeId', | |||
}, | |||
{ | |||
title: '毛利率', | |||
dataIndex: 'marginRatio', | |||
key: 'marginRatio', | |||
} | |||
]; | |||
const LoadingCard = () => { | |||
return ( | |||
<div className={styles['loading-card']}> | |||
<Spin size="large" /> | |||
</div> | |||
) | |||
} | |||
/** | |||
* 门店销售毛利分析 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
//订单报表列表 | |||
const [costSalesData, setCostSalesData] = useState([]); | |||
const [current, setCurrent] = useState(1); | |||
const [pageSize, setPageSize] = useState(10); | |||
const [total, setTotal] = useState(0); | |||
const [showLoading, setShowLoading] = useState(false); | |||
const [timeRange, setTimeRange] = useState([ | |||
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')), | |||
]); //日期选择器 | |||
//门店 | |||
const [storeIdArray, setStoreIdArray] = useState([]); | |||
const [storeSelect, setStoreSelect] = useState([]); | |||
//页码变化 | |||
const onPageChange = (current, pageSize) => { | |||
setCurrent(current); | |||
setPageSize(pageSize); | |||
} | |||
//查询 | |||
const onQueryReportSalescost = async () => { | |||
const jsonData = { | |||
"storeId": storeIdArray, | |||
"begintime": timeRange[0], | |||
"endtime": timeRange[1], | |||
current, | |||
pageSize | |||
} | |||
setShowLoading(true); | |||
const response = await costSalesAPI.getReportOrgsalesmargin({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setCostSalesData(response.data.data); | |||
} else { | |||
message.error(response.errors || '获取销售成本失败'); | |||
} | |||
} | |||
/** | |||
* 查询店铺列表 | |||
*/ | |||
const onQueryStoreList = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.gettree({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
const storeList = []; | |||
response.data.forEach(item => { | |||
if (item.type === 2 || item.type === 3) { | |||
storeList.push(item) | |||
} | |||
}) | |||
setStoreSelect(storeList); | |||
} else { | |||
message.error('查询店铺列表失败'); | |||
} | |||
} | |||
useEffect(() => { | |||
onQueryReportSalescost(); | |||
onQueryStoreList(); | |||
}, []); | |||
return ( | |||
<PageContainer> | |||
{showLoading ? <LoadingCard></LoadingCard> : null} | |||
<Card className={styles['data-search-card']}> | |||
<div className={styles['data-search-box']}> | |||
<div className={styles['data-search-left']}> | |||
<Select | |||
showSearch | |||
mode="tags" | |||
size="middle" | |||
allowClear | |||
placeholder="请选择门店" | |||
onChange={(values) => setStoreIdArray(values)} | |||
filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())} | |||
style={{ | |||
width: '300px', | |||
marginRight: '50px' | |||
}} | |||
> | |||
{ | |||
storeSelect.map( (item) => { | |||
return ( | |||
<Option key={item.key}>{item.title}</Option> | |||
) | |||
}) | |||
} | |||
</Select> | |||
<RangePicker size='middle' className={styles['my-range-picker']} 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); | |||
}} /> | |||
</div> | |||
<div className={styles['data-search-btns']}> | |||
<Button className={styles['search-btn-item']}>重置</Button> | |||
<Button className={styles['search-btn-item']} type="primary" onClick={onQueryReportSalescost}>查询</Button> | |||
</div> | |||
</div> | |||
</Card> | |||
<Card className={styles['table-card']}> | |||
<Table dataSource={costSalesData} columns={columns} pagination={false} /> | |||
<div className={styles['table-page']}> | |||
<Pagination current={current} pageSize={pageSize} total={total} onChange={onPageChange} /> | |||
</div> | |||
</Card> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,36 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-page { | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
height: 50px; | |||
} | |||
// 加载中 | |||
.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); | |||
} |
@@ -0,0 +1,21 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
getReportOrgsalesmargin(data) { | |||
return request(`/kitchen/api/report/orgsalesmargin`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
/** | |||
* 查询门店列表 | |||
*/ | |||
gettree(params) { | |||
return request('/kitchen/api/sysOrg/tree', { | |||
data: { | |||
...params, | |||
}, | |||
}); | |||
} | |||
}; |
@@ -0,0 +1,591 @@ | |||
import React, { useEffect, useState } from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { DownloadOutlined } from '@ant-design/icons'; | |||
import { Button, Table, Card, DatePicker, message, Pagination, Spin, TreeSelect } from 'antd'; | |||
import styles from './index.less'; | |||
import moment from 'moment'; | |||
import marketAPI from "./service"; | |||
import ExportJsonExcel from "js-export-excel" | |||
const { TreeNode } = TreeSelect; | |||
const { RangePicker } = DatePicker; | |||
const columns = [ | |||
{ | |||
title: '时间', | |||
dataIndex: 'date', | |||
key: 'date', | |||
width: 170, | |||
align: 'center', | |||
fixed: 'left' | |||
}, | |||
{ | |||
title: '店铺', | |||
dataIndex: 'shopName', | |||
key: 'shopName', | |||
width: 170, | |||
align: 'center', | |||
fixed: 'left', | |||
filters: [ | |||
{ | |||
text: '四川黑菠萝科技', | |||
value: '四川黑菠萝科技', | |||
}, | |||
{ | |||
text: '四川白菠萝科技', | |||
value: '四川白菠萝科技', | |||
} | |||
], | |||
onFilter: (value, record) => record.shopName.indexOf(value) === 0, | |||
}, | |||
{ | |||
title: '营收', | |||
dataIndex: 'revenue', | |||
key: 'revenue', | |||
children: [ | |||
{ | |||
title: '营业额', | |||
dataIndex: 'turnover', | |||
key: 'turnover', | |||
children: [ | |||
{ | |||
title: '流水', | |||
dataIndex: 'revenueFlow', | |||
key: 'revenueFlow', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '实收', | |||
dataIndex: 'paidAmount', | |||
key: 'paidAmount', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
] | |||
}, | |||
{ | |||
title: '优惠明细', | |||
dataIndex: 'discountDetails', | |||
key: 'discountDetails', | |||
children: [ | |||
{ | |||
title: '会员价', | |||
dataIndex: 'discountMember', | |||
key: 'discountMember', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '优惠券', | |||
dataIndex: 'discountCoupon', | |||
key: 'discountCoupon', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '活动', | |||
dataIndex: 'discountActivity', | |||
key: 'discountActivity', | |||
width: 100, | |||
align: 'center', | |||
} | |||
] | |||
}, | |||
] | |||
}, | |||
{ | |||
title: '分类营收', | |||
dataIndex: 'classifiedRevenue', | |||
key: 'classifiedRevenue', | |||
children: [ | |||
{ | |||
title: '外卖营业额', | |||
dataIndex: 'takeOutTurnover', | |||
key: 'takeOutTurnover', | |||
children: [ | |||
{ | |||
title: '外卖营收', | |||
dataIndex: 'takeOutRevenue', | |||
key: 'takeOutRevenue', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '美团外卖', | |||
dataIndex: 'meituanTakeout', | |||
key: 'meituanTakeout', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '饿了么外卖', | |||
dataIndex: 'hungryTakeOut', | |||
key: 'hungryTakeOut', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
] | |||
}, | |||
{ | |||
title: '食堂营收', | |||
dataIndex: 'canteenRevenue', | |||
key: 'canteenRevenue', | |||
children: [ | |||
{ | |||
title: '堂食流水', | |||
dataIndex: 'freshWaterInTheHall', | |||
key: 'freshWaterInTheHall', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '堂食实收', | |||
dataIndex: 'paidInFood', | |||
key: 'paidInFood', | |||
width: 100, | |||
align: 'center', | |||
} | |||
] | |||
}, | |||
{ | |||
title: '第三方平台营收', | |||
dataIndex: 'thirdPartyPlatformRevenue', | |||
key: 'thirdPartyPlatformRevenue', | |||
children: [ | |||
{ | |||
title: '大众点评', | |||
dataIndex: 'publicComments', | |||
key: 'publicComments', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '口碑', | |||
dataIndex: 'wordOfMouth', | |||
key: 'wordOfMouth', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '抖音', | |||
dataIndex: 'tiktok', | |||
key: 'tiktok', | |||
width: 100, | |||
align: 'center', | |||
} | |||
] | |||
}, | |||
] | |||
}, | |||
{ | |||
title: '堂食单价', | |||
dataIndex: 'unitPriceCanteenFood', | |||
key: 'unitPriceCanteenFood', | |||
children: [ | |||
{ | |||
title: '客单价', | |||
dataIndex: 'customerUnitPrice', | |||
key: 'customerUnitPrice', | |||
children: [ | |||
{ | |||
title: '流水单价', | |||
dataIndex: 'customerRevenueFlow', | |||
key: 'customerRevenueFlow', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '实收单价', | |||
dataIndex: 'customerPaidAmount', | |||
key: 'customerPaidAmount', | |||
width: 100, | |||
align: 'center', | |||
} | |||
] | |||
}, | |||
{ | |||
title: '单单价', | |||
dataIndex: 'singleUnitPrice', | |||
key: 'singleUnitPrice', | |||
children: [ | |||
{ | |||
title: '流水单价', | |||
dataIndex: 'orderRevenueFlow', | |||
key: 'orderRevenueFlow', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '实收单价', | |||
dataIndex: 'orderPaidAmount', | |||
key: 'orderPaidAmount', | |||
width: 100, | |||
align: 'center', | |||
} | |||
] | |||
}, | |||
] | |||
}, | |||
{ | |||
title: '交易笔数', | |||
dataIndex: 'numberOfTransactions', | |||
key: 'numberOfTransactions', | |||
children: [ | |||
{ | |||
title: '现金', | |||
dataIndex: 'sourceCashCount', | |||
key: 'sourceCashCount', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '微信支付', | |||
dataIndex: 'sourceWeChatCount', | |||
key: 'sourceWeChatCount', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '大众点评', | |||
dataIndex: 'numPublicComments', | |||
key: 'numPublicComments', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '口碑', | |||
dataIndex: 'numWordOfMouth', | |||
key: 'numWordOfMouth', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '抖音', | |||
dataIndex: 'numTiktok', | |||
key: 'numTiktok', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '美团外卖', | |||
dataIndex: 'numMeituanTakeout', | |||
key: 'numMeituanTakeout', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '饿了么外卖', | |||
dataIndex: 'numHungryTakeOut', | |||
key: 'numHungryTakeOut', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
] | |||
}, | |||
{ | |||
title: '收入构成', | |||
dataIndex: 'revenueComposition', | |||
key: 'revenueComposition', | |||
children: [ | |||
{ | |||
title: '收入来源', | |||
dataIndex: 'sourceOfIncome', | |||
key: 'sourceOfIncome', | |||
children: [ | |||
{ | |||
title: '会员', | |||
dataIndex: 'memberIncomeMoney', | |||
key: 'memberIncomeMoney', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '非会员', | |||
dataIndex: 'nonMemberIncomeMoney', | |||
key: 'nonMemberIncomeMoney', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '外卖', | |||
dataIndex: 'incomeTakeOut', | |||
key: 'incomeTakeOut', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '团购', | |||
dataIndex: 'incomeGroupPurchase', | |||
key: 'incomeGroupPurchase', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
] | |||
}, | |||
{ | |||
title: '收入结构', | |||
dataIndex: 'revenueStructure', | |||
key: 'revenueStructure', | |||
children: [ | |||
{ | |||
title: '微信支付', | |||
dataIndex: 'sourceWeChatMoney', | |||
key: 'sourceWeChatMoney', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '现金支付', | |||
dataIndex: 'sourceCashMoney', | |||
key: 'sourceCashMoney', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '大众点评', | |||
dataIndex: 'incomePublicComments', | |||
key: 'incomePublicComments', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '口碑', | |||
dataIndex: 'incomeWordOfMouth', | |||
key: 'incomeWordOfMouth', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '抖音', | |||
dataIndex: 'incomeTiktok', | |||
key: 'incomeTiktok', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '美团外卖', | |||
dataIndex: 'incomeMeituan', | |||
key: 'incomeMeituan', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
{ | |||
title: '饿了么外卖', | |||
dataIndex: 'incomeHungry', | |||
key: 'incomeHungry', | |||
width: 100, | |||
align: 'center', | |||
}, | |||
] | |||
}, | |||
] | |||
} | |||
] | |||
const LoadingCard = () => { | |||
return ( | |||
<div className={styles['loading-card']}> | |||
<Spin size="large" /> | |||
</div> | |||
) | |||
} | |||
/** | |||
* 营销报表 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
const [data, setData] = useState([]); | |||
const [current, setCurrent] = useState(1); | |||
const [pageSize, setPageSize] = useState(10); | |||
const [total, setTotal] = useState(0); | |||
const [orgTree, setOrgTree] = useState([]); | |||
const [showLoading, setShowLoading] = useState(false); | |||
const [timeRange, setTimeRange] = useState([ | |||
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')), | |||
]); //日期选择器 | |||
const [currentOrg, setCurrentOrg] = useState(''); | |||
useEffect(() => { | |||
queryMarketReport(); | |||
}, [current, pageSize, timeRange]); | |||
useEffect(() => { | |||
onGetOrgTree(); | |||
}, []); | |||
//计算总计 | |||
const calcTotal = () => { | |||
const originData = JSON.parse(JSON.stringify(data)); | |||
const totalData = {} | |||
data.forEach(dataObj => { | |||
Object.keys(dataObj).forEach(key => { | |||
if (key === 'date') { | |||
totalData.date = '合计'; | |||
} else if (key == 'shopName') { | |||
totalData.shopName = ''; | |||
} else if (typeof dataObj[key] === 'number') { | |||
if (totalData[key] && typeof totalData[key] === 'number') { | |||
totalData[key] += dataObj[key]; | |||
} else { | |||
totalData[key] = 0; | |||
totalData[key] += dataObj[key]; | |||
} | |||
} else { | |||
totalData[key] = '暂无统计' | |||
} | |||
}); | |||
}); | |||
originData.push(totalData); | |||
setData(originData); | |||
} | |||
const queryMarketReport = async () => { | |||
const jsonData = { | |||
startTime: timeRange[0], | |||
endTime: timeRange[1], | |||
current: current, | |||
pageSize: pageSize | |||
} | |||
currentOrg.key && (jsonData.shopId = currentOrg.key); | |||
setShowLoading(true); | |||
const response = await marketAPI.getMarketReportList(jsonData); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setData(response.data.data); | |||
setTotal(response.data.total); | |||
} else { | |||
message.error(response.errors || '获取营销报表失败'); | |||
} | |||
} | |||
//页码变化 | |||
const onPageChange = (current, pageSize) => { | |||
setCurrent(current); | |||
setPageSize(pageSize); | |||
} | |||
//下载Excel | |||
const onDownloadExcel = async () => { | |||
const jsonData = { | |||
startTime: timeRange[0], | |||
endTime: timeRange[1], | |||
} | |||
currentOrg.key && (jsonData.shopId = currentOrg.key); | |||
setShowLoading(true); | |||
const response = await marketAPI.marketingReportExport(jsonData); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
let option = {}; //option代表的就是excel文件 | |||
const date = new Date(); | |||
const excelName = "营销报表-" + date.toLocaleString().replaceAll('\/', '-'); | |||
option.fileName = excelName; //excel文件名称 | |||
const sheetHeader = []; | |||
Object.keys(response.data[0]).forEach(item => { | |||
sheetHeader.push(item); | |||
}); | |||
console.log('sheetHeader', sheetHeader); | |||
option.datas = [ | |||
{ | |||
sheetData: response.data, //excel文件中的数据源 | |||
sheetName: excelName, //excel文件中sheet页名称 | |||
sheetFilter: sheetHeader, //excel文件中需显示的列数据 | |||
sheetHeader: ['ID', '时间', '店铺名称', '店铺ID', '营收流水', | |||
'实际营收', '会员折扣', '优惠券折扣', '活动折扣','客单价-流水单价', '客单价-实收单价', | |||
'单单价-总流水', '单单价-总实收', | |||
'现金-交易笔数', '现金收入', '微信-交易笔数', '微信收入', '会员收入', '非会员收入' | |||
] //excel文件中每列的表头名称 | |||
} | |||
] | |||
let toExcel = new ExportJsonExcel(option); //生成excel文件 | |||
toExcel.saveExcel(); | |||
} else { | |||
message.error(response.errors || '导出失败'); | |||
} | |||
} | |||
//获取组织树 | |||
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; | |||
} | |||
} | |||
}); | |||
} | |||
return ( | |||
<PageContainer> | |||
{showLoading ? <LoadingCard></LoadingCard> : null} | |||
<Card className={styles['data-search-card']}> | |||
<div className={styles['data-search-box']}> | |||
<div className={styles['data-search-left']}> | |||
<RangePicker className={styles['my-range-picker']} 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); | |||
}} /> | |||
<TreeSelect | |||
style={{ | |||
width: '300px', | |||
marginLeft: '50px' | |||
}} | |||
dropdownStyle={{ | |||
maxHeight: 400, | |||
overflow: 'auto', | |||
}} | |||
treeData={orgTree} | |||
onSelect={(value, node) => { | |||
if (node.type === 2 || node === 3) { | |||
setCurrentOrg(node); | |||
} else { | |||
setCurrentOrg(""); | |||
} | |||
}} | |||
placeholder="请选择组织架构" | |||
treeDefaultExpandAll | |||
/> | |||
</div> | |||
<div className={styles['data-search-btns']}> | |||
<Button type="primary" icon={<DownloadOutlined />} size='middle' onClick={onDownloadExcel}> | |||
下载 | |||
</Button> | |||
<Button className={styles['search-btn-item']}>重置</Button> | |||
<Button className={styles['search-btn-item']} type="primary" onClick={queryMarketReport}>查询</Button> | |||
</div> | |||
</div> | |||
</Card> | |||
<Card className={styles['table-card']}> | |||
<Table dataSource={data} columns={columns} bordered={true} sticky={true} scroll={{ x: 1000 }} pagination={false}> | |||
</Table> | |||
<div className={styles['table-page']}> | |||
<Pagination current={current} pageSize={pageSize} total={total} onChange={onPageChange} /> | |||
</div> | |||
</Card> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,41 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-page { | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
height: 50px; | |||
} | |||
.data-search-left { | |||
display: flex; | |||
align-items: center; | |||
} | |||
// 加载中 | |||
.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); | |||
} |
@@ -0,0 +1,26 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
//获取分页数据 | |||
getMarketReportList(data) { | |||
return request(`/kitchen/api/report-statistics/marketing-report`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
//获取组织架构 | |||
getOrgTree() { | |||
return request(`/kitchen/api/report-statistics/org-tree`, { | |||
method: 'GET', | |||
}); | |||
}, | |||
//下载报表 | |||
marketingReportExport(data) { | |||
return request(`/kitchen/api/report-statistics/marketing-report-export`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
} | |||
}; |
@@ -0,0 +1,28 @@ | |||
import React from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker } from 'antd'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
/** | |||
* 会员报表 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
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> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,16 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-card { | |||
} |
@@ -0,0 +1,28 @@ | |||
import React from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker } from 'antd'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
/** | |||
* 产品报表 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
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> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,16 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-card { | |||
} |
@@ -0,0 +1,245 @@ | |||
import React, { useState, useEffect } from 'react'; | |||
import { Button, Card, Image, Timeline, message, Tag } from 'antd'; | |||
import styles from './index.less'; | |||
import { ExclamationCircleOutlined, UserOutlined, RedEnvelopeOutlined, WhatsAppOutlined } from '@ant-design/icons'; | |||
import { history } from 'umi'; | |||
import orderReportAPI from "./service"; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
const orderNumber = history.location.query.orderNumber; | |||
/** | |||
* 订单详情 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
const [orderInfo, setOrderInfo] = useState({ | |||
memberInfo: {}, //用户信息 | |||
goodsInfo: [], //商品信息 | |||
informationRefundInfo: [], //售后信息 | |||
discountInfo: [] //优惠信息 | |||
}); | |||
const [orderStatus, setOrderStatus] = useState({ | |||
isTrue: true | |||
}); | |||
const onQueryOrderDetail = async () => { | |||
if (orderNumber) { | |||
const response = await orderReportAPI.getOrderReporByNumber(orderNumber); | |||
if (response.statusCode === 200) { | |||
setOrderInfo(response.data); | |||
orderIsNormal(response.data); | |||
} else { | |||
message.error(response.errors || '订单详情获取失败'); | |||
} | |||
} | |||
} | |||
const orderIsNormal = (order) => { | |||
let isNormal = { | |||
isTrue: true, | |||
msg: '正常订单' | |||
}; | |||
if (!(order.payMoneyEx === '正常')) { | |||
isNormal.isTrue = false; | |||
isNormal.msg = '订单支付金额异常'; | |||
} | |||
else if (!(order.payOrderTimeEx === '正常')) { | |||
isNormal.isTrue = false; | |||
isNormal.msg = '订单支付时间异常'; | |||
} | |||
else if (!(order.payOrderEx === '正常')) { | |||
isNormal.isTrue = false; | |||
isNormal.msg = '订单支付异常'; | |||
} | |||
else if (!(order.refundMoneyEx === '正常' || order.refundMoneyEx === '' || order.refundMoneyEx === null)) { | |||
isNormal.isTrue = false; | |||
isNormal.msg = '订单退款金额异常'; | |||
} | |||
else if (!(order.refundOrderTimeEx === '正常' || order.refundOrderTimeEx === '' || order.refundOrderTimeEx === null)) { | |||
isNormal.isTrue = false; | |||
isNormal.msg = '订单退款时间异常'; | |||
} | |||
else if (!(order.refundOrderEx === '正常' || order.refundOrderEx === '' || order.refundOrderEx === null)) { | |||
isNormal.isTrue = false; | |||
isNormal.msg = '订单退款异常'; | |||
} | |||
console.log('isNormal', isNormal); | |||
setOrderStatus(isNormal); | |||
} | |||
useEffect(() => { | |||
onQueryOrderDetail(); | |||
}, []); | |||
return ( | |||
<PageContainer> | |||
<div className={styles['order-detail-container']}> | |||
{/* 用户基本信息 */} | |||
<div className={styles['order-head-card']}> | |||
<div className={styles['order-head']}> | |||
<div className={styles['order-number']}> | |||
#{orderInfo.sortId} | |||
</div> | |||
<div className={styles['order-cook-state']}> | |||
{ | |||
orderStatus.isTrue ? <div className={styles['order-status-true']}>正常订单</div> : <div className={styles['order-status-false']}>{orderStatus.msg}</div> | |||
} | |||
</div> | |||
<div className={styles['order-cook-date']}> | |||
{orderInfo.createdAt} | |||
</div> | |||
</div> | |||
<div className={styles['member-info']}> | |||
<div className={styles['member-info-img']}> | |||
<Image | |||
width={100} | |||
height={100} | |||
src={orderInfo.memberInfo.headUrl} | |||
fallback="data:image/png;base64,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" | |||
/> | |||
</div> | |||
<div className={styles['member-info-row']}> | |||
<div className={styles['member-info-head']}> | |||
{orderInfo.memberInfo.memberName || '系统用户'} | |||
</div> | |||
<div className={styles['member-info-label']}> | |||
<span>#门店新客</span> | |||
<span>#门店会员</span> | |||
</div> | |||
</div> | |||
</div> | |||
<div className={styles['member-info-privacy']}> | |||
<ExclamationCircleOutlined /> | |||
<span>为保证服务体验,您在拨打或接听隐私号电话时,可能被录音</span> | |||
</div> | |||
<div className={styles['member-info-item']}> | |||
<span className={styles['member-info-prefix']}> | |||
顾客电话 | |||
</span> | |||
<span className={styles['member-info-suffix']}> | |||
{orderInfo.memberInfo.phone || '暂无电话'} | |||
</span> | |||
</div> | |||
<div className={styles['prepare-meals']}> | |||
<div className={styles['prepare-meals-title']}> | |||
优惠信息 | |||
</div> | |||
<div className={styles['prepare-meals-status']}> | |||
{ | |||
orderInfo.discountInfo?.map(discountItem => { | |||
return ( | |||
<div> | |||
{discountItem.type === 1 ? <Tag icon={<UserOutlined />} color="#f50">{discountItem.name}:{discountItem.value}</Tag> : null} | |||
{discountItem.type === 2 ? <Tag icon={<RedEnvelopeOutlined />} color="#87d068">{discountItem.name}:{discountItem.value}</Tag> : null} | |||
{discountItem.type === 3 ? <Tag icon={<WhatsAppOutlined />} color="#108ee9">{discountItem.name}:{discountItem.value}</Tag> : null} | |||
</div> | |||
) | |||
}) | |||
} | |||
</div> | |||
</div> | |||
</div> | |||
{/* 订单数据 */} | |||
<div className={styles['order-base-card']}> | |||
<div className={styles['goods-title']}> | |||
订单信息 | |||
</div> | |||
<div className={styles['goods-calc-item']}> | |||
<div className={styles['goods-calc-prefix']}>订单编号</div> | |||
<div className={styles['goods-calc-sufix']}>{orderInfo.orderNumber}</div> | |||
</div> | |||
<div className={styles['goods-calc-item']}> | |||
<div className={styles['goods-calc-prefix']}>支付方式</div> | |||
<div className={styles['goods-calc-sufix']}>微信支付</div> | |||
</div> | |||
<div className={styles['goods-calc-item']}> | |||
<div className={styles['goods-calc-prefix']}>下单时间</div> | |||
<div className={styles['goods-calc-sufix']}>{orderInfo.createdAt}</div> | |||
</div> | |||
<div className={styles['goods-calc-item']}> | |||
<div className={styles['goods-calc-prefix']}>就餐人数</div> | |||
<div className={styles['goods-calc-sufix']}>商家按餐量提供</div> | |||
</div> | |||
<div className={styles['goods-calc-item']}> | |||
<div className={styles['goods-calc-prefix']}>桌台号码</div> | |||
<div className={styles['goods-calc-sufix']}>0</div> | |||
</div> | |||
</div> | |||
{/* 商品信息 */} | |||
<div className={styles['goods-card']}> | |||
<div className={styles['goods-title']}> | |||
商品信息 | |||
</div> | |||
<div className={styles['goods-list']}> | |||
{ | |||
orderInfo.goodsInfo.map(item => { | |||
return ( | |||
<div className={styles['goods-item']} key={item.goodsId}> | |||
<div className={styles['goods-item-img']}> | |||
<Image | |||
width={80} | |||
height={80} | |||
src={item.goodsImg} | |||
fallback="data:image/png;base64,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" | |||
/> | |||
</div> | |||
<div className={styles['goods-item-name']}> | |||
{item.goodsName || '暂无名称'} | |||
</div> | |||
<div className={styles['goods-item-price']}> | |||
¥{item.memberPrice} | |||
</div> | |||
<div className={styles['goods-item-count']}> | |||
x{item.count}份 | |||
</div> | |||
</div> | |||
) | |||
}) | |||
} | |||
</div> | |||
{/* 小计 */} | |||
<div className={styles['goods-calc']}> | |||
<div className={styles['goods-calc-title']}>小计</div> | |||
<div className={styles['goods-calc-item']}> | |||
<div className={styles['goods-calc-prefix']}>优惠金额</div> | |||
<div className={styles['goods-calc-sufix']}>¥{orderInfo.discountMoney}</div> | |||
</div> | |||
<div className={`${styles['goods-calc-item']} ${styles['goods-calc-pay']}`}> | |||
<div className={styles['goods-calc-prefix']}>支付金额</div> | |||
<div className={styles['goods-calc-sufix']}>¥{orderInfo.orderRealMoney}</div> | |||
</div> | |||
</div> | |||
</div> | |||
{/* 售后信息 */} | |||
<div className={styles['sale-after-card']}> | |||
<div className={styles['goods-title']}> | |||
售后信息 | |||
</div> | |||
<Timeline> | |||
{ | |||
orderInfo.informationRefundInfo.length > 0 ? | |||
orderInfo.informationRefundInfo.map(item => { | |||
return ( | |||
<Timeline.Item color="green" key={item.id}> | |||
<div className={styles['sale-after-line']}> | |||
{item.createdAt} {item.refundDesc} | |||
</div> | |||
</Timeline.Item> | |||
) | |||
}) | |||
: | |||
<Timeline.Item color="green"> | |||
暂无售后信息 | |||
</Timeline.Item> | |||
} | |||
</Timeline> | |||
</div> | |||
</div> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,201 @@ | |||
.order-detail-container { | |||
display: flex; | |||
justify-content: space-between; | |||
flex-wrap: wrap; | |||
} | |||
.order-head { | |||
display: flex; | |||
align-items: center; | |||
position: relative; | |||
height: 50px; | |||
margin-bottom: 10px; | |||
} | |||
.order-head::before { | |||
position: absolute; | |||
content: ''; | |||
width: 0; | |||
height: 0; | |||
top: 50%; | |||
transform: translateY(-50%); | |||
left: 50px; | |||
border-top: 20px solid transparent; | |||
border-bottom: 20px solid transparent; | |||
border-left: 20px solid rgb(88, 91, 110); | |||
} | |||
.order-number { | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
position: absolute; | |||
width: 70px; | |||
height: 40px; | |||
left: -20px; | |||
top: 50%; | |||
transform: translateY(-50%); | |||
background-color: rgb(88, 91, 110); | |||
color: #FFF; | |||
font-size: 26px; | |||
} | |||
.order-status-true { | |||
font-size: 14px; | |||
padding: 5px 20px; | |||
background-color: #2db7f5; | |||
color: #FFF; | |||
} | |||
.order-status-false { | |||
font-size: 14px; | |||
padding: 5px 20px; | |||
background-color: #F84352; | |||
color: #FFF; | |||
} | |||
.order-cook-state { | |||
font-size: 22px; | |||
margin-left: 100px; | |||
margin-right: 20px; | |||
} | |||
.order-cook-date { | |||
font-size: 16px; | |||
color: rgb(246, 152, 38); | |||
} | |||
// 用户信息 | |||
.member-info { | |||
display: flex; | |||
} | |||
.member-info-img { | |||
margin-right: 20px; | |||
} | |||
.member-info-row { | |||
display: flex; | |||
flex-direction: column; | |||
justify-content: center; | |||
} | |||
.member-info-head { | |||
font-size: 22px; | |||
margin-right: 10px; | |||
} | |||
.member-info-label { | |||
font-size: 16px; | |||
color: #999; | |||
} | |||
.member-info-label span { | |||
margin-right: 10px; | |||
} | |||
.member-info-privacy { | |||
margin: 10px 0; | |||
font-size: 16px; | |||
color: rgb(88, 91, 110); | |||
} | |||
.member-info-privacy span { | |||
margin-left: 10px; | |||
} | |||
.member-info-prefix { | |||
font-size: 16px; | |||
color: rgb(88, 91, 110); | |||
margin-right: 10px; | |||
} | |||
.member-info-suffix { | |||
font-size: 16px; | |||
color: #222; | |||
} | |||
// 备餐状态 | |||
.prepare-meals { | |||
margin-top: 20px; | |||
font-size: 16px; | |||
color: #222; | |||
} | |||
.prepare-meals-title { | |||
font-weight: 700; | |||
} | |||
// 商品信息 | |||
.goods-card { | |||
// margin-top: 20px; | |||
} | |||
.goods-title { | |||
font-size: 22px; | |||
font-weight: 700; | |||
margin-bottom: 10px; | |||
} | |||
.goods-item { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
margin-bottom: 10px; | |||
font-size: 16px; | |||
color: #222; | |||
} | |||
// 小计 | |||
.goods-calc { | |||
padding-top: 10px; | |||
border-top: 1px dashed #dedede; | |||
font-size: 16px; | |||
} | |||
.goods-calc-title { | |||
font-size: 16px; | |||
font-weight: 700; | |||
} | |||
.goods-calc-item { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
font-size: 16px; | |||
} | |||
.goods-calc-pay .goods-calc-sufix { | |||
color: #F84352; | |||
} | |||
// 订单信息 | |||
.sale-after-line { | |||
font-size: 16px; | |||
} | |||
@media screen and (min-width: 1200px) { | |||
.order-head-card, | |||
.goods-card, | |||
.order-base-card, | |||
.sale-after-card { | |||
padding: 20px; | |||
background-color: #FFF; | |||
width: 49.5%; | |||
box-sizing: border-box; | |||
} | |||
} | |||
@media screen and (max-width: 1200px) { | |||
.order-head-card, | |||
.goods-card, | |||
.order-base-card, | |||
.sale-after-card { | |||
padding: 20px; | |||
background-color: #FFF; | |||
width: 100%; | |||
box-sizing: border-box; | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
getOrderReporByNumber(orderNumber) { | |||
return request(`/kitchen/api/report-statistics/order-info-report/${orderNumber}`, { | |||
method: 'GET', | |||
}); | |||
} | |||
}; |
@@ -0,0 +1,299 @@ | |||
import React, { useState, useEffect } from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker, Table, message, Pagination, Space, Tag, Spin, TreeSelect } from 'antd'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
import orderReportAPI from "./service"; | |||
import moment from 'moment'; | |||
import { history } from "umi"; | |||
import { CheckCircleOutlined, CloseCircleOutlined, DownloadOutlined } from '@ant-design/icons'; | |||
import ExportJsonExcel from "js-export-excel" | |||
const columns = [ | |||
{ | |||
title: '交易单号', | |||
dataIndex: 'transactionId', | |||
key: 'transactionId', | |||
}, | |||
{ | |||
title: '店铺名称', | |||
dataIndex: 'shopName', | |||
key: 'shopName', | |||
}, | |||
{ | |||
title: '原价(¥)', | |||
dataIndex: 'orderOriginalMoney', | |||
key: 'orderOriginalMoney', | |||
}, | |||
{ | |||
title: '优惠(¥)', | |||
dataIndex: 'discountMoney', | |||
key: 'discountMoney', | |||
}, | |||
{ | |||
title: '实际支付(¥)', | |||
dataIndex: 'orderRealMoney', | |||
key: 'orderRealMoney', | |||
}, | |||
{ | |||
title: '创建时间', | |||
dataIndex: 'createdAt', | |||
key: 'createdAt', | |||
}, | |||
{ | |||
title: '订单状态', | |||
key: 'orderStatus', | |||
render: (record) => { | |||
let isNormal = true; | |||
if (!(record.payMoneyEx === '正常' && | |||
record.payOrderTimeEx === '正常' && | |||
record.payOrderEx === '正常') | |||
) { | |||
isNormal = false; | |||
} | |||
if (!(record.refundMoneyEx === '正常' || record.refundMoneyEx === '' || record.refundMoneyEx === null)) { | |||
isNormal = false; | |||
} | |||
if (!(record.refundOrderTimeEx === '正常' || record.refundOrderTimeEx === '' || record.refundOrderTimeEx === null)) { | |||
isNormal = false; | |||
} | |||
if (!(record.refundOrderEx === '正常' || record.refundOrderEx === '' || record.refundOrderEx === null)) { | |||
isNormal = false; | |||
} | |||
return ( | |||
<> | |||
{ | |||
isNormal ? <Tag icon={<CheckCircleOutlined />} color="success"> | |||
正常 | |||
</Tag> | |||
: | |||
<Tag icon={<CloseCircleOutlined />} color="error"> | |||
异常 | |||
</Tag> | |||
} | |||
</> | |||
) | |||
}, | |||
}, | |||
{ | |||
title: '售后信息', | |||
key: 'orderStatus', | |||
render: (record) => { | |||
return ( | |||
<> | |||
{ | |||
record.isRefund ? <Tag color="error"> | |||
有 | |||
</Tag> | |||
: | |||
<Tag color="success"> | |||
无 | |||
</Tag> | |||
} | |||
</> | |||
) | |||
}, | |||
}, | |||
{ | |||
title: '操作', | |||
key: 'action', | |||
render: (_, record) => ( | |||
<Space size="middle"> | |||
<a onClick={() => { | |||
history.push({ | |||
pathname: '/order/order-report/order-report-detail', | |||
query: { | |||
orderNumber: record.orderNumber | |||
} | |||
}); | |||
}}>详情</a> | |||
</Space> | |||
), | |||
}, | |||
]; | |||
const LoadingCard = () => { | |||
return ( | |||
<div className={styles['loading-card']}> | |||
<Spin size="large" /> | |||
</div> | |||
) | |||
} | |||
/** | |||
* 订单报表 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
//订单报表列表 | |||
const [orderReportList, setOrderReportList] = useState([]); | |||
const [current, setCurrent] = useState(1); | |||
const [pageSize, setPageSize] = useState(10); | |||
const [total, setTotal] = useState(0); | |||
const [showLoading, setShowLoading] = useState(false); | |||
const [currentOrg, setCurrentOrg] = useState(''); | |||
const [orgTree, setOrgTree] = useState([]); | |||
const [timeRange, setTimeRange] = useState([ | |||
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')), | |||
]); //日期选择器 | |||
useEffect(() => { | |||
onQueryOrderReportList(); | |||
}, [current, pageSize]); | |||
useEffect(() => { | |||
onGetOrgTree(); | |||
}, []); | |||
//获取订单列表 | |||
const onQueryOrderReportList = async () => { | |||
const jsonData = { | |||
startTime: timeRange[0], | |||
endTime: timeRange[1], | |||
current, | |||
pageSize | |||
} | |||
currentOrg.key && (jsonData.shopId = currentOrg.key); | |||
setShowLoading(true); | |||
const response = await orderReportAPI.getOrderReportList(jsonData); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setOrderReportList(response.data.data); | |||
setTotal(response.data.total) | |||
} else { | |||
message.error(response.errors || '获取订单报表列表失败'); | |||
} | |||
} | |||
//页码变化 | |||
const onPageChange = (current, pageSize) => { | |||
setCurrent(current); | |||
setPageSize(pageSize); | |||
} | |||
//获取组织树 | |||
const onGetOrgTree = async () => { | |||
setShowLoading(true); | |||
const response = await orderReportAPI.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; | |||
} | |||
} | |||
}); | |||
} | |||
//下载Excel | |||
const onDownloadExcel = async () => { | |||
const jsonData = { | |||
startTime: timeRange[0], | |||
endTime: timeRange[1], | |||
} | |||
currentOrg.key && (jsonData.shopId = currentOrg.key); | |||
setShowLoading(true); | |||
const response = await orderReportAPI.orderReportExport(jsonData); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
let option = {}; //option代表的就是excel文件 | |||
const date = new Date(); | |||
const excelName = "订单报表-" + date.toLocaleString().replaceAll('\/', '-'); | |||
option.fileName = excelName; //excel文件名称 | |||
const sheetHeader = []; | |||
Object.keys(response.data[0]).forEach(item => { | |||
sheetHeader.push(item); | |||
}); | |||
option.datas = [ | |||
{ | |||
sheetData: response.data, //excel文件中的数据源 | |||
sheetName: excelName, //excel文件中sheet页名称 | |||
sheetFilter: sheetHeader, //excel文件中需显示的列数据 | |||
sheetHeader: ['ID', '取餐号', '用户Id', '优惠合集', '订单号(本系统)', | |||
'交易号', '店铺ID', '店铺名字', '商品总价', '优惠金额', '实付金额', | |||
'创建时间', '支付金额异常', | |||
'订单支付时间异常', '订单异常', '售后金额异常', '售后时间异常', '售后订单异常' | |||
] //excel文件中每列的表头名称 | |||
} | |||
] | |||
let toExcel = new ExportJsonExcel(option); //生成excel文件 | |||
toExcel.saveExcel(); | |||
} else { | |||
message.error(response.errors || '导出失败'); | |||
} | |||
} | |||
return ( | |||
<PageContainer> | |||
{showLoading ? <LoadingCard></LoadingCard> : null} | |||
<Card className={styles['data-search-card']}> | |||
<div className={styles['data-search-box']}> | |||
<div className={styles['data-search-left']}> | |||
<RangePicker className={styles['my-range-picker']} 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); | |||
}} /> | |||
<TreeSelect | |||
style={{ | |||
width: '300px', | |||
marginLeft: '50px' | |||
}} | |||
dropdownStyle={{ | |||
maxHeight: 400, | |||
overflow: 'auto', | |||
}} | |||
treeData={orgTree} | |||
onSelect={(value, node) => { | |||
console.log('node>>>', node); | |||
if (node.type === 2 || node === 3) { | |||
setCurrentOrg(node); | |||
} else { | |||
setCurrentOrg(""); | |||
} | |||
}} | |||
placeholder="请选择组织架构" | |||
treeDefaultExpandAll | |||
/> | |||
</div> | |||
<div className={styles['data-search-btns']}> | |||
<Button type="primary" icon={<DownloadOutlined />} size='middle' onClick={onDownloadExcel}> | |||
下载 | |||
</Button> | |||
<Button className={styles['search-btn-item']}>重置</Button> | |||
<Button className={styles['search-btn-item']} type="primary" onClick={onQueryOrderReportList}>查询</Button> | |||
</div> | |||
</div> | |||
</Card> | |||
<Card className={styles['table-card']}> | |||
<Table dataSource={orderReportList} columns={columns} pagination={false} /> | |||
<div className={styles['table-page']}> | |||
<Pagination current={current} pageSize={pageSize} total={total} onChange={onPageChange} /> | |||
</div> | |||
</Card> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,36 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-page { | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
height: 50px; | |||
} | |||
// 加载中 | |||
.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); | |||
} |
@@ -0,0 +1,25 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
getOrderReportList(data) { | |||
return request(`/kitchen/api/report-statistics/order-report`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
//获取组织架构 | |||
getOrgTree() { | |||
return request(`/kitchen/api/report-statistics/org-tree`, { | |||
method: 'GET', | |||
}); | |||
}, | |||
//下载报表 | |||
orderReportExport(data) { | |||
return request(`/kitchen/api/report-statistics/order-report-export`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
} | |||
}; |
@@ -0,0 +1,28 @@ | |||
import React from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker } from 'antd'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
/** | |||
* 营收报表 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
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> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,16 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-card { | |||
} |
@@ -0,0 +1,229 @@ | |||
import React, { useState, useEffect } from 'react'; | |||
import { PageContainer } from '@ant-design/pro-layout'; | |||
import { Button, Card, DatePicker, Table, Pagination, Spin, Select, message } from 'antd'; | |||
import styles from './index.less'; | |||
const { RangePicker } = DatePicker; | |||
import costSalesAPI from "./service"; | |||
import moment from 'moment'; | |||
const columns = [ | |||
{ | |||
title: '门店名称', | |||
dataIndex: 'storeId', | |||
key: 'storeId', | |||
}, | |||
{ | |||
title: '商品名称', | |||
dataIndex: 'goodsId', | |||
key: 'goodsId', | |||
}, | |||
{ | |||
title: '毛利率', | |||
dataIndex: 'marginRatio', | |||
key: 'marginRatio', | |||
} | |||
]; | |||
const LoadingCard = () => { | |||
return ( | |||
<div className={styles['loading-card']}> | |||
<Spin size="large" /> | |||
</div> | |||
) | |||
} | |||
/** | |||
* 销售毛利分析 | |||
* @returns | |||
*/ | |||
export default function Index() { | |||
//订单报表列表 | |||
const [costSalesData, setCostSalesData] = useState([]); | |||
const [current, setCurrent] = useState(1); | |||
const [pageSize, setPageSize] = useState(10); | |||
const [total, setTotal] = useState(0); | |||
const [showLoading, setShowLoading] = useState(false); | |||
const [timeRange, setTimeRange] = useState([ | |||
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')), | |||
]); //日期选择器 | |||
//门店 | |||
const [storeIdArray, setStoreIdArray] = useState([]); | |||
const [storeSelect, setStoreSelect] = useState([]); | |||
//商品 | |||
const [goodsIdArray, setGoodsIdArray] = useState([]); | |||
const [goodsIdSelect, setGoodsIdSelect] = useState([]); | |||
//商品类型 | |||
const [goodsTypeArray, setGoodsTypeArray] = useState([]); | |||
const [goodsTypeSelect, setGoodsTypeSelect] = useState([]); | |||
//页码变化 | |||
const onPageChange = (current, pageSize) => { | |||
setCurrent(current); | |||
setPageSize(pageSize); | |||
} | |||
//查询 | |||
const onQueryReportSalescost = async () => { | |||
const jsonData = { | |||
"storeId": storeIdArray, | |||
"goodsId": goodsIdArray, | |||
"goodsTypeId": goodsTypeArray, | |||
"begintime": timeRange[0], | |||
"endtime": timeRange[1], | |||
current, | |||
pageSize | |||
} | |||
setShowLoading(true); | |||
const response = await costSalesAPI.getReportSalesmargin({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setCostSalesData(response.data.data); | |||
} else { | |||
message.error(response.errors || '获取销售成本失败'); | |||
} | |||
} | |||
/** | |||
* 查询店铺列表 | |||
*/ | |||
const onQueryStoreList = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.gettree({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
const storeList = []; | |||
response.data.forEach(item => { | |||
if (item.type === 2 || item.type === 3) { | |||
storeList.push(item) | |||
} | |||
}) | |||
setStoreSelect(storeList); | |||
} else { | |||
message.error('查询店铺列表失败'); | |||
} | |||
} | |||
//查询商品列表 | |||
const onQueryGoodsList = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.goodsList({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setGoodsIdSelect(response.data); | |||
} else { | |||
message.error('查询商品列表失败'); | |||
} | |||
} | |||
//查询商品分类类型列表 | |||
const onQueryGoodsType = async () => { | |||
setShowLoading(true); | |||
const response = await costSalesAPI.goodsTypeList({}); | |||
setShowLoading(false); | |||
if (response.statusCode === 200) { | |||
setGoodsTypeSelect(response.data); | |||
} else { | |||
message.error('查询商品分类列表失败'); | |||
} | |||
} | |||
useEffect(() => { | |||
onQueryReportSalescost(); | |||
onQueryStoreList(); | |||
onQueryGoodsList(); | |||
onQueryGoodsType(); | |||
}, []); | |||
return ( | |||
<PageContainer> | |||
{showLoading ? <LoadingCard></LoadingCard> : null} | |||
<Card className={styles['data-search-card']}> | |||
<div className={styles['data-search-box']}> | |||
<div className={styles['data-search-left']}> | |||
<Select | |||
showSearch | |||
mode="tags" | |||
size="middle" | |||
allowClear | |||
placeholder="请选择门店" | |||
onChange={(values) => setStoreIdArray(values)} | |||
filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())} | |||
style={{ | |||
width: '300px', | |||
marginRight: '50px' | |||
}} | |||
> | |||
{ | |||
storeSelect.map( (item) => { | |||
return ( | |||
<Option key={item.key}>{item.title}</Option> | |||
) | |||
}) | |||
} | |||
</Select> | |||
<Select | |||
showSearch | |||
mode="tags" | |||
size="middle" | |||
allowClear | |||
placeholder="请选择商品" | |||
onChange={(values) => setGoodsIdArray(values)} | |||
filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())} | |||
style={{ | |||
width: '300px', | |||
marginRight: '50px' | |||
}} | |||
> | |||
{ | |||
goodsIdSelect.map( (item, index) => { | |||
return ( | |||
<Option key={item.id}>{item.name}</Option> | |||
) | |||
}) | |||
} | |||
</Select> | |||
<Select | |||
showSearch | |||
mode="tags" | |||
size="middle" | |||
allowClear | |||
placeholder="请选择商品类别" | |||
onChange={(values) => setGoodsTypeArray(values)} | |||
filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())} | |||
style={{ | |||
width: '300px', | |||
marginRight: '50px' | |||
}} | |||
> | |||
{ | |||
goodsTypeSelect.map( (item) => { | |||
return ( | |||
<Option key={item.id}>{item.name}</Option> | |||
) | |||
}) | |||
} | |||
</Select> | |||
<RangePicker size='middle' className={styles['my-range-picker']} 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); | |||
}} /> | |||
</div> | |||
<div className={styles['data-search-btns']}> | |||
<Button className={styles['search-btn-item']}>重置</Button> | |||
<Button className={styles['search-btn-item']} type="primary" onClick={onQueryReportSalescost}>查询</Button> | |||
</div> | |||
</div> | |||
</Card> | |||
<Card className={styles['table-card']}> | |||
<Table dataSource={costSalesData} columns={columns} pagination={false} /> | |||
<div className={styles['table-page']}> | |||
<Pagination current={current} pageSize={pageSize} total={total} onChange={onPageChange} /> | |||
</div> | |||
</Card> | |||
</PageContainer> | |||
) | |||
} |
@@ -0,0 +1,36 @@ | |||
.data-search-card { | |||
margin-bottom: 20px; | |||
} | |||
.data-search-box { | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
} | |||
.search-btn-item { | |||
margin-left: 20px; | |||
} | |||
.table-page { | |||
display: flex; | |||
justify-content: flex-end; | |||
align-items: center; | |||
height: 50px; | |||
} | |||
// 加载中 | |||
.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); | |||
} |
@@ -0,0 +1,39 @@ | |||
import { request } from 'umi'; | |||
export default { | |||
getReportSalesmargin(data) { | |||
return request(`/kitchen/api/report/salesmargin`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
/** | |||
* 查询商品信息 | |||
*/ | |||
goodsList(data) { | |||
return request(`/kitchen/api/goodes/list`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
/** | |||
* 查询商品类型 | |||
*/ | |||
goodsTypeList(data) { | |||
return request(`/kitchen/api/goodstype/list`, { | |||
method: 'Post', | |||
data: data, | |||
}); | |||
}, | |||
/** | |||
* 查询门店列表 | |||
*/ | |||
gettree(params) { | |||
return request('/kitchen/api/sysOrg/tree', { | |||
data: { | |||
...params, | |||
}, | |||
}); | |||
} | |||
}; |