更改统计

This commit is contained in:
扫地僧 2026-05-05 18:31:24 +08:00
parent cf7c94c7e7
commit 6170d5a619
2 changed files with 144 additions and 7 deletions

View File

@ -11,3 +11,11 @@ export function getAccountPoolDailyExtract(params) {
params,
});
}
/** 号池账号总数 / 已售卖(按 Cursor、Kiro、Windsurf */
export function getAccountPoolInventoryTotals() {
return request({
url: '/platform/home/accountPoolInventoryTotals',
method: 'get',
});
}

View File

@ -12,7 +12,7 @@
<div class="value">{{ item.value.toLocaleString() }}</div>
<div class="trend" :class="item.isUp ? 'up' : 'down'">
{{ item.isUp ? '↑' : '↓' }} {{ item.percentage }}%
<span>较上月</span>
<span>{{ item.trendLabel ?? '较上月' }}</span>
</div>
</div>
</div>
@ -20,13 +20,21 @@
</el-col>
</el-row>
<el-row :gutter="20" class="charts-row">
<el-col :xs="24" :sm="24" :md="16">
<el-row :gutter="20" class="charts-row charts-row--token-bar">
<el-col :xs="24" :sm="24" :md="12">
<el-card shadow="hover" header="Token售卖统计">
<div ref="lineChartRef" class="chart-box"></div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="8">
<el-col :xs="24" :sm="24" :md="12">
<el-card shadow="hover" header="号池账号统计">
<div ref="barChartRef" class="chart-box"></div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="charts-row">
<el-col :span="24">
<el-card shadow="hover" header="用户等级分布">
<div ref="pieChartRef" class="chart-box"></div>
</el-card>
@ -39,8 +47,8 @@
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
import * as echarts from 'echarts';
import { ElMessage } from 'element-plus';
import { User, Pointer, Connection, Histogram } from '@element-plus/icons-vue';
import { getAccountPoolDailyExtract } from '@/api/home';
import { User, Pointer, Connection, ShoppingCart } from '@element-plus/icons-vue';
import { getAccountPoolDailyExtract, getAccountPoolInventoryTotals } from '@/api/home';
// --- ---
interface SummaryItem {
@ -50,21 +58,49 @@ interface SummaryItem {
color: string;
percentage: number;
isUp: boolean;
/** 趋势说明,默认「较上月」 */
trendLabel?: string;
}
// --- ---
const lineChartRef = ref<HTMLElement | null>(null);
const pieChartRef = ref<HTMLElement | null>(null);
const barChartRef = ref<HTMLElement | null>(null);
const lineChartInstance = shallowRef<echarts.ECharts | null>(null);
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
const barChartInstance = shallowRef<echarts.ECharts | null>(null);
const summaryData = ref<SummaryItem[]>([
{ title: '总用户数', value: 12840, icon: User, color: '#3973FF', percentage: 12, isUp: true },
{ title: '今日新增', value: 156, icon: Pointer, color: '#67C23A', percentage: 5, isUp: true },
{ title: '活跃用户', value: 3420, icon: Connection, color: '#E6A23C', percentage: 2, isUp: false },
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
{
title: '今日售卖',
value: 0,
icon: ShoppingCart,
color: '#F56C6C',
percentage: 0,
isUp: true,
trendLabel: '较昨日',
},
]);
/** 三条产品线当日销量之和,及相对昨日的涨跌比例(用于首页第四张卡片) */
function todaySalesVsYesterday(cursor: number[], kiro: number[], windsurf: number[]) {
const n = Math.min(cursor.length, kiro.length, windsurf.length);
if (n < 1) return { today: 0, pct: 0, isUp: true };
const iToday = n - 1;
const today = cursor[iToday] + kiro[iToday] + windsurf[iToday];
if (n < 2) return { today, pct: 0, isUp: true };
const iY = n - 2;
const yesterday = cursor[iY] + kiro[iY] + windsurf[iY];
if (yesterday > 0) {
const raw = Math.round(((today - yesterday) / yesterday) * 100);
return { today, pct: Math.abs(raw), isUp: today >= yesterday };
}
return { today, pct: today > 0 ? 100 : 0, isUp: true };
}
function buildSalesLineOption(
days: string[],
cursor: number[],
@ -126,6 +162,71 @@ function buildSalesLineOption(
};
}
function buildInventoryBarOption(labels: string[], totalData: number[], soldData: number[]) {
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
},
legend: {
data: ['账号总数', '已售卖'],
top: 8,
},
grid: { left: '3%', right: '4%', bottom: '3%', top: 48, containLabel: true },
xAxis: {
type: 'category',
data: labels,
axisTick: { alignWithLabel: true },
},
yAxis: {
type: 'value',
minInterval: 1,
},
series: [
{
name: '账号总数',
type: 'bar',
data: totalData,
barMaxWidth: 56,
itemStyle: { color: '#409EFF', borderRadius: [4, 4, 0, 0] },
},
{
name: '已售卖',
type: 'bar',
data: soldData,
barMaxWidth: 56,
itemStyle: { color: '#67C23A', borderRadius: [4, 4, 0, 0] },
},
],
};
}
async function loadAccountPoolInventoryTotals() {
await nextTick();
if (!barChartRef.value) return;
if (!barChartInstance.value) {
barChartInstance.value = echarts.init(barChartRef.value);
}
try {
const res = await getAccountPoolInventoryTotals();
if (res?.code !== 200) {
ElMessage.error((res as { msg?: string })?.msg || '加载号池统计失败');
return;
}
const data = (res as { data?: { modules?: Array<{ label: string; total: number; sold: number }> } }).data;
const mods = Array.isArray(data?.modules) ? data!.modules : [];
const labels = mods.map((m) => m.label || '');
const totalData = mods.map((m) => Number(m.total) || 0);
const soldData = mods.map((m) => Number(m.sold) || 0);
barChartInstance.value.setOption(
buildInventoryBarOption(labels.length ? labels : ['Cursor', 'Kiro', 'Windsurf'], totalData, soldData),
{ notMerge: true },
);
} catch {
ElMessage.error('加载号池统计失败');
}
}
async function loadAccountPoolDailyExtract() {
await nextTick();
if (!lineChartRef.value) return;
@ -146,6 +247,17 @@ async function loadAccountPoolDailyExtract() {
lineChartInstance.value.setOption(buildSalesLineOption(days, cursor, kiro, windsurf), {
notMerge: true,
});
const sale = todaySalesVsYesterday(cursor, kiro, windsurf);
const row = summaryData.value[3];
if (row) {
summaryData.value[3] = {
...row,
value: sale.today,
percentage: sale.pct,
isUp: sale.isUp,
};
}
} catch {
ElMessage.error('加载售卖数据失败');
}
@ -190,17 +302,29 @@ const initLineChartShell = () => {
);
};
const initBarChartShell = () => {
if (!barChartRef.value) return;
barChartInstance.value = echarts.init(barChartRef.value);
barChartInstance.value.setOption(
buildInventoryBarOption(['Cursor', 'Kiro', 'Windsurf'], [0, 0, 0], [0, 0, 0]),
{ notMerge: true },
);
};
// --- ---
const handleResize = () => {
lineChartInstance.value?.resize();
pieChartInstance.value?.resize();
barChartInstance.value?.resize();
};
onMounted(async () => {
await nextTick();
initLineChartShell();
initBarChartShell();
initPieChart();
void loadAccountPoolDailyExtract();
void loadAccountPoolInventoryTotals();
window.addEventListener('resize', handleResize);
});
@ -210,6 +334,8 @@ onUnmounted(() => {
lineChartInstance.value = null;
pieChartInstance.value?.dispose();
pieChartInstance.value = null;
barChartInstance.value?.dispose();
barChartInstance.value = null;
});
</script>
@ -275,6 +401,9 @@ onUnmounted(() => {
height: 350px;
width: 100%;
}
&--token-bar .chart-box {
height: 340px;
}
}
}