This commit is contained in:
2026-08-26 15:10:25 +08:00
parent 06debfd1fc
commit fc9613197b
202 changed files with 28919 additions and 5551 deletions
@@ -0,0 +1,252 @@
<!--#
layout("/layouts/platform.html"){
#-->
<script src="${base!}/assets/platform/plugins/echarts/echarts.min.js" nonce="${cspNonce!}"></script>
<style>
#sub-app-container-main-content { background: #f4f6f8; }
.spread-statistics { width: 100%; }
.spread-statistics .filter-card { margin-bottom: 12px; }
.spread-statistics .metrics-card { margin-bottom: 18px; border: 0; border-radius: 12px; }
.spread-statistics .metrics-card > .el-card__body { padding: 24px; }
.spread-statistics .stat-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 18px; }
.spread-statistics .metric-tile { min-width: 0; min-height: 108px; padding: 20px; display: flex; align-items: center; gap: 16px; box-sizing: border-box; border: 1px solid #e8edf3; border-radius: 13px; background: #fff; box-shadow: 0 3px 14px rgba(15,23,42,.06); transition: transform .25s, box-shadow .25s; }
.spread-statistics .metric-tile:hover { transform: translateY(-2px); box-shadow: 0 8px 22px rgba(15,23,42,.11); }
.spread-statistics .metric-icon { width: 58px; height: 58px; flex: 0 0 58px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 27px; border-radius: 14px; }
.spread-statistics .metric-icon.purple { background: linear-gradient(135deg,#667eea,#764ba2); }
.spread-statistics .metric-icon.green { background: linear-gradient(135deg,#43e97b,#38f9d7); }
.spread-statistics .metric-icon.orange { background: linear-gradient(135deg,#f7971e,#ffd200); }
.spread-statistics .metric-icon.blue { background: linear-gradient(135deg,#4facfe,#00d7ef); }
.spread-statistics .metric-icon.pink { background: linear-gradient(135deg,#f093fb,#f5576c); }
.spread-statistics .metric-info { min-width: 0; }
.spread-statistics .stat-label { margin-bottom: 6px; overflow: hidden; color: #8492a6; font-size: 15px; text-overflow: ellipsis; white-space: nowrap; }
.spread-statistics .stat-value { color: #17233d; font-size: 31px; font-weight: 700; line-height: 1; }
.spread-statistics .chart-grid { display: grid; grid-template-columns: minmax(360px, 1fr) minmax(0, 2fr); gap: 14px; }
.spread-statistics .chart-card { min-width: 0; border: 0; border-radius: 11px; }
.spread-statistics .chart-card > .el-card__header { padding: 26px 26px 18px; font-size: 18px; border-bottom: 1px solid #e4e7ed; }
.spread-statistics .chart-card > .el-card__body { padding: 8px 24px 18px; }
.spread-statistics .chart-box { width: 100%; height: 410px; }
.spread-statistics .top-table-card { margin-top: 14px; border: 0; border-radius: 11px; }
.spread-statistics .top-table-card > .el-card__header { padding: 20px 24px; font-size: 17px; }
@media(max-width: 1199px) { .spread-statistics .stat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media(max-width: 980px) { .spread-statistics .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .spread-statistics .chart-grid { grid-template-columns: 1fr; } }
@media(max-width: 560px) { .spread-statistics .stat-grid { grid-template-columns: 1fr; } .spread-statistics .metrics-card > .el-card__body { padding: 12px; } }
</style>
<div id="app" class="spread-statistics" v-cloak>
<guava ref="guava">
<el-card class="filter-card" shadow="never">
<search :is-search-button="false">
<search-item label="所属栏目">
<el-cascader v-model="query.categoryId" :options="categoryOptions"
:props="{value:'id',label:'name',children:'children',emitPath:false,checkStrictly:true}"
clearable filterable style="width: 100%"></el-cascader>
</search-item>
<search-item label="发布状态">
<el-select v-model="query.isPublished" clearable placeholder="全部">
<el-option label="草稿" :value="0"></el-option>
<el-option label="已发布" :value="1"></el-option>
</el-select>
</search-item>
<search-item label="创建时间">
<el-date-picker v-model="query.createTime" type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss" start-placeholder="开始时间"
end-placeholder="结束时间" style="width: 100%"></el-date-picker>
</search-item>
<div class="search-query">
<el-button type="primary" size="medium" icon="el-icon-search" @click="loadAll">查询</el-button>
<el-button size="medium" @click="resetQuery">重置</el-button>
</div>
</search>
</el-card>
<el-card class="metrics-card" shadow="never" v-loading="loading">
<div class="stat-grid">
<div class="metric-tile">
<div class="metric-icon purple"><i class="el-icon-folder-opened"></i></div>
<div class="metric-info"><div class="stat-label">栏目总数</div><div class="stat-value">{{summary.categoryCount || 0}}</div></div>
</div>
<div class="metric-tile">
<div class="metric-icon purple"><i class="el-icon-document"></i></div>
<div class="metric-info"><div class="stat-label">内容总数</div><div class="stat-value">{{summary.contentCount || 0}}</div></div>
</div>
<div class="metric-tile">
<div class="metric-icon green"><i class="el-icon-circle-check"></i></div>
<div class="metric-info"><div class="stat-label">已发布</div><div class="stat-value">{{summary.publishedCount || 0}}</div></div>
</div>
<div class="metric-tile">
<div class="metric-icon orange"><i class="el-icon-time"></i></div>
<div class="metric-info"><div class="stat-label">未发布</div><div class="stat-value">{{unpublishedCount}}</div></div>
</div>
<div class="metric-tile">
<div class="metric-icon blue"><i class="el-icon-view"></i></div>
<div class="metric-info"><div class="stat-label">总访问量</div><div class="stat-value">{{summary.viewCount || 0}}</div></div>
</div>
<div class="metric-tile">
<div class="metric-icon pink"><i class="el-icon-data-line"></i></div>
<div class="metric-info"><div class="stat-label">平均访问量</div><div class="stat-value">{{summary.avgViewCount || 0}}</div></div>
</div>
</div>
</el-card>
<div class="chart-grid">
<el-card class="chart-card" shadow="never">
<div slot="header"><b>栏目访问量占比</b></div>
<div ref="categoryChart" class="chart-box"></div>
</el-card>
<el-card class="chart-card" shadow="never">
<div slot="header"><b>新闻内容访问量 Top10</b></div>
<div ref="topChart" class="chart-box"></div>
</el-card>
</div>
<el-card class="top-table-card" shadow="never" v-loading="loading">
<div slot="header"><b>新闻内容访问量 Top10</b></div>
<el-table :data="topData" stripe border style="width: 100%">
<el-table-column type="index" label="序号" width="70" align="center"></el-table-column>
<el-table-column prop="categoryName" label="栏目" min-width="120" align="center"></el-table-column>
<el-table-column prop="title" label="标题" min-width="260" show-overflow-tooltip></el-table-column>
<el-table-column prop="summary" label="摘要" min-width="260" show-overflow-tooltip></el-table-column>
<el-table-column prop="unionName" label="所属工会" min-width="150" show-overflow-tooltip align="center"></el-table-column>
<el-table-column label="发布时间" width="170" align="center">
<template slot-scope="{row}">{{formatTime(row.publishedAt)}}</template>
</el-table-column>
<el-table-column prop="viewCount" label="访问量" width="100" align="center"></el-table-column>
</el-table>
</el-card>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
apiBase: "/platform/spread/statistics",
loading: false,
query: {categoryId: "", isPublished: "", createTime: []},
categoryOptions: [],
summary: {},
topData: [],
categoryData: [],
topChart: null,
categoryChart: null
}
},
computed: {
// 未发布数量由内容总数减去已发布数量得到。
unpublishedCount() {
return Math.max(Number(this.summary.contentCount || 0) - Number(this.summary.publishedCount || 0), 0)
}
},
methods: {
// 将时间范围拆分成统计接口参数。
buildParams() {
return {
categoryId: this.query.categoryId,
isPublished: this.query.isPublished,
beginTime: this.query.createTime && this.query.createTime.length ? this.query.createTime[0] : "",
endTime: this.query.createTime && this.query.createTime.length ? this.query.createTime[1] : ""
}
},
// 加载栏目筛选树。
loadCategories() {
return this.$axios.post(this.apiBase + "/categoryTree").then(resp => {
if (resp.code === 0) this.categoryOptions = resp.data || []
})
},
// 并行加载卡片、排行和栏目统计,减少页面等待时间。
loadAll() {
this.loading = true
const params = this.buildParams()
Promise.all([
this.$axios.post(this.apiBase + "/summary", params),
this.$axios.post(this.apiBase + "/top10", params),
this.$axios.post(this.apiBase + "/categoryData", params)
]).then(values => {
this.summary = values[0].code === 0 ? (values[0].data || {}) : {}
this.topData = values[1].code === 0 ? (values[1].data || []) : []
this.categoryData = values[2].code === 0 ? (values[2].data || []) : []
this.$nextTick(this.renderCharts)
}).finally(() => this.loading = false)
},
// 清空筛选并重新统计。
resetQuery() {
this.query = {categoryId: "", isPublished: "", createTime: []}
this.loadAll()
},
// 创建或刷新两个统计图表。
renderCharts() {
if (!window.echarts) return
if (!this.topChart) this.topChart = echarts.init(this.$refs.topChart)
if (!this.categoryChart) this.categoryChart = echarts.init(this.$refs.categoryChart)
this.topChart.setOption({
color: ["#5b8ff9"],
tooltip: {trigger: "axis", axisPointer: {type: "shadow"}},
grid: {left: 58, right: 28, top: 42, bottom: 82},
xAxis: {
type: "category",
data: this.topData.map(item => item.title),
axisTick: {show: false},
axisLine: {lineStyle: {color: "#cfd5df"}},
axisLabel: {
interval: 0,
rotate: 24,
color: "#606266",
formatter: value => value.length > 12 ? value.slice(0, 12) + "…" : value
}
},
yAxis: {
type: "value",
minInterval: 1,
axisLine: {show: false},
axisTick: {show: false},
splitLine: {lineStyle: {color: "#dfe3e8", type: "dashed"}}
},
series: [{
type: "bar",
barMaxWidth: 44,
itemStyle: {color: "#5b8ff9"},
data: this.topData.map(item => item.viewCount || 0)
}]
}, true)
this.categoryChart.setOption({
color: ["#5b8ff9", "#5ad8a6", "#6f86ad", "#f6bd16", "#e8684a", "#9270ca"],
tooltip: {trigger: "item", formatter: "{b}<br/>访问量:{c}<br/>占比:{d}%"},
series: [{
type: "pie",
radius: ["35%", "64%"],
center: ["50%", "56%"],
avoidLabelOverlap: true,
itemStyle: {borderColor: "#fff", borderWidth: 3, borderRadius: 10},
label: {color: "#303133", formatter: "{b}: {d}%"},
labelLine: {length: 16, length2: 18},
data: this.categoryData.map(item => ({name: item.categoryName, value: item.viewCount || 0}))
}]
}, true)
},
// 格式化 Top10 表格的发布时间。
formatTime(value) {
return value ? this.$moment(Number(value)).format("YYYY-MM-DD HH:mm:ss") : ""
},
// 浏览器尺寸变化时重算图表尺寸。
resizeCharts() {
if (this.topChart) this.topChart.resize()
if (this.categoryChart) this.categoryChart.resize()
}
},
mounted() {
window.addEventListener("resize", this.resizeCharts)
// 图表容器挂载完成后自动执行首次查询,避免必须点击“查询”才显示数据。
this.$nextTick(() => {
this.loadCategories().then(() => this.loadAll())
})
},
beforeDestroy() {
window.removeEventListener("resize", this.resizeCharts)
if (this.topChart) this.topChart.dispose()
if (this.categoryChart) this.categoryChart.dispose()
}
})
</script>
<!--#
}
#-->