|
@@ -78,6 +78,38 @@
|
|
|
</el-col>
|
|
</el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
+ <!-- 停留时段热力(运营看「一天中哪些时段内容最抓人」) -->
|
|
|
|
|
+ <div class="adm-section-title">内容吸引力 · 停留时段热力</div>
|
|
|
|
|
+ <div class="section-caption">近 30 天真实停留时长按小时聚合(文章阅读 + 商品浏览),颜色越深代表该时段用户停留越久,帮助运营把握内容推送黄金时段</div>
|
|
|
|
|
+ <el-card class="heatmap-card" shadow="never" v-loading="trafficLoading">
|
|
|
|
|
+ <div class="heatmap" v-if="dwellByHour.length">
|
|
|
|
|
+ <div class="heatmap__row">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(v, h) in dwellByHour"
|
|
|
|
|
+ :key="h"
|
|
|
|
|
+ class="heatmap__cell"
|
|
|
|
|
+ :style="heatCellStyle(v)"
|
|
|
|
|
+ :title="h + ':00-' + h + ':59 | 真实停留 ' + fmtDuration(v)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="heatmap__hour" v-if="h % 3 === 0">{{ h }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="heatmap__axis">
|
|
|
|
|
+ <span>0</span><span>6</span><span>12</span><span>18</span><span>23 时</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="heatmap__legend">
|
|
|
|
|
+ <span class="heatmap__legend-label">浅</span>
|
|
|
|
|
+ <span class="heatmap__legend-bar"></span>
|
|
|
|
|
+ <span class="heatmap__legend-label">深(停留越久)</span>
|
|
|
|
|
+ <span class="heatmap__total">近 30 天真实停留合计 <b>{{ fmtDuration(dwellByHourTotal) }}</b></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else class="dwell-empty">
|
|
|
|
|
+ <i class="el-icon-data-analysis"></i>
|
|
|
|
|
+ <span>暂无真实停留数据,文章阅读 / 商品浏览埋点接入后展示</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
<!-- 内容广度 · 热度排行 -->
|
|
<!-- 内容广度 · 热度排行 -->
|
|
|
<div class="adm-section-title">内容广度 · 热度排行</div>
|
|
<div class="adm-section-title">内容广度 · 热度排行</div>
|
|
|
<div class="section-caption">以下均基于用户真实行为数据(实际阅读记录 / 真实成交订单 / 真实分享点击),不使用文章浏览计数等虚高指标</div>
|
|
<div class="section-caption">以下均基于用户真实行为数据(实际阅读记录 / 真实成交订单 / 真实分享点击),不使用文章浏览计数等虚高指标</div>
|
|
@@ -244,6 +276,10 @@ export default {
|
|
|
articleDwell: [],
|
|
articleDwell: [],
|
|
|
productDwell: [],
|
|
productDwell: [],
|
|
|
userDwell: [],
|
|
userDwell: [],
|
|
|
|
|
+ // 停留时段热力(近30天真实停留按小时聚合 0-23 点)
|
|
|
|
|
+ dwellByHour: [],
|
|
|
|
|
+ dwellByHourPeak: 0,
|
|
|
|
|
+ dwellByHourTotal: 0,
|
|
|
// 热度排行(归一化后用于渲染)
|
|
// 热度排行(归一化后用于渲染)
|
|
|
hotShares: [],
|
|
hotShares: [],
|
|
|
hotArticles: [],
|
|
hotArticles: [],
|
|
@@ -275,7 +311,7 @@ export default {
|
|
|
],
|
|
],
|
|
|
dwellSections: [
|
|
dwellSections: [
|
|
|
{ key: 'articleDwell', title: '文章停留时长 Top', sub: '按平均停留排序 · 近30天', icon: 'el-icon-document', iconClass: 'sec--primary', emptyHint: '暂无文章阅读停留数据' },
|
|
{ key: 'articleDwell', title: '文章停留时长 Top', sub: '按平均停留排序 · 近30天', icon: 'el-icon-document', iconClass: 'sec--primary', emptyHint: '暂无文章阅读停留数据' },
|
|
|
- { key: 'productDwell', title: '商品停留时长 Top', sub: '按平均停留排序 · 近30天', icon: 'el-icon-shopping-bag-1', iconClass: 'sec--warning', emptyHint: '商品详情页埋点接入后展示' },
|
|
|
|
|
|
|
+ { key: 'productDwell', title: '商品停留时长 Top', sub: '按累计停留排序 · 近30天', icon: 'el-icon-shopping-bag-1', iconClass: 'sec--warning', emptyHint: '商品详情页埋点接入后展示' },
|
|
|
{ key: 'userDwell', title: '用户停留时长 Top', sub: '阅读+分享累计 · 近30天', icon: 'el-icon-user', iconClass: 'sec--success', emptyHint: '暂无用户停留数据' }
|
|
{ key: 'userDwell', title: '用户停留时长 Top', sub: '阅读+分享累计 · 近30天', icon: 'el-icon-user', iconClass: 'sec--success', emptyHint: '暂无用户停留数据' }
|
|
|
],
|
|
],
|
|
|
hotSections: [
|
|
hotSections: [
|
|
@@ -355,6 +391,14 @@ export default {
|
|
|
if (!max) return 0
|
|
if (!max) return 0
|
|
|
return Math.max(4, Math.round((Number(value) || 0) / max * 100))
|
|
return Math.max(4, Math.round((Number(value) || 0) / max * 100))
|
|
|
},
|
|
},
|
|
|
|
|
+ // 时段热力单元格颜色:按相对峰值线性加深(浅蓝 -> 品牌主色 #1677FF)
|
|
|
|
|
+ heatCellStyle(v) {
|
|
|
|
|
+ const peak = this.dwellByHourPeak || 0
|
|
|
|
|
+ if (!peak || !v) return { background: 'var(--adm-fill-quaternary, #f0f3f7)' }
|
|
|
|
|
+ const ratio = Math.min(1, Number(v) / peak)
|
|
|
|
|
+ const alpha = (0.1 + ratio * 0.9).toFixed(3)
|
|
|
|
|
+ return { background: `rgba(22, 119, 255, ${alpha})` }
|
|
|
|
|
+ },
|
|
|
fmtDuration(sec) {
|
|
fmtDuration(sec) {
|
|
|
sec = Number(sec) || 0
|
|
sec = Number(sec) || 0
|
|
|
if (sec < 60) return sec + '秒'
|
|
if (sec < 60) return sec + '秒'
|
|
@@ -408,14 +452,18 @@ export default {
|
|
|
}))
|
|
}))
|
|
|
this.productDwell = (d.productDwellList || []).map(it => ({
|
|
this.productDwell = (d.productDwellList || []).map(it => ({
|
|
|
name: it.productName,
|
|
name: it.productName,
|
|
|
- value: it.avgDurationSeconds || 0,
|
|
|
|
|
- meta: (it.views || 0) + ' 次浏览'
|
|
|
|
|
|
|
+ value: it.totalDurationSeconds || 0,
|
|
|
|
|
+ meta: '平均 ' + this.fmtDuration(it.avgDurationSeconds || 0) + ' · ' + (it.views || 0) + ' 次浏览'
|
|
|
}))
|
|
}))
|
|
|
this.userDwell = (d.userDwellList || []).map(it => ({
|
|
this.userDwell = (d.userDwellList || []).map(it => ({
|
|
|
name: it.userName,
|
|
name: it.userName,
|
|
|
value: it.totalDurationSeconds || 0,
|
|
value: it.totalDurationSeconds || 0,
|
|
|
meta: '累计停留'
|
|
meta: '累计停留'
|
|
|
}))
|
|
}))
|
|
|
|
|
+ // 停留时段热力
|
|
|
|
|
+ this.dwellByHour = d.dwellByHour || []
|
|
|
|
|
+ this.dwellByHourPeak = d.dwellByHourPeak || 0
|
|
|
|
|
+ this.dwellByHourTotal = d.dwellByHourTotal || 0
|
|
|
// 热度 TOP5(归一化)
|
|
// 热度 TOP5(归一化)
|
|
|
this.hotShares = (d.hotShareList || []).slice(0, 5).map(it => ({
|
|
this.hotShares = (d.hotShareList || []).slice(0, 5).map(it => ({
|
|
|
name: it.shareTitle || this.shareTypeLabel(it.targetType),
|
|
name: it.shareTitle || this.shareTypeLabel(it.targetType),
|
|
@@ -919,6 +967,49 @@ export default {
|
|
|
}
|
|
}
|
|
|
.dwell-empty i { font-size: 26px; color: var(--adm-text-25); }
|
|
.dwell-empty i { font-size: 26px; color: var(--adm-text-25); }
|
|
|
|
|
|
|
|
|
|
+/* ===== 停留时段热力图 ===== */
|
|
|
|
|
+.heatmap-card { margin-bottom: 24px; border-radius: var(--adm-radius-card); }
|
|
|
|
|
+.heatmap { padding: 8px 4px; }
|
|
|
|
|
+.heatmap__row { display: flex; gap: 3px; }
|
|
|
|
|
+.heatmap__cell {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ height: 48px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ cursor: default;
|
|
|
|
|
+ transition: background 0.25s ease;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+.heatmap__cell:hover { opacity: 0.88; }
|
|
|
|
|
+.heatmap__hour {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: rgba(255,255,255,0.88);
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ margin-bottom: 3px;
|
|
|
|
|
+}
|
|
|
|
|
+.heatmap__axis { display: flex; justify-content: space-between; margin-top: 6px; }
|
|
|
|
|
+.heatmap__axis span { font-size: 11px; color: var(--adm-text-45); }
|
|
|
|
|
+.heatmap__legend {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--adm-text-65);
|
|
|
|
|
+}
|
|
|
|
|
+.heatmap__legend-bar {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: linear-gradient(90deg, #e6f4ff, #1677FF);
|
|
|
|
|
+}
|
|
|
|
|
+.heatmap__total { margin-left: auto; color: var(--adm-text-65); white-space: nowrap; }
|
|
|
|
|
+.heatmap__total b { color: var(--adm-primary); font-weight: 700; }
|
|
|
|
|
+
|
|
|
/* ===== 图表 ===== */
|
|
/* ===== 图表 ===== */
|
|
|
.chart-row { margin-bottom: 8px; }
|
|
.chart-row { margin-bottom: 8px; }
|
|
|
.chart-card {
|
|
.chart-card {
|