697 lines
23 KiB
Vue
Raw Normal View History

2025-06-14 10:30:10 +08:00
<template>
<div class="pageBox">
<div class="searchBox">
<div class="row row1">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="filter.title" class="wid100" clearable></el-input></div>
</div>
<div class="row row1">
<span class="span">规格编码</span>
<div class="right"><el-input v-model="filter.sku_code" class="wid100" clearable></el-input></div>
</div>
<div class="row row1">
<span class="span">商品品牌</span>
<div class="right">
<el-select v-model="filter.brand_id" placeholder="请选择" clearable class="wid100">
<el-option v-for="it in brandList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</div>
</div>
<div class="row row1">
<span class="span">赠品</span>
<div class="right">
<el-select v-model="filter.gift" placeholder="请选择" :clearable="false" class="wid100">
<el-option label="全部" value="all" />
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</div>
</div>
<div class="row">
<span class="span">时间区间</span>
<div class="right">
<el-date-picker
v-model="rangeTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 250px;">
</el-date-picker>
</div>
</div>
<div class="row">
<span class="span"></span>
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div>
<el-card shadow="never">
2025-08-29 17:25:42 +08:00
<div class="opaBox">
<el-button type="warning" @click="handleExport" :loading="exportLoading"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button>
</div>
2025-06-14 10:30:10 +08:00
<el-table :data="statisticsList" style="width: 100%" border v-loading="loading" @sort-change="sortChange"
:default-sort="{
prop: 'number,order_num,seven_day_avg_number,stock_wait,three_day_avg_number,three_day_stock_wait,thirty_day_number,actual_inventory,total_profit',
}">
<el-table-column prop="date" label="日期" align="center" />
<el-table-column prop="type" label="商品信息" width="220">
<template #default="scope">
<div class="goodInfo" v-if="scope.row.goods_sku">
<div class="imgBox" v-if="scope.row.goods_sku.goods && scope.row.goods_sku.goods.images">
<el-image v-for="(it, i) in scope.row.goods_sku.goods.images" :key="i" :z-index="9999"
:src="it" :hide-on-click-modal="true" :preview-src-list="[scope.row.goods_sku.goods.images]"
fit="cover" :preview-teleported="true" />
</div>
<div class="tit">{{ scope.row.goods_sku.goods.title }}({{ scope.row.goods_sku.title }})</div>
</div>
</template>
</el-table-column>
<el-table-column prop="number" label="销量" align="center" sortable="custom" />
<el-table-column prop="order_num" label="单数" align="center" sortable="custom" />
<el-table-column prop="seven_day_avg_number" label="7天日销" align="center" sortable="custom" />
<el-table-column prop="stock_wait" label="7天周转天数" align="center" sortable="custom" />
<el-table-column prop="three_day_avg_number" label="3天日销" align="center" sortable="custom" />
<el-table-column prop="three_day_stock_wait" label="3天周转天数" align="center" sortable="custom" />
<el-table-column prop="thirty_day_number" label="近30天销量" align="center" sortable="custom" />
<el-table-column prop="actual_inventory" label="总库存" align="center" sortable="custom">
<template #header>
<span style="margin-right: 5px;">总库存</span>
<span>
<el-tooltip placement="top" :hide-after="0" :show-after="200">
<template #content>总库存包含未锁定和锁定库存括号里为锁定库存</template>
<el-icon size="18"><QuestionFilled /></el-icon>
</el-tooltip>
</span>
</template>
<template #default="scope">
<div v-if="scope.row.goods_sku">{{ scope.row.goods_sku.actual_inventory }}{{ scope.row.goods_sku.lock_in_stock }}</div>
</template>
</el-table-column>
2025-08-29 17:25:42 +08:00
<el-table-column prop="goods_cost" label="商品成本" align="center" />
<el-table-column prop="freight_cost" label="运费成本" align="center" />
2025-06-14 10:30:10 +08:00
<el-table-column prop="refund_amount" label="退款金额" align="center" />
<el-table-column prop="red_refund_amount" label="红包退款有责金额" align="center" />
<el-table-column prop="total_profit" label="总利润" align="center" />
<el-table-column label="数据" align="center" width="180">
<template #default="scope">
<el-button type="primary" circle @click="handleAnalysis(scope.row.sku_code)" title="店铺数据"><el-icon><Shop /></el-icon></el-button>
<el-button type="primary" circle :loading="scope.row.loading" @click="trendCharts(scope.row)" title="趋势图"><el-icon><TrendCharts /></el-icon></el-button>
<el-button type="primary" circle @click="orderCharts(scope.row)" title="订单商品销量趋势"><el-icon><List /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="1000px" title="店铺数据">
<div>
<div style="margin-bottom: 15px;">
<span>时间</span>
<el-date-picker
v-model="pickTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 250px;"
:clearable="false"
@change="getDialogList()">
</el-date-picker>
</div>
<div class="tabBox">
<el-table :data="dialogList" style="width: 100%" border v-loading="opa_loading">
<el-table-column label="排名" width="70" align="center">
<template #default="scope">
<span>{{ scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="mx_shop_name" label="店铺" align="center" />
<el-table-column prop="date" label="日期" align="center" />
<el-table-column prop="number" label="销量" align="center" />
<el-table-column prop="order_num" label="单数" align="center" />
<el-table-column prop="avg_number" label="日销量" align="center" />
<el-table-column prop="total_price" label="支付金额" align="center" />
<el-table-column prop="refund_amount" label="退款金额" align="center" />
<el-table-column prop="total_profit" label="利润" align="center" />
<el-table-column label="趋势图" align="center" width="80">
<template #default="scope">
<el-button type="primary" circle @click="getDataLine(scope.row)" :loading="scope.row.loading"><el-icon><TrendCharts /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-dialog>
<el-dialog v-model="showChart" width="900px" title="趋势图分析">
<el-form label-width="110px">
<el-form-item label="时间:">
<div>
<el-date-picker
v-model="chartTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 250px;"
:clearable="false"
@change="changeTime0()">
</el-date-picker>
</div>
</el-form-item>
</el-form>
<div id="lineChart" style="width: 100%;height:500px;" v-loading="loading1"></div>
</el-dialog>
<el-dialog v-model="showTrend" width="900px" title="趋势图分析">
<el-form label-width="110px" :inline="true">
<el-form-item label="时间:">
<div>
<el-date-picker
v-model="trendTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 250px;"
:clearable="false"
@change="changeTime()">
</el-date-picker>
</div>
</el-form-item>
<el-form-item label="总计:"><span style="color: #f00;">{{ sumDaily }}</span></el-form-item>
<el-form-item label="平均值:"><span style="color: #f00;">{{ avg }}</span></el-form-item>
2025-06-14 10:30:10 +08:00
</el-form>
<div id="trendChart" style="width: 100%;height:500px;" v-loading="loading1"></div>
</el-dialog>
<el-dialog v-model="showOrderTrend" width="900px" title="订单商品销量趋势">
<el-form label-width="80px" :inline="true">
<el-form-item label="店铺:">
<el-select v-model="from_shop_ids" @change="changeOrderTime()" placeholder="请选择" clearable filterable multiple collapse-tags style="width: 200px;">
<el-option v-for="it in shopsList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="时间:">
<div>
<el-date-picker
v-model="orderTrendTime"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
style="width: 340px;"
:clearable="false"
@change="changeOrderTime()">
</el-date-picker>
</div>
</el-form-item>
<el-form-item label="粒度:">
<el-radio-group v-model="grain_size" size="mini" @change="changeOrderTime()">
<el-radio-button label="60分钟" :value="60" />
<el-radio-button label="30分钟" :value="30" />
<el-radio-button label="15分钟" :value="15" />
</el-radio-group>
</el-form-item>
</el-form>
<div id="orderChart" style="width: 100%;height:500px;" v-loading="loading1"></div>
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from "vue"
import { get } from "@/api/request"
import { Search, Shop, TrendCharts, QuestionFilled, List } from '@element-plus/icons'
import * as echarts from 'echarts'
2025-08-29 17:25:42 +08:00
import { ElMessage } from 'element-plus'
2025-06-14 10:30:10 +08:00
import dayjs from 'dayjs'
export default {
components: {
Search, Shop, TrendCharts, QuestionFilled, List
},
setup() {
const data = reactive({
filter: {
title: '',
gift: 0
},
rangeTime: [],
statisticsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
opaType: '',
showDialog: false,
opa_loading: false,
itemId: 0,
from_shop_id: 0,
dialogList: [],
pickTime: [],
loading1: false,
dataLineX: [],
dataLineY: [],
showChart: false,
brandList: [],
ascOrDesc: 'desc',
sort: 'number',
showTrend: false,
sumDaily: 0,
avg: 0,
2025-06-14 10:30:10 +08:00
trendTime: [],
chartTime: [],
showOrderTrend: false,
orderTrendTime: [],
from_shop_ids: [],
shopsList: [],
grain_size: 60,
2025-08-29 17:25:42 +08:00
dataLineY2: [],
exportLoading: false
2025-06-14 10:30:10 +08:00
})
function handleSearch() {
data.page = 1
fetchData()
}
function getShopsList() {
get(`/api/mxShops`).then((res) => {
data.shopsList = res.data
})
}
const fetchData = () => {
data.loading = true
let params = {
...data.filter,
page: data.page,
pageSize: data.pageSize,
start_date: data.rangeTime ? data.rangeTime[0] : '',
end_date: data.rangeTime ? data.rangeTime[1] : '',
direction: data.ascOrDesc,
order_by_column: data.sort
}
params.brand_id = data.filter.brand_id || 0
get(`/api/orderItemDailyReport`, params).then((res) => {
data.statisticsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAnalysis(sku_code) {
let end = dayjs().format('YYYY-MM-DD')
let start = dayjs().subtract(30, 'day').format('YYYY-MM-DD')
data.pickTime = [end, end]
data.itemId = sku_code
getDialogList()
data.showDialog = true
}
const getDialogList = () => {
data.opa_loading = true
let params = {
sku_code: data.itemId,
start_date: data.pickTime ? data.pickTime[0] : '',
end_date: data.pickTime ? data.pickTime[1] : ''
}
get(`/api/mxShopDailyReport`, params).then((res) => {
data.dialogList = res.data
data.opa_loading = false
}).catch(() => {
data.opa_loading = false
})
}
const getDataLine = (row) => {
row.loading = true
data.loading1 = true
let end = dayjs().format('YYYY-MM-DD')
let start = dayjs().subtract(30, 'day').format('YYYY-MM-DD')
data.chartTime = [start, end]
data.from_shop_id = row.from_shop_id
let params = {
from_shop_id: row.from_shop_id,
sku_code: data.itemId,
start_date: data.chartTime ? data.chartTime[0] : '',
end_date: data.chartTime ? data.chartTime[1] : ''
}
get(`/api/mxShopTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
res.data.forEach((item) => {
data.dataLineX.push(item.date)
data.dataLineY.push(item.number)
})
data.showChart = true
setTimeout(() => {
getTrendChart('lineChart')
data.loading1 = false
}, 500)
row.loading = false
}).catch(() => {
row.loading = false
})
}
function changeTime0() {
data.loading1 = true
let params = {
from_shop_id: data.from_shop_id,
sku_code: data.itemId,
start_date: data.chartTime ? data.chartTime[0] : '',
end_date: data.chartTime ? data.chartTime[1] : ''
}
get(`/api/mxShopTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
res.data.forEach((item) => {
data.dataLineX.push(item.date)
data.dataLineY.push(item.number)
})
setTimeout(() => {
getTrendChart('lineChart')
data.loading1 = false
}, 500)
}).catch(() => {
data.loading = false
})
}
function getTrendChart(id, val = 0) {
let myChart = echarts.init(document.getElementById(id))
let option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['销量']
},
xAxis: {
type: 'category',
data: data.dataLineX
},
yAxis: {},
series: [
{
name: '销量',
type: 'line',
smooth: true,
data: data.dataLineY
}
]
}
if(val == 2) {
option.legend.data.push('累计销量')
option.series.push({
name: '累计销量',
type: 'line',
smooth: true,
data: data.dataLineY2
})
}
myChart.setOption(option)
}
function trendCharts(row) {
data.itemId = row.sku_code
row.loading = true
data.loading1 = true
let end = dayjs().format('YYYY-MM-DD')
let start = dayjs().subtract(30, 'day').format('YYYY-MM-DD')
data.trendTime = [start, end]
let params = {
sku_code: row.sku_code,
start_date: data.trendTime ? data.trendTime[0] : '',
end_date: data.trendTime ? data.trendTime[1] : ''
}
get(`/api/orderItemDailyTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
res.data.forEach((item) => {
data.dataLineX.push(item.date)
data.dataLineY.push(item.number)
})
data.avg = res.avg
2025-06-14 10:30:10 +08:00
data.sumDaily = res.sum
data.showTrend = true
setTimeout(() => {
getTrendChart('trendChart')
data.loading1 = false
}, 500)
row.loading = false
}).catch(() => {
row.loading = false
})
}
function changeTime() {
data.loading1 = true
let params = {
sku_code: data.itemId,
start_date: data.trendTime ? data.trendTime[0] : '',
end_date: data.trendTime ? data.trendTime[1] : ''
}
get(`/api/orderItemDailyTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
res.data.forEach((item) => {
data.dataLineX.push(item.date)
data.dataLineY.push(item.number)
})
data.sumDaily = res.sum
2025-06-20 11:10:09 +08:00
data.avg = res.avg
2025-06-14 10:30:10 +08:00
setTimeout(() => {
getTrendChart('trendChart')
data.loading1 = false
}, 500)
data.loading1 = false
}).catch(() => {
data.loading1 = false
})
}
function getBrandList() {
get(`/api/all/brands`).then((res) => {
data.brandList = res.data
})
}
function sortChange({ prop, order }) {
console.log(prop, order)
let arr = prop.split('.')
let length = arr.length
data.ascOrDesc = order == 'ascending' ? 'asc' : 'desc'
data.sort = arr[length - 1]
fetchData()
}
function orderCharts(row) {
data.from_shop_ids = []
data.itemId = row.sku_code
data.grain_size = 60
let end = dayjs().format('YYYY-MM-DD HH:mm:ss')
let start = dayjs().format('YYYY-MM-DD') + ' 00:00:00'
data.orderTrendTime = [start, end]
data.showOrderTrend = true
getOrderCharts()
}
function changeOrderTime() {
getOrderCharts()
}
function getOrderCharts() {
data.loading1 = true
let params = {
sku_code: data.itemId,
from_shop_ids: data.from_shop_ids,
start_time: data.orderTrendTime ? data.orderTrendTime[0] : '',
end_time: data.orderTrendTime ? data.orderTrendTime[1] : '',
minute: data.grain_size
}
get(`/api/orderItemTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
data.dataLineY2 = []
res.data.forEach((item) => {
data.dataLineX.push(item.time_period)
data.dataLineY.push(item.total_number)
data.dataLineY2.push(item.total)
})
setTimeout(() => {
getTrendChart('orderChart', 2)
data.loading1 = false
}, 500)
data.loading1 = false
}).catch(() => {
data.loading1 = false
})
}
2025-08-29 17:25:42 +08:00
const handleExport = () => {
data.exportLoading = true
let params = {
...data.filter,
page: data.page,
pageSize: data.pageSize,
start_date: data.rangeTime ? data.rangeTime[0] : '',
end_date: data.rangeTime ? data.rangeTime[1] : '',
direction: data.ascOrDesc,
order_by_column: data.sort,
export: 1
}
params.brand_id = data.filter.brand_id || 0
get(`/api/orderItemDailyReport`, params, 'blob').then((res) => {
downLoadXls(res)
ElMessage({ type: "success", message: "导出成功!" })
data.exportLoading = false
}).catch(() => {
data.exportLoading = false
})
}
function downLoadXls(response) {
const content = response
const blob = new Blob([content])
const fileName = `数据统计.xlsx`
if ('download' in document.createElement('a')) {
// 非IE下载
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href) // 释放URL 对象
document.body.removeChild(elink)
} else {
// IE10+下载
navigator.msSaveBlob(blob, fileName)
}
}
2025-06-14 10:30:10 +08:00
onMounted(() => {
fetchData()
getBrandList()
getShopsList()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAnalysis,
getDataLine,
getBrandList,
getDialogList,
sortChange,
trendCharts,
changeTime,
changeTime0,
orderCharts,
getOrderCharts,
changeOrderTime,
2025-08-29 17:25:42 +08:00
getShopsList,
handleExport
2025-06-14 10:30:10 +08:00
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: auto;
box-sizing: border-box;
margin-bottom: 15px;
margin-right: 15px;
&.row1{
width: 300px;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 80px);
}
}
}
2025-08-29 17:25:42 +08:00
.opaBox{
margin-bottom: 15px;
}
2025-06-14 10:30:10 +08:00
.imgBox{
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
display: inline-block;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>