feat(stats): 为统计页面添加域名评论分布饼图

- 在 CommentItem 类型中新增 isAdmin 字段以支持后续功能
- 重构域名统计布局,采用表格与饼图并排的响应式设计
- 实现饼图展示各域名评论数量分布,并添加响应式调整与清理逻辑
This commit is contained in:
anghunk
2026-02-05 17:12:19 +08:00
parent 4dd2f6e525
commit f2e9160638
3 changed files with 104 additions and 21 deletions

View File

@@ -21,6 +21,7 @@ export type CommentItem = {
priority?: number; priority?: number;
likes?: number; likes?: number;
ua?: string | null; ua?: string | null;
isAdmin?: boolean;
}; };
export type CommentListResponse = { export type CommentListResponse = {

View File

@@ -88,10 +88,16 @@
color: var(--color-danger); color: var(--color-danger);
} }
.domain-stats-layout {
display: flex;
align-items: stretch;
gap: 16px;
}
.domain-table-wrapper { .domain-table-wrapper {
width: 100%;
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
flex: 1;
} }
.domain-table { .domain-table {
@@ -125,6 +131,19 @@
font-weight: 500; font-weight: 500;
} }
.domain-pie-wrapper {
width: 40%;
display: flex;
align-items: center;
justify-content: center;
}
.domain-pie-chart {
width: 100%;
max-width: 400px;
height: 260px;
}
.chart-wrapper { .chart-wrapper {
margin-top: 4px; margin-top: 4px;
} }
@@ -138,4 +157,12 @@
.stats-grid { .stats-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
}
.domain-stats-layout {
flex-direction: column;
}
.domain-pie-wrapper {
width: 100%;
}
}

View File

@@ -79,27 +79,32 @@
<div v-if="statsLoading" class="page-hint">加载中...</div> <div v-if="statsLoading" class="page-hint">加载中...</div>
<div v-else-if="statsError" class="page-error">{{ statsError }}</div> <div v-else-if="statsError" class="page-error">{{ statsError }}</div>
<div v-else-if="domainStats.length === 0" class="page-hint">暂无评论数据</div> <div v-else-if="domainStats.length === 0" class="page-hint">暂无评论数据</div>
<div v-else class="domain-table-wrapper"> <div v-else class="domain-stats-layout">
<div class="domain-table"> <div class="domain-table-wrapper">
<div class="domain-table-header"> <div class="domain-table">
<div class="domain-cell domain-cell-domain">域名</div> <div class="domain-table-header">
<div class="domain-cell">总数</div> <div class="domain-cell domain-cell-domain">域名</div>
<div class="domain-cell">已通过</div> <div class="domain-cell">总数</div>
<div class="domain-cell">待审核</div> <div class="domain-cell">已通过</div>
<div class="domain-cell">已拒绝</div> <div class="domain-cell">待审核</div>
</div> <div class="domain-cell">已拒绝</div>
<div </div>
v-for="item in domainStats" <div
:key="item.domain" v-for="item in domainStats"
class="domain-table-row" :key="item.domain"
> class="domain-table-row"
<div class="domain-cell domain-cell-domain">{{ item.domain }}</div> >
<div class="domain-cell">{{ item.total }}</div> <div class="domain-cell domain-cell-domain">{{ item.domain }}</div>
<div class="domain-cell">{{ item.approved }}</div> <div class="domain-cell">{{ item.total }}</div>
<div class="domain-cell">{{ item.pending }}</div> <div class="domain-cell">{{ item.approved }}</div>
<div class="domain-cell">{{ item.rejected }}</div> <div class="domain-cell">{{ item.pending }}</div>
<div class="domain-cell">{{ item.rejected }}</div>
</div>
</div> </div>
</div> </div>
<div class="domain-pie-wrapper">
<div ref="domainPieEl" class="domain-pie-chart"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -140,7 +145,9 @@ const toastType = ref<"success" | "error">("success");
const toastVisible = ref(false); const toastVisible = ref(false);
const chartEl = ref<HTMLDivElement | null>(null); const chartEl = ref<HTMLDivElement | null>(null);
const domainPieEl = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null; let chartInstance: echarts.ECharts | null = null;
let domainPieChartInstance: echarts.ECharts | null = null;
function loadChartRangeFromStorage() { function loadChartRangeFromStorage() {
if (typeof window === "undefined") { if (typeof window === "undefined") {
@@ -196,6 +203,7 @@ async function loadStats() {
await nextTick(); await nextTick();
if (!statsError.value) { if (!statsError.value) {
renderChart(); renderChart();
renderDomainPieChart();
} }
} }
} }
@@ -258,6 +266,45 @@ function renderChart() {
chartInstance.setOption(option); chartInstance.setOption(option);
} }
function renderDomainPieChart() {
const el = domainPieEl.value;
if (!el) {
return;
}
if (!domainPieChartInstance) {
domainPieChartInstance = echarts.init(el);
}
if (!domainStats.value.length) {
domainPieChartInstance.clear();
return;
}
const source = domainStats.value.slice().sort((a, b) => b.total - a.total);
const data = source.map((item) => ({
name: item.domain || "未知",
value: item.total,
}));
const option: echarts.EChartsOption = {
tooltip: {
trigger: "item",
formatter: "{b}: {c} ({d}%)",
},
legend: {
orient: "vertical",
left: "left",
},
series: [
{
type: "pie",
radius: ["40%", "70%"],
center: ["60%", "50%"],
avoidLabelOverlap: false,
data,
},
],
};
domainPieChartInstance.setOption(option);
}
function changeChartRange(range: "7" | "30") { function changeChartRange(range: "7" | "30") {
if (chartRange.value === range) { if (chartRange.value === range) {
return; return;
@@ -265,12 +312,16 @@ function changeChartRange(range: "7" | "30") {
chartRange.value = range; chartRange.value = range;
saveChartRangeToStorage(range); saveChartRangeToStorage(range);
renderChart(); renderChart();
renderDomainPieChart();
} }
function handleResize() { function handleResize() {
if (chartInstance) { if (chartInstance) {
chartInstance.resize(); chartInstance.resize();
} }
if (domainPieChartInstance) {
domainPieChartInstance.resize();
}
} }
onMounted(() => { onMounted(() => {
@@ -289,6 +340,10 @@ onBeforeUnmount(() => {
chartInstance.dispose(); chartInstance.dispose();
chartInstance = null; chartInstance = null;
} }
if (domainPieChartInstance) {
domainPieChartInstance.dispose();
domainPieChartInstance = null;
}
}); });
</script> </script>