feat(stats): 为统计页面添加域名评论分布饼图
- 在 CommentItem 类型中新增 isAdmin 字段以支持后续功能 - 重构域名统计布局,采用表格与饼图并排的响应式设计 - 实现饼图展示各域名评论数量分布,并添加响应式调整与清理逻辑
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user