销售看板和供货看板

This commit is contained in:
砂糖
2025-09-04 14:57:05 +08:00
parent 3aac9872ed
commit 1e0387acbb
6 changed files with 1227 additions and 5 deletions

View 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>