修改页面结构
This commit is contained in:
345
pages/dashboard/OrderChart.uvue
Normal file
345
pages/dashboard/OrderChart.uvue
Normal file
@@ -0,0 +1,345 @@
|
||||
<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>
|
||||
207
pages/dashboard/PurchaseUserPie.uvue
Normal file
207
pages/dashboard/PurchaseUserPie.uvue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<view class="chart-container">
|
||||
<view class="chart-header">
|
||||
<view class="header-left">
|
||||
<view class="title-icon">
|
||||
<text class="iconfont">O</text>
|
||||
</view>
|
||||
<text class="chart-title">购买用户分析</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="chart-body">
|
||||
<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 && chartData.length === 0" class="empty-state">
|
||||
<text class="empty-text">无相关消费记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { PurchaseUserStat } from '@/types/charts.uts'
|
||||
|
||||
/**
|
||||
* PurchaseUserPie 购买用户统计饼图
|
||||
* 展示不同消费特征的用户分布情况
|
||||
*/
|
||||
|
||||
// --- 状态定义 ---
|
||||
const loading = ref(false)
|
||||
const chartData = ref<PurchaseUserStat[]>([])
|
||||
|
||||
// --- ECharts 配置计算属性 ---
|
||||
const chartOption = computed(() : any => {
|
||||
if (chartData.value.length === 0) return null
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
bottom: '5%',
|
||||
left: 'center',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
// 视觉色彩方案
|
||||
color: ['#1890ff', '#36cfc9', '#ffc53d', '#ff4d4f'],
|
||||
series: [
|
||||
{
|
||||
name: '用户分布',
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'], // 采用环形图设计,更具现代感
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 6,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: '{b}\n{d}%',
|
||||
fontSize: 11,
|
||||
color: '#888'
|
||||
},
|
||||
emphasis: {
|
||||
scale: true,
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 12,
|
||||
length2: 10,
|
||||
smooth: true
|
||||
},
|
||||
data: chartData.value.map(item => {
|
||||
return {
|
||||
name: item.label,
|
||||
value: item.value
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// --- 数据获取逻辑 ---
|
||||
|
||||
/**
|
||||
* 模拟获取统计数据
|
||||
*/
|
||||
const fetchData = () => {
|
||||
loading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
chartData.value = [
|
||||
{ label: '未消费用户', value: 342 } as PurchaseUserStat,
|
||||
{ label: '消费一次', value: 156 } as PurchaseUserStat,
|
||||
{ label: '留存客户', value: 218 } as PurchaseUserStat,
|
||||
{ label: '回流客户', value: 84 } as PurchaseUserStat
|
||||
]
|
||||
loading.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
refresh: fetchData
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-container {
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.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: #fff7e6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-view-wrapper {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
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>
|
||||
370
pages/dashboard/UserTrendChart.uvue
Normal file
370
pages/dashboard/UserTrendChart.uvue
Normal file
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<view class="chart-container">
|
||||
<!-- 图表标题与切换 -->
|
||||
<view class="chart-header">
|
||||
<view class="header-left">
|
||||
<view class="title-icon">
|
||||
<text class="iconfont">≡</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>
|
||||
</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 && trendData.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 { UserTrend } from '@/types/charts.uts'
|
||||
|
||||
/**
|
||||
* UserTrendChart 用户趋势图
|
||||
* 展示日粒度用户增长,采用平滑曲线面积图
|
||||
*/
|
||||
|
||||
// --- 状态定义 ---
|
||||
const loading = ref(false)
|
||||
const range = ref('7d')
|
||||
const trendData = ref<UserTrend[]>([])
|
||||
|
||||
const periods = [
|
||||
{ label: '7天', value: '7d' },
|
||||
{ label: '30天', value: '30d' }
|
||||
]
|
||||
|
||||
// --- ECharts 配置计算属性 ---
|
||||
const chartOption = computed(() : any => {
|
||||
if (trendData.value.length === 0) return null
|
||||
|
||||
const xData = trendData.value.map(item => item.date)
|
||||
const yData = trendData.value.map(item => item.count)
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
lineStyle: { color: '#1890ff', type: 'dashed' }
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '5%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xData,
|
||||
axisLine: { lineStyle: { color: '#f0f0f0' } },
|
||||
axisLabel: {
|
||||
color: '#999',
|
||||
fontSize: 10,
|
||||
rotate: xData.length > 7 ? 45 : 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#f5f5f5' } },
|
||||
axisLabel: { color: '#999', fontSize: 10 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
data: yData,
|
||||
smooth: true, // 平滑折线
|
||||
showSymbol: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: {
|
||||
color: '#1890ff',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: '#1890ff'
|
||||
},
|
||||
// 区域填充效果
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(24, 144, 255, 0.25)' },
|
||||
{ offset: 1, color: 'rgba(24, 144, 255, 0)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// --- 方法定义 ---
|
||||
|
||||
const changeRange = (v: string) => {
|
||||
range.value = v
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟接口请求
|
||||
* 使用 useRequest 模式的封装
|
||||
*/
|
||||
const fetchData = (r: string) => {
|
||||
loading.value = true
|
||||
|
||||
// 模拟业务逻辑
|
||||
setTimeout(() => {
|
||||
const mockData: UserTrend[] = []
|
||||
const count = r === '7d' ? 7 : 30
|
||||
const now = new Date()
|
||||
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
const d = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
||||
mockData.push({
|
||||
date: `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`,
|
||||
count: Math.floor(Math.random() * 80) + (i % 7 === 0 ? 50 : 20)
|
||||
} as UserTrend)
|
||||
}
|
||||
|
||||
trendData.value = mockData
|
||||
loading.value = false
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// 暴露方法给外部调用
|
||||
defineExpose({
|
||||
refresh: () => fetchData(range.value)
|
||||
})
|
||||
|
||||
// --- 生命周期与监听 ---
|
||||
onMounted(() => {
|
||||
fetchData(range.value)
|
||||
})
|
||||
|
||||
watch(range, (newVal) => {
|
||||
fetchData(newVal)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-container {
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.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: 26px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.seg-btn:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.seg-btn.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.seg-btn-text {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.seg-btn.active .seg-btn-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.axis-titles {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.axis-title {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chart-view-wrapper {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
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>
|
||||
|
||||
// --- Data Fetching ---
|
||||
const fetchTrendData = () => {
|
||||
loading.value = true
|
||||
// 模拟 API 请求: GET /api/user/trend
|
||||
setTimeout(() => {
|
||||
const mock: UserTrend[] = []
|
||||
const now = new Date()
|
||||
for (let i = 14; i >= 0; i--) {
|
||||
const d = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
||||
mock.push({
|
||||
date: `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`,
|
||||
count: Math.floor(Math.random() * 50) + 20 + (i === 5 ? 100 : 0)
|
||||
})
|
||||
}
|
||||
trendData.value = mock
|
||||
loading.value = false
|
||||
}, 600)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTrendData()
|
||||
})
|
||||
|
||||
// 暴露刷新接口
|
||||
defineExpose({
|
||||
refresh: fetchTrendData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.echarts-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-overlay, .empty-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
.loading-text, .empty-text {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user