销售看板和供货看板
This commit is contained in:
481
gear-ui3/src/views/oms/dashboard/index.vue
Normal file
481
gear-ui3/src/views/oms/dashboard/index.vue
Normal file
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div class="sales-dashboard">
|
||||
<el-loading v-if="loading" fullscreen text="数据加载中..." />
|
||||
|
||||
<!-- 指标卡区域 -->
|
||||
<el-row :gutter="20" class="mb-6">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">总客户数</p>
|
||||
<h3 class="text-2xl font-bold">{{ totalCustomers }}</h3>
|
||||
</div>
|
||||
<el-icon class="text-primary text-xl"><UserFilled /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">总订单数</p>
|
||||
<h3 class="text-2xl font-bold">{{ totalOrders }}</h3>
|
||||
</div>
|
||||
<el-icon class="text-success text-xl"><ShoppingCart /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">总销售额</p>
|
||||
<h3 class="text-2xl font-bold">¥{{ totalSales.toFixed(2) }}</h3>
|
||||
</div>
|
||||
<el-icon class="text-warning text-xl"><Money /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">退换货率</p>
|
||||
<h3 class="text-2xl font-bold">{{ returnExchangeRate.toFixed(2) }}%</h3>
|
||||
</div>
|
||||
<el-icon class="text-danger text-xl"><RefreshRight /></el-icon>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="20" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>订单趋势分析</span>
|
||||
<el-select v-model="orderTrendRange" style="width: 100px;" placeholder="时间范围" size="small" @change="renderOrderTrendChart">
|
||||
<el-option label="近7天" value="7" />
|
||||
<el-option label="近30天" value="30" />
|
||||
<el-option label="近90天" value="90" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="orderTrendChart" class="chart-container" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>产品销售排行</span>
|
||||
<el-select v-model="productRankType" style="width: 100px;" placeholder="统计类型" size="small" @change="renderProductRankChart">
|
||||
<el-option label="销量" value="quantity" />
|
||||
<el-option label="销售额" value="amount" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="productRankChart" class="chart-container" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>客户跟进状态分布</span>
|
||||
</template>
|
||||
<div ref="followUpStatusChart" class="chart-container" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>订单状态分布</span>
|
||||
</template>
|
||||
<div ref="orderStatusChart" class="chart-container" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 订单明细表 -->
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span>最近订单列表</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
:data="paginatedOrders"
|
||||
border
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f5f7fa' }"
|
||||
>
|
||||
<el-table-column prop="orderCode" label="订单编号" />
|
||||
<el-table-column prop="customerName" label="客户名称" />
|
||||
<el-table-column prop="salesManager" label="销售经理" />
|
||||
<el-table-column
|
||||
prop="taxAmount"
|
||||
label="订单金额"
|
||||
align="right"
|
||||
:formatter="(row) => `¥${Number(row.taxAmount).toFixed(2)}`"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="orderStatus"
|
||||
label="订单状态"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<dict-tag :options="order_status" :value="scope.row.orderStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="filteredOrders.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
class="mt-4 flex justify-end"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
UserFilled, ShoppingCart, Money, RefreshRight
|
||||
} from '@element-plus/icons-vue'
|
||||
import { listOrder } from '@/api/wms/order'
|
||||
import { listOrderDetail } from '@/api/wms/orderDetail'
|
||||
import { listReturnExchange } from '@/api/oa/returnExchange'
|
||||
import { listCustomer } from '@/api/wms/customer'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { order_status } = proxy.useDict("order_status")
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(true)
|
||||
const customers = ref([])
|
||||
const orders = ref([])
|
||||
const orderDetails = ref([])
|
||||
const returnExchanges = ref([])
|
||||
|
||||
// 图表容器引用
|
||||
const orderTrendChart = ref(null)
|
||||
const productRankChart = ref(null)
|
||||
const followUpStatusChart = ref(null)
|
||||
const orderStatusChart = ref(null)
|
||||
|
||||
// 筛选与分页参数
|
||||
const orderTrendRange = ref('30') // 默认近30天
|
||||
const productRankType = ref('quantity') // 默认按销量统计
|
||||
const orderSearch = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const followUpStatusLabel = {
|
||||
0: '未跟进',
|
||||
1: '跟进中',
|
||||
2: '已跟进'
|
||||
}
|
||||
|
||||
// 指标计算(computed确保响应式)
|
||||
const totalCustomers = computed(() => customers.value.length)
|
||||
const totalOrders = computed(() => orders.value.length)
|
||||
const totalSales = computed(() => {
|
||||
return orders.value.reduce(
|
||||
(sum, order) => sum + Number(order.taxAmount || 0),
|
||||
0
|
||||
)
|
||||
})
|
||||
const returnExchangeRate = computed(() => {
|
||||
if (totalOrders.value === 0) return 0
|
||||
return (returnExchanges.value.length / totalOrders.value) * 100
|
||||
})
|
||||
|
||||
// 筛选后的订单(支持搜索)
|
||||
const filteredOrders = computed(() => {
|
||||
return orders.value.filter(order =>
|
||||
order.orderCode.includes(orderSearch.value)
|
||||
)
|
||||
})
|
||||
|
||||
// 分页后的订单
|
||||
const paginatedOrders = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredOrders.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
// 页面加载时初始化数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
// 并行请求所有数据(提升加载效率)
|
||||
const [customerRes, orderRes, orderDetailRes, returnExchangeRes] = await Promise.all([
|
||||
listCustomer({pageNum: 1, pageSize: 1000}),
|
||||
listOrder({pageNum: 1, pageSize: 1000}),
|
||||
listOrderDetail({pageNum: 1, pageSize: 1000}),
|
||||
listReturnExchange({pageNum: 1, pageSize: 1000})
|
||||
])
|
||||
// 赋值数据(假设接口返回格式为 { data: [...] })
|
||||
customers.value = customerRes.rows || []
|
||||
orders.value = orderRes.rows || []
|
||||
orderDetails.value = orderDetailRes.rows || []
|
||||
returnExchanges.value = returnExchangeRes.rows || []
|
||||
|
||||
// 初始化所有图表
|
||||
renderOrderTrendChart()
|
||||
renderProductRankChart()
|
||||
renderFollowUpStatusChart()
|
||||
renderOrderStatusChart()
|
||||
} catch (error) {
|
||||
console.error('销售数据加载失败:', error)
|
||||
// 可添加全局错误提示
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// ---------- 图表渲染函数 ----------
|
||||
|
||||
// 订单趋势图(双轴:订单数+销售额)
|
||||
const renderOrderTrendChart = () => {
|
||||
const chartDom = orderTrendChart.value
|
||||
if (!chartDom) return
|
||||
const chart = echarts.init(chartDom)
|
||||
|
||||
// 生成时间范围日期
|
||||
const days = Number(orderTrendRange.value)
|
||||
const dates = []
|
||||
const now = new Date()
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(now)
|
||||
date.setDate(now.getDate() - i)
|
||||
dates.push(date.toLocaleDateString())
|
||||
}
|
||||
|
||||
// 按日期统计订单数和销售额
|
||||
const orderCountMap = {}
|
||||
const salesMap = {}
|
||||
dates.forEach(date => {
|
||||
orderCountMap[date] = 0
|
||||
salesMap[date] = 0
|
||||
})
|
||||
orders.value.forEach(order => {
|
||||
// 假设订单有createTime字段,需与dates格式匹配
|
||||
const orderDate = new Date(order.createTime).toLocaleDateString()
|
||||
if (orderCountMap[orderDate] !== undefined) {
|
||||
orderCountMap[orderDate]++
|
||||
salesMap[orderDate] += Number(order.taxAmount || 0)
|
||||
}
|
||||
})
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' }
|
||||
},
|
||||
legend: { data: ['订单数', '销售额'] },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: { rotate: 30, fontSize: 12 }
|
||||
},
|
||||
yAxis: [
|
||||
{ type: 'value', name: '订单数', min: 0 },
|
||||
{
|
||||
type: 'value',
|
||||
name: '销售额',
|
||||
min: 0,
|
||||
axisLabel: { formatter: '¥{value}' }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '订单数',
|
||||
type: 'bar',
|
||||
data: dates.map(date => orderCountMap[date]),
|
||||
barWidth: '40%'
|
||||
},
|
||||
{
|
||||
name: '销售额',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: dates.map(date => salesMap[date]),
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// 产品销售排行图(饼图:支持销量/销售额切换)
|
||||
const renderProductRankChart = () => {
|
||||
const chartDom = productRankChart.value
|
||||
if (!chartDom) return
|
||||
const chart = echarts.init(chartDom)
|
||||
|
||||
// 统计产品数据
|
||||
const productStats = {}
|
||||
orderDetails.value.forEach(detail => {
|
||||
const productName = detail.productName
|
||||
if (!productStats[productName]) {
|
||||
productStats[productName] = {
|
||||
quantity: 0,
|
||||
amount: 0
|
||||
}
|
||||
}
|
||||
productStats[productName].quantity += Number(detail.quantity || 0)
|
||||
productStats[productName].amount +=
|
||||
Number(detail.quantity || 0) * Number(detail.taxPrice || 0)
|
||||
})
|
||||
|
||||
// 转换为图表数据(取Top10)
|
||||
const chartData = Object.entries(productStats)
|
||||
.map(([name, stats]) => ({
|
||||
name,
|
||||
value: productRankType.value === 'quantity'
|
||||
? stats.quantity
|
||||
: stats.amount
|
||||
}))
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 10)
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
name: productRankType.value === 'quantity' ? '销量' : '销售额',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
data: chartData,
|
||||
label: { formatter: '{b}: {c}' },
|
||||
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// 客户跟进状态分布图(饼图)
|
||||
const renderFollowUpStatusChart = () => {
|
||||
const chartDom = followUpStatusChart.value
|
||||
if (!chartDom) return
|
||||
const chart = echarts.init(chartDom)
|
||||
|
||||
// 统计跟进状态
|
||||
const statusStats = { 0: 0, 1: 0, 2: 0 }
|
||||
customers.value.forEach(customer => {
|
||||
const status = customer.followUpStatus
|
||||
if (statusStats[status] !== undefined) {
|
||||
statusStats[status]++
|
||||
}
|
||||
})
|
||||
|
||||
// 转换为图表数据
|
||||
const chartData = Object.entries(statusStats).map(([key, value]) => ({
|
||||
name: followUpStatusLabel[key],
|
||||
value
|
||||
}))
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
name: '客户跟进状态',
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
data: chartData,
|
||||
label: { formatter: '{b}: {c} ({d}%)' }
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// 订单状态分布图(饼图)
|
||||
const renderOrderStatusChart = () => {
|
||||
const chartDom = orderStatusChart.value
|
||||
if (!chartDom) return
|
||||
const chart = echarts.init(chartDom)
|
||||
|
||||
// 统计订单状态
|
||||
const statusStats = {}
|
||||
orders.value.forEach(order => {
|
||||
const status = order.orderStatus
|
||||
if (!statusStats[status]) {
|
||||
statusStats[status] = 0
|
||||
}
|
||||
statusStats[status]++
|
||||
})
|
||||
|
||||
// 转换为图表数据
|
||||
const chartData = Object.entries(statusStats).map(([key, value]) => ({
|
||||
name: key,
|
||||
value
|
||||
}))
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [
|
||||
{
|
||||
name: '订单状态',
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
data: chartData,
|
||||
label: { formatter: '{b}: {c} ({d}%)' }
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option)
|
||||
}
|
||||
|
||||
// ---------- 分页与操作函数 ----------
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
}
|
||||
const handleCurrentChange = (val) => {
|
||||
currentPage.value = val
|
||||
}
|
||||
|
||||
const viewOrderDetail = (row) => {
|
||||
console.log('查看订单详情:', row)
|
||||
// 可跳转至订单详情页,例:router.push(`/order/detail/${row.orderId}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sales-dashboard {
|
||||
padding: 16px;
|
||||
}
|
||||
.stat-card {
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user