Files
medical-mall/mall/pages/dashboard/OrderChart.uvue

346 lines
7.1 KiB
Plaintext

<template>
<view class="order-chart-container">
<view class="chart-header">
<view class="header-left">
<view class="title-icon">
<text class="iconfont">Icon</text>
</view>
<text class="chart-title">订单分析</text>
</view>
<view class="chart-controls">
<view
v-for="p in periods"
:key="p.value"
class="seg-btn"
:class="{ active: range === p.value }"
@click="changeRange(p.value)"
>
<text class="seg-btn-text">{{ p.label }}</text>
</view>
</view>
</view>
<view class="chart-body">
<!-- 轴标题 -->
<view class="axis-titles">
<text class="axis-title">金额 (元)</text>
<text class="axis-title">数量 (笔)</text>
</view>
<!-- 图表容器 -->
<view class="chart-view-wrapper">
<EChartsView
v-if="!loading && chartOption"
:option="chartOption"
class="echarts-view"
/>
<!-- Loading 状态 -->
<view v-if="loading" class="loading-state">
<text class="loading-text">加载中...</text>
</view>
<!-- 空状态 -->
<view v-if="!loading && stats.length === 0" class="empty-state">
<text class="empty-text">暂无数据</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, onMounted, watch } from 'vue'
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
import { OrderStat } from '@/types/orders.uts'
// --- Props ---
const props = defineProps<{
// 可以在这里扩展外部传入的配置
}>()
// --- State ---
const range = ref('30d')
const loading = ref(false)
const stats = ref<OrderStat[]>([])
const periods = [
{ label: '30天', value: '30d' },
{ label: '周', value: 'week' },
{ label: '月', value: 'month' },
{ label: '年', value: 'year' }
]
// --- ECharts Options ---
const chartOption = computed(() => {
if (stats.value.length === 0) return null
const dates = stats.value.map(item => item.date)
const amounts = stats.value.map(item => item.totalAmount)
const counts = stats.value.map(item => item.orderCount)
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: { color: '#999' }
}
},
grid: {
left: '3%',
right: '3%',
bottom: '3%',
top: '10%',
containLabel: true
},
legend: {
data: ['订单金额', '订单数'],
top: 0,
itemWidth: 12,
itemHeight: 12
},
xAxis: [
{
type: 'category',
data: dates,
axisPointer: { type: 'shadow' },
axisLine: { lineStyle: { color: '#f0f0f0' } },
axisLabel: { color: '#999', fontSize: 10 }
}
],
yAxis: [
{
type: 'value',
name: '', // 已经在外部显示标题了
splitLine: { lineStyle: { type: 'dashed', color: '#f0f0f0' } },
axisLabel: { color: '#999' }
},
{
type: 'value',
name: '',
splitLine: { show: false },
axisLabel: { color: '#999' }
}
],
series: [
{
name: '订单金额',
type: 'bar',
barWidth: '40%',
data: amounts,
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: '#36a3f7' },
{ offset: 1, color: '#a0d4fb' }
]
},
borderRadius: [4, 4, 0, 0]
}
},
{
name: '订单数',
type: 'line',
yAxisIndex: 1,
data: counts,
smooth: true,
showSymbol: true,
symbolSize: 6,
itemStyle: { color: '#2fc25b' },
lineStyle: { width: 2, color: '#2fc25b' },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(47, 194, 91, 0.2)' },
{ offset: 1, color: 'rgba(47, 194, 91, 0)' }
]
}
}
}
]
}
})
// --- Methods ---
/**
* 内部刷新逻辑,供外部或生命周期调用
*/
const refresh = async () => {
await fetchOrderStats(range.value)
}
/**
* 模拟接口请求
*/
const fetchOrderStats = (r: string) => {
loading.value = true
// 模拟网络延迟
setTimeout(() => {
// 模拟数据生成逻辑
const mockData: OrderStat[] = []
const count = r === 'week' ? 7 : (r === 'month' ? 30 : (r === 'year' ? 12 : 30))
for (let i = 0; i < count; i++) {
mockData.push({
date: r === 'year' ? `${i + 1}月` : `01-${(i + 4).toString().padStart(2, '0')}`,
totalAmount: Math.floor(Math.random() * 80000) + (i === 4 ? 60000 : 20000),
orderCount: Math.floor(Math.random() * 25) + (i % 3 === 0 ? 8 : 2)
})
}
stats.value = mockData
loading.value = false
// 如果是下拉刷新触发,通知原生停止
uni.stopPullDownRefresh()
}, 800)
}
const changeRange = (v: string) => {
range.value = v
}
// 暴露给父组件的主动刷新方案
defineExpose({
refresh
})
// --- Lifecycle & Watchers ---
onMounted(() => {
fetchOrderStats(range.value)
})
watch(range, (newVal) => {
fetchOrderStats(newVal)
})
</script>
<style scoped lang="scss">
.order-chart-container {
background: #ffffff;
border-radius: 4px;
border: 1px solid #f0f0f0;
padding: 16px;
margin-top: 20px;
}
.chart-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-left {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.title-icon {
width: 24px;
height: 24px;
background: #e6f7ff;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.chart-title {
font-size: 15px;
font-weight: 500;
color: #333;
}
/* 分段按钮样式 */
.chart-controls {
display: flex;
flex-direction: row;
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
}
.seg-btn {
height: 28px;
padding: 0 12px;
display: flex;
align-items: center;
justify-content: center;
border-left: 1px solid #d9d9d9;
background: #fff;
cursor: pointer;
}
.seg-btn:first-child {
border-left: 0;
}
.seg-btn.active {
background: #1890ff;
border-color: #1890ff;
}
.seg-btn-text {
font-size: 13px;
color: #333;
}
.seg-btn.active .seg-btn-text {
color: #fff;
}
/* 图表内容区域 */
.chart-body {
position: relative;
}
.axis-titles {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 4px;
margin-bottom: 8px;
}
.axis-title {
font-size: 12px;
color: #999;
}
.chart-view-wrapper {
width: 100%;
height: 350px;
display: flex;
align-items: center;
justify-content: center;
}
.echarts-view {
width: 100%;
height: 100%;
}
.loading-state, .empty-state {
display: flex;
align-items: center;
justify-content: center;
}
.loading-text, .empty-text {
color: #999;
font-size: 14px;
}
</style>