feat: 优化商品洞察相关代码逻辑、新增性能优化 SQL 并更新前端样式

This commit is contained in:
Peter
2026-03-23 18:01:13 +08:00
parent 6f962d67a2
commit e37e69511f
9 changed files with 263 additions and 35 deletions

View File

@@ -12,6 +12,7 @@ let tagChart;
document.addEventListener('DOMContentLoaded', () => {
initCharts();
setupNavigation();
setupRepurchasePeriod();
// Fetch cutoff dates + initial page data
fetchDataCutoff();
@@ -181,9 +182,14 @@ async function fetchTrend() {
}
// ============== PAGE 2: PRODUCT INSIGHTS ==============
async function fetchRepurchase() {
// 当前选中的复购率统计周期
let repurchaseDays = 30;
async function fetchRepurchase(days) {
if (days !== undefined) repurchaseDays = days;
try {
const response = await fetch(`${BASE_URL}/product/repurchase`);
const response = await fetch(`${BASE_URL}/product/repurchase?days=${repurchaseDays}`);
if (!response.ok) return;
const data = await response.json();
@@ -215,6 +221,20 @@ async function fetchRepurchase() {
} catch (e) {}
}
// 初始化复购率周期切换控件
function setupRepurchasePeriod() {
const container = document.getElementById('repurchase-period');
if (!container) return;
container.addEventListener('click', (e) => {
const btn = e.target.closest('.segment-btn');
if (!btn) return;
container.querySelectorAll('.segment-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const days = parseInt(btn.dataset.days);
fetchRepurchase(days);
});
}
async function fetchBasket() {
try {
const response = await fetch(`${BASE_URL}/product/basket`);

View File

@@ -4,10 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>餐饮零售数据中台 | 核心引擎</title>
<!-- Google Fonts for Apple-like Typography -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<!-- System font stack for Apple-like Typography (no external CDN dependency) -->
<link rel="stylesheet" href="styles.css">
<!-- ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
@@ -92,17 +89,22 @@
<div class="dashboard-grid">
<div class="glass-card chart-container">
<div class="chart-header">
<h2>商品30天复购率与波士顿矩阵 <span class="info-icon" data-tooltip="每日洞察计算任务生成, 统计近30天各商品的购买/复购人数"></span></h2>
<h2>商品复购率与波士顿矩阵 <span class="info-icon" data-tooltip="每日洞察计算任务生成, 统计各商品的购买/复购人数"></span></h2>
<div class="segment-control" id="repurchase-period">
<button class="segment-btn" data-days="7">7天</button>
<button class="segment-btn" data-days="15">15天</button>
<button class="segment-btn active" data-days="30">30天</button>
</div>
</div>
<div class="table-responsive">
<table class="data-table" id="repurchase-table">
<thead>
<tr>
<th>商品名称</th>
<th>30天购买人数 <span class="info-icon info-icon-sm" data-tooltip="COUNT(DISTINCT yz_open_id), 近30天购买过该商品的不重复客户数"></span></th>
<th>30天复购人数 <span class="info-icon info-icon-sm" data-tooltip="近30天内购买该商品≥2次的客户数"></span></th>
<th id="th-buyer-count">购买人数 <span class="info-icon info-icon-sm" data-tooltip="COUNT(DISTINCT yz_open_id), 该周期内购买过该商品的不重复客户数"></span></th>
<th id="th-repurchase-count">复购人数 <span class="info-icon info-icon-sm" data-tooltip="该周期内购买该商品≥2次的客户数"></span></th>
<th>复购率 <span class="info-icon info-icon-sm" data-tooltip="= 复购人数 ÷ 购买人数 × 100%"></span></th>
<th>波士顿矩阵打标 <span class="info-icon info-icon-sm" data-tooltip="购买人数高+复购率高=核心引流爆款; 购买人数高+复购率低=体验差需淘汰; 购买人数低+复购率高=小众潜力款; 其他=长尾观测品"></span></th>
<th>波士顿矩阵打标 <span class="info-icon info-icon-sm" data-tooltip="购买人数高+复购率高=核心引流爆款; 购买人数高+复购率低=体验差需淘汰; 购买人数低+复购率高=小众潜力款; 其他=平庸款"></span></th>
</tr>
</thead>
<tbody>

View File

@@ -36,7 +36,7 @@
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
}
@@ -302,6 +302,52 @@ body {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.08);
}
/* Chart header with optional controls */
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 1rem;
}
.chart-header h2 {
margin: 0;
}
/* Apple-style Segmented Control */
.segment-control {
display: inline-flex;
background: rgba(134, 134, 139, 0.08);
border-radius: 10px;
padding: 3px;
gap: 2px;
}
.segment-btn {
padding: 5px 16px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
font-family: inherit;
}
.segment-btn:hover {
color: var(--text-primary);
}
.segment-btn.active {
background: var(--text-primary);
color: var(--bg-base);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
/* Dashboard Grid */
.dashboard-grid {
display: flex;