更改统计
This commit is contained in:
parent
cf7c94c7e7
commit
6170d5a619
@ -11,3 +11,11 @@ export function getAccountPoolDailyExtract(params) {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 号池账号总数 / 已售卖(按 Cursor、Kiro、Windsurf) */
|
||||
export function getAccountPoolInventoryTotals() {
|
||||
return request({
|
||||
url: '/platform/home/accountPoolInventoryTotals',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user