更改统计
This commit is contained in:
parent
cf7c94c7e7
commit
6170d5a619
@ -11,3 +11,11 @@ export function getAccountPoolDailyExtract(params) {
|
|||||||
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="value">{{ item.value.toLocaleString() }}</div>
|
||||||
<div class="trend" :class="item.isUp ? 'up' : 'down'">
|
<div class="trend" :class="item.isUp ? 'up' : 'down'">
|
||||||
{{ item.isUp ? '↑' : '↓' }} {{ item.percentage }}%
|
{{ item.isUp ? '↑' : '↓' }} {{ item.percentage }}%
|
||||||
<span>较上月</span>
|
<span>{{ item.trendLabel ?? '较上月' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -20,13 +20,21 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20" class="charts-row">
|
<el-row :gutter="20" class="charts-row charts-row--token-bar">
|
||||||
<el-col :xs="24" :sm="24" :md="16">
|
<el-col :xs="24" :sm="24" :md="12">
|
||||||
<el-card shadow="hover" header="Token售卖统计">
|
<el-card shadow="hover" header="Token售卖统计">
|
||||||
<div ref="lineChartRef" class="chart-box"></div>
|
<div ref="lineChartRef" class="chart-box"></div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</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="用户等级分布">
|
<el-card shadow="hover" header="用户等级分布">
|
||||||
<div ref="pieChartRef" class="chart-box"></div>
|
<div ref="pieChartRef" class="chart-box"></div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@ -39,8 +47,8 @@
|
|||||||
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { User, Pointer, Connection, Histogram } from '@element-plus/icons-vue';
|
import { User, Pointer, Connection, ShoppingCart } from '@element-plus/icons-vue';
|
||||||
import { getAccountPoolDailyExtract } from '@/api/home';
|
import { getAccountPoolDailyExtract, getAccountPoolInventoryTotals } from '@/api/home';
|
||||||
|
|
||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
interface SummaryItem {
|
interface SummaryItem {
|
||||||
@ -50,21 +58,49 @@ interface SummaryItem {
|
|||||||
color: string;
|
color: string;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
isUp: boolean;
|
isUp: boolean;
|
||||||
|
/** 趋势说明,默认「较上月」 */
|
||||||
|
trendLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 响应式数据 ---
|
// --- 响应式数据 ---
|
||||||
const lineChartRef = ref<HTMLElement | null>(null);
|
const lineChartRef = ref<HTMLElement | null>(null);
|
||||||
const pieChartRef = ref<HTMLElement | null>(null);
|
const pieChartRef = ref<HTMLElement | null>(null);
|
||||||
|
const barChartRef = ref<HTMLElement | null>(null);
|
||||||
const lineChartInstance = shallowRef<echarts.ECharts | null>(null);
|
const lineChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||||
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
|
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||||
|
const barChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||||
|
|
||||||
const summaryData = ref<SummaryItem[]>([
|
const summaryData = ref<SummaryItem[]>([
|
||||||
{ title: '总用户数', value: 12840, icon: User, color: '#3973FF', percentage: 12, isUp: true },
|
{ title: '总用户数', value: 12840, icon: User, color: '#3973FF', percentage: 12, isUp: true },
|
||||||
{ title: '今日新增', value: 156, icon: Pointer, color: '#67C23A', percentage: 5, 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: 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(
|
function buildSalesLineOption(
|
||||||
days: string[],
|
days: string[],
|
||||||
cursor: number[],
|
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() {
|
async function loadAccountPoolDailyExtract() {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (!lineChartRef.value) return;
|
if (!lineChartRef.value) return;
|
||||||
@ -146,6 +247,17 @@ async function loadAccountPoolDailyExtract() {
|
|||||||
lineChartInstance.value.setOption(buildSalesLineOption(days, cursor, kiro, windsurf), {
|
lineChartInstance.value.setOption(buildSalesLineOption(days, cursor, kiro, windsurf), {
|
||||||
notMerge: true,
|
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 {
|
} catch {
|
||||||
ElMessage.error('加载售卖数据失败');
|
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 = () => {
|
const handleResize = () => {
|
||||||
lineChartInstance.value?.resize();
|
lineChartInstance.value?.resize();
|
||||||
pieChartInstance.value?.resize();
|
pieChartInstance.value?.resize();
|
||||||
|
barChartInstance.value?.resize();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
initLineChartShell();
|
initLineChartShell();
|
||||||
|
initBarChartShell();
|
||||||
initPieChart();
|
initPieChart();
|
||||||
void loadAccountPoolDailyExtract();
|
void loadAccountPoolDailyExtract();
|
||||||
|
void loadAccountPoolInventoryTotals();
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -210,6 +334,8 @@ onUnmounted(() => {
|
|||||||
lineChartInstance.value = null;
|
lineChartInstance.value = null;
|
||||||
pieChartInstance.value?.dispose();
|
pieChartInstance.value?.dispose();
|
||||||
pieChartInstance.value = null;
|
pieChartInstance.value = null;
|
||||||
|
barChartInstance.value?.dispose();
|
||||||
|
barChartInstance.value = null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -275,6 +401,9 @@ onUnmounted(() => {
|
|||||||
height: 350px;
|
height: 350px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
&--token-bar .chart-box {
|
||||||
|
height: 340px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user