update dashboard page
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DashboardBreakdownItem } from '@/types/api'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
data: DashboardBreakdownItem[]
|
||||||
|
loading: boolean
|
||||||
|
dimension: 'company' | 'platform' | 'store'
|
||||||
|
dataType: string | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:dimension': ['company' | 'platform' | 'store']
|
||||||
|
'update:dataType': [string | undefined]
|
||||||
|
'update:dateRange': [[string, string] | null]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dimensionOptions = [
|
||||||
|
{ label: '公司', value: 'company' },
|
||||||
|
{ label: '平台', value: 'platform' },
|
||||||
|
{ label: '店铺', value: 'store' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const dataTypeOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '订单', value: 'order' },
|
||||||
|
{ label: '产品', value: 'product' },
|
||||||
|
{ label: '退款', value: 'refund' },
|
||||||
|
{ label: '库存', value: 'inventory' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||||
|
{ title: '成功数', dataIndex: 'success', key: 'success', sorter: (a: DashboardBreakdownItem, b: DashboardBreakdownItem) => a.success - b.success },
|
||||||
|
{ title: '失败数', dataIndex: 'failed', key: 'failed', sorter: (a: DashboardBreakdownItem, b: DashboardBreakdownItem) => a.failed - b.failed },
|
||||||
|
{ title: '合计', key: 'total', sorter: (a: DashboardBreakdownItem, b: DashboardBreakdownItem) => (a.success + a.failed) - (b.success + b.failed) },
|
||||||
|
{ title: '失败率', key: 'failRate', sorter: (a: DashboardBreakdownItem, b: DashboardBreakdownItem) => failRate(a) - failRate(b) },
|
||||||
|
])
|
||||||
|
|
||||||
|
function failRate(row: DashboardBreakdownItem): number {
|
||||||
|
const total = row.success + row.failed
|
||||||
|
return total === 0 ? 0 : row.failed / total
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent(row: DashboardBreakdownItem): string {
|
||||||
|
return (failRate(row) * 100).toFixed(1) + '%'
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDimensionChange(e: unknown) {
|
||||||
|
const val = (e as { target: { value?: string } }).target.value as 'company' | 'platform' | 'store'
|
||||||
|
emit('update:dimension', val)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDataTypeChange(val: unknown) {
|
||||||
|
emit('update:dataType', (val as string) || undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDateRangeChange(_dates: unknown, dateStrings: [string, string]) {
|
||||||
|
if (dateStrings[0] && dateStrings[1]) {
|
||||||
|
emit('update:dateRange', dateStrings)
|
||||||
|
} else {
|
||||||
|
emit('update:dateRange', null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-card title="分维度统计" class="mb-4">
|
||||||
|
<template #extra>
|
||||||
|
<a-space>
|
||||||
|
<a-radio-group
|
||||||
|
:value="dimension"
|
||||||
|
size="small"
|
||||||
|
@change="handleDimensionChange"
|
||||||
|
>
|
||||||
|
<a-radio-button v-for="opt in dimensionOptions" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-range-picker
|
||||||
|
size="small"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
@change="handleDateRangeChange"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
:value="dataType ?? ''"
|
||||||
|
:options="dataTypeOptions"
|
||||||
|
style="width: 100px"
|
||||||
|
size="small"
|
||||||
|
@change="handleDataTypeChange"
|
||||||
|
/>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="false"
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'failed'">
|
||||||
|
<span :class="{ 'text-red-500 font-bold': (record as DashboardBreakdownItem).failed > 0 }">
|
||||||
|
{{ (record as DashboardBreakdownItem).failed }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'total'">
|
||||||
|
{{ (record as DashboardBreakdownItem).success + (record as DashboardBreakdownItem).failed }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'failRate'">
|
||||||
|
<span :class="{ 'text-red-500': failRate(record as DashboardBreakdownItem) > 0 }">
|
||||||
|
{{ formatPercent(record as DashboardBreakdownItem) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template #emptyText>
|
||||||
|
<a-empty description="暂无统计数据" />
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
ShoppingCartOutlined,
|
||||||
|
ShoppingOutlined,
|
||||||
|
DollarOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import type { DataTypeCount } from '@/types/api'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
byType: DataTypeCount[]
|
||||||
|
loading: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const iconMap: Record<string, ReturnType<typeof ShoppingCartOutlined>> = {
|
||||||
|
order: ShoppingCartOutlined,
|
||||||
|
product: ShoppingOutlined,
|
||||||
|
refund: DollarOutlined,
|
||||||
|
inventory: DatabaseOutlined,
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelMap: Record<string, string> = {
|
||||||
|
order: '订单',
|
||||||
|
product: '产品',
|
||||||
|
refund: '退款',
|
||||||
|
inventory: '库存',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-row :gutter="16" class="mb-4">
|
||||||
|
<a-col :span="6" v-for="item in byType" :key="item.data_type">
|
||||||
|
<a-card :bordered="false">
|
||||||
|
<a-skeleton :loading="loading" active :paragraph="{ rows: 2 }">
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<component :is="iconMap[item.data_type] ?? DatabaseOutlined" class="text-lg" />
|
||||||
|
<span class="text-base font-medium">{{ labelMap[item.data_type] ?? item.data_type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<a-statistic
|
||||||
|
title="成功"
|
||||||
|
:value="item.success"
|
||||||
|
:value-style="{ color: '#3f8600' }"
|
||||||
|
/>
|
||||||
|
<a-statistic
|
||||||
|
title="失败"
|
||||||
|
:value="item.failed"
|
||||||
|
:value-style="item.failed > 0 ? { color: '#cf1322' } : {}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-skeleton>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
|
||||||
|
import type { DashboardOverview } from '@/types/api'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
overview: DashboardOverview | null
|
||||||
|
loading: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const periods = [
|
||||||
|
{ key: 'today' as const, label: '今日' },
|
||||||
|
{ key: 'this_week' as const, label: '本周' },
|
||||||
|
{ key: 'this_month' as const, label: '本月' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-row :gutter="16" class="mb-4">
|
||||||
|
<a-col :span="8" v-for="p in periods" :key="p.key">
|
||||||
|
<a-card :bordered="false">
|
||||||
|
<a-skeleton :loading="loading" active :paragraph="{ rows: 2 }">
|
||||||
|
<h3 class="text-base font-medium mb-3">{{ p.label }}</h3>
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<a-statistic
|
||||||
|
title="成功"
|
||||||
|
:value="overview?.[p.key]?.success ?? 0"
|
||||||
|
:value-style="{ color: '#3f8600' }"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<CheckCircleOutlined />
|
||||||
|
</template>
|
||||||
|
</a-statistic>
|
||||||
|
<a-statistic
|
||||||
|
title="失败"
|
||||||
|
:value="overview?.[p.key]?.failed ?? 0"
|
||||||
|
:value-style="(overview?.[p.key]?.failed ?? 0) > 0 ? { color: '#cf1322' } : {}"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<CloseCircleOutlined />
|
||||||
|
</template>
|
||||||
|
</a-statistic>
|
||||||
|
</div>
|
||||||
|
</a-skeleton>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Line } from '@antv/g2plot'
|
||||||
|
import type { DashboardTrendPoint } from '@/types/api'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: DashboardTrendPoint[]
|
||||||
|
loading: boolean
|
||||||
|
groupBy: 'day' | 'week' | 'month'
|
||||||
|
dataType: string | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:groupBy': ['day' | 'week' | 'month']
|
||||||
|
'update:dataType': [string | undefined]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const chartRef = ref<HTMLDivElement>()
|
||||||
|
let chart: Line | null = null
|
||||||
|
|
||||||
|
const groupByOptions = [
|
||||||
|
{ label: '日', value: 'day' },
|
||||||
|
{ label: '周', value: 'week' },
|
||||||
|
{ label: '月', value: 'month' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const dataTypeOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '订单', value: 'order' },
|
||||||
|
{ label: '产品', value: 'product' },
|
||||||
|
{ label: '退款', value: 'refund' },
|
||||||
|
{ label: '库存', value: 'inventory' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const flatData = computed(() => {
|
||||||
|
const result: { date: string; value: number; category: string }[] = []
|
||||||
|
for (const point of props.data) {
|
||||||
|
result.push({ date: point.date, value: point.success, category: '成功' })
|
||||||
|
result.push({ date: point.date, value: point.failed, category: '失败' })
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
function createChart() {
|
||||||
|
if (!chartRef.value) return
|
||||||
|
chart = new Line(chartRef.value, {
|
||||||
|
data: flatData.value,
|
||||||
|
xField: 'date',
|
||||||
|
yField: 'value',
|
||||||
|
seriesField: 'category',
|
||||||
|
color: ['#52c41a', '#ff4d4f'],
|
||||||
|
smooth: true,
|
||||||
|
legend: { position: 'top' },
|
||||||
|
xAxis: { label: { autoRotate: true } },
|
||||||
|
yAxis: { label: { formatter: (v: string) => v } },
|
||||||
|
animation: false,
|
||||||
|
})
|
||||||
|
chart.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGroupByChange(e: unknown) {
|
||||||
|
const val = (e as { target: { value?: string } }).target.value as 'day' | 'week' | 'month'
|
||||||
|
emit('update:groupBy', val)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDataTypeChange(val: unknown) {
|
||||||
|
emit('update:dataType', (val as string) || undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(flatData, () => {
|
||||||
|
if (chart) {
|
||||||
|
chart.changeData(flatData.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.data.length > 0) {
|
||||||
|
createChart()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal.length > 0 && !chart) {
|
||||||
|
nextTick(() => createChart())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.destroy()
|
||||||
|
chart = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-card title="数据趋势" class="mb-4">
|
||||||
|
<template #extra>
|
||||||
|
<a-space>
|
||||||
|
<a-radio-group
|
||||||
|
:value="groupBy"
|
||||||
|
size="small"
|
||||||
|
@change="handleGroupByChange"
|
||||||
|
>
|
||||||
|
<a-radio-button v-for="opt in groupByOptions" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-select
|
||||||
|
:value="dataType ?? ''"
|
||||||
|
:options="dataTypeOptions"
|
||||||
|
style="width: 100px"
|
||||||
|
size="small"
|
||||||
|
@change="handleDataTypeChange"
|
||||||
|
/>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div ref="chartRef" style="height: 350px" />
|
||||||
|
<a-empty v-if="!loading && data.length === 0" description="暂无趋势数据" />
|
||||||
|
</a-spin>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user