feat: reoede web to tailwindcss
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<Card class="w-full">
|
||||
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle class="text-sm font-medium text-muted-foreground">
|
||||
{{ title }}
|
||||
</CardTitle>
|
||||
<component :is="icon" class="h-5 w-5 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="text-2xl font-bold">{{ value }}</div>
|
||||
<p class="text-xs text-muted-foreground">{{ description }}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import type { Component } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
value: string | number
|
||||
description?: string
|
||||
icon?: Component
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'CardNum'
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { DefineComponent } from 'vue';
|
||||
|
||||
declare const CardNum: DefineComponent<{
|
||||
title: string;
|
||||
value: string | number;
|
||||
description?: string;
|
||||
icon?: any;
|
||||
}>;
|
||||
|
||||
export default CardNum;
|
||||
@@ -0,0 +1,412 @@
|
||||
<script setup lang="ts">
|
||||
import StatCard from '@/components/pages/dashboard/CardNum.vue'
|
||||
import { DatabaseIcon, BarChartIcon, SendIcon, CheckCircleIcon, XCircleIcon } from 'lucide-vue-next'
|
||||
// import { LineChart } from "@/components/ui/chart-line"
|
||||
import { onMounted, reactive } from 'vue';
|
||||
import { request } from '@/api/api';
|
||||
// import VueApexCharts from 'vue3-apexcharts'
|
||||
import ApexCharts from 'apexcharts'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
interface SendData {
|
||||
day: string
|
||||
day_failed_num: number
|
||||
day_succ_num: number
|
||||
num: number
|
||||
succ_num: number
|
||||
}
|
||||
interface CateData {
|
||||
count_num: number
|
||||
way_name: string
|
||||
}
|
||||
|
||||
let state = reactive({
|
||||
data: {
|
||||
message_total_num: 0,
|
||||
hosted_message_total_num: 0,
|
||||
today_total_num: 0,
|
||||
today_succ_num: 0,
|
||||
today_failed_num: 0,
|
||||
way_cate_data: [] as CateData[],
|
||||
latest_send_data: [] as SendData[],
|
||||
}
|
||||
});
|
||||
|
||||
const getStatisticData = async () => {
|
||||
try {
|
||||
const rsp = await request.get('/statistic');
|
||||
if (rsp && rsp.data && rsp.data.code == 200) {
|
||||
state.data = rsp.data.data;
|
||||
// 数据加载完成后重新渲染图表
|
||||
setTimeout(() => {
|
||||
renderLineChart();
|
||||
renderPieChart();
|
||||
}, 100);
|
||||
} else {
|
||||
console.error('Failed to load statistics:', rsp?.data?.msg || 'Unknown error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading statistics:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const renderLineChart = () => {
|
||||
const options = {
|
||||
series: [
|
||||
{
|
||||
name: '发送总数',
|
||||
data: state.data.latest_send_data.length > 0
|
||||
? state.data.latest_send_data.map(item => item.num || 0)
|
||||
: []
|
||||
},
|
||||
{
|
||||
name: '发送成功数',
|
||||
data: state.data.latest_send_data.length > 0
|
||||
? state.data.latest_send_data.map(item => item.day_succ_num || 0)
|
||||
: []
|
||||
},
|
||||
{
|
||||
name: '发送失败数',
|
||||
data: state.data.latest_send_data.length > 0
|
||||
? state.data.latest_send_data.map(item => item.day_failed_num || 0)
|
||||
: []
|
||||
},
|
||||
],
|
||||
chart: {
|
||||
type: 'line',
|
||||
height: 350,
|
||||
toolbar: { show: false },
|
||||
background: 'transparent',
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'easeinout',
|
||||
speed: 800,
|
||||
animateGradually: {
|
||||
enabled: true,
|
||||
delay: 150
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 350
|
||||
}
|
||||
},
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
color: '#000',
|
||||
top: 18,
|
||||
left: 7,
|
||||
blur: 10,
|
||||
opacity: 0.2
|
||||
}
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 3,
|
||||
lineCap: 'round'
|
||||
},
|
||||
markers: {
|
||||
size: 6,
|
||||
colors: ['#3b82f6', '#10b981', '#ef4444'],
|
||||
strokeColors: '#fff',
|
||||
strokeWidth: 2,
|
||||
hover: {
|
||||
size: 8,
|
||||
sizeOffset: 3
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
categories: state.data.latest_send_data.length > 0
|
||||
? state.data.latest_send_data.map(item => item.day)
|
||||
: [],
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
},
|
||||
labels: {
|
||||
style: {
|
||||
colors: '#64748b',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
style: {
|
||||
colors: '#64748b',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
},
|
||||
formatter: function (val: number) {
|
||||
return val + ' 条'
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: ['#3b82f6', '#10b981', '#ef4444'], // 蓝色表示总数,绿色表示成功,红色表示失败
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shade: 'light',
|
||||
type: 'vertical',
|
||||
shadeIntensity: 0.5,
|
||||
gradientToColors: ['#60a5fa', '#34d399', '#f87171'],
|
||||
inverseColors: false,
|
||||
opacityFrom: 0.8,
|
||||
opacityTo: 0.1,
|
||||
stops: [0, 100]
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
borderColor: '#e2e8f0',
|
||||
strokeDashArray: 3,
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
padding: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
horizontalAlign: 'right',
|
||||
floating: true,
|
||||
offsetY: -25,
|
||||
offsetX: -5,
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
markers: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
radius: 4
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
shared: true,
|
||||
intersect: false,
|
||||
theme: 'light',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
},
|
||||
x: {
|
||||
show: true,
|
||||
format: 'MM/dd'
|
||||
},
|
||||
y: {
|
||||
formatter: function (val: number, { seriesIndex: _seriesIndex }: { seriesIndex: number }) {
|
||||
return val + ' 条'
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
show: true
|
||||
},
|
||||
custom: function ({ series, seriesIndex: _seriesIndex, dataPointIndex, w }: { series: number[][], seriesIndex: number, dataPointIndex: number, w: any }) {
|
||||
const successCount = series[1][dataPointIndex];
|
||||
const failedCount = series[2][dataPointIndex];
|
||||
const total = successCount + failedCount;
|
||||
const successRate = total > 0 ? ((successCount / total) * 100).toFixed(1) : '0.0';
|
||||
|
||||
return `
|
||||
<div class="bg-white p-3 rounded-lg shadow-lg border">
|
||||
<div class="font-medium text-gray-900 mb-2">${w.globals.categoryLabels[dataPointIndex]}</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
<span class="text-sm text-gray-600">成功:</span>
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900">${successCount} 条</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="flex items-center">
|
||||
<span class="w-2 h-2 bg-red-500 rounded-full mr-2"></span>
|
||||
<span class="text-sm text-gray-600">失败:</span>
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900">${failedCount} 条</span>
|
||||
</div>
|
||||
<div class="border-t pt-1 mt-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-gray-600">成功率:</span>
|
||||
<span class="text-sm font-medium text-green-600">${successRate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
responsive: [{
|
||||
breakpoint: 768,
|
||||
options: {
|
||||
chart: {
|
||||
height: 300
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
offsetY: 0
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
const chart = new ApexCharts(document.querySelector("#sales-chart"), options)
|
||||
console.log(1111);
|
||||
|
||||
chart.render();
|
||||
console.log(chart);
|
||||
|
||||
}
|
||||
|
||||
const renderPieChart = () => {
|
||||
const options = {
|
||||
series: state.data.way_cate_data.length > 0
|
||||
? state.data.way_cate_data.map(item => item.count_num)
|
||||
: [],
|
||||
chart: {
|
||||
type: 'pie',
|
||||
height: 350,
|
||||
toolbar: { show: false },
|
||||
background: 'transparent',
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'easeinout',
|
||||
speed: 800,
|
||||
animateGradually: {
|
||||
enabled: true,
|
||||
delay: 150
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 350
|
||||
}
|
||||
}
|
||||
},
|
||||
labels: state.data.way_cate_data.length > 0
|
||||
? state.data.way_cate_data.map(item => item.way_name)
|
||||
: [],
|
||||
colors: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'],
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
markers: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
radius: 4
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
size: '0%'
|
||||
},
|
||||
expandOnClick: true
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter: function (val: number) {
|
||||
return val.toFixed(1) + '%'
|
||||
},
|
||||
style: {
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme: 'light',
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
},
|
||||
y: {
|
||||
formatter: function (val: number) {
|
||||
return val + ' 条'
|
||||
}
|
||||
}
|
||||
},
|
||||
responsive: [{
|
||||
breakpoint: 768,
|
||||
options: {
|
||||
chart: {
|
||||
height: 300
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
const pieChart = new ApexCharts(document.querySelector("#pie-chart"), options)
|
||||
pieChart.render();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStatisticData();
|
||||
// renderLineChart();
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-[90%] mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
<StatCard title="推送记录留存数" :value="state.data.message_total_num" description="" :icon="DatabaseIcon" />
|
||||
<StatCard title="托管消息数" :value="state.data.hosted_message_total_num" description="" :icon="BarChartIcon" />
|
||||
<StatCard title="今日发送总数" :value="state.data.today_total_num" description="" :icon="SendIcon" />
|
||||
<StatCard title="今日成功数" :value="state.data.today_succ_num" description="" :icon="CheckCircleIcon" />
|
||||
<StatCard title="今日失败数" :value="state.data.today_failed_num" description="" :icon="XCircleIcon" />
|
||||
</div>
|
||||
|
||||
<!-- 折线图 -->
|
||||
<!-- <LineChart
|
||||
:data="data"
|
||||
index="year"
|
||||
:categories="['Export Growth Rate', 'Import Growth Rate']"
|
||||
:y-formatter="(tick, i) => {
|
||||
return typeof tick === 'number'
|
||||
? `$ ${new Intl.NumberFormat('us').format(tick).toString()}`
|
||||
: ''
|
||||
}"
|
||||
/> -->
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="w-[90%] mx-auto mt-8 grid grid-cols-1 lg:grid-cols-10 gap-6">
|
||||
<!-- 折线图 -->
|
||||
<Card class="w-full lg:col-span-7">
|
||||
<CardHeader>
|
||||
<CardTitle>消息发送趋势</CardTitle>
|
||||
<CardDescription>最近30天的发送情况统计</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div id="sales-chart" class="w-full h-[350px]"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 饼图 -->
|
||||
<Card class="w-full lg:col-span-3">
|
||||
<CardHeader>
|
||||
<CardTitle>发送渠道分布</CardTitle>
|
||||
<CardDescription>各发送渠道的使用情况统计</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div id="pie-chart" class="w-full h-[350px]"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user