国内市场双十一活动仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

434 lines
16 KiB

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>落地页详情</title>
<style>
body, html { margin:0; padding:0; height:100%; font-family: Arial, "Helvetica Neue", "PingFang SC", "Microsoft Yahei"; background:#f4f6f8; color:#333; }
.page-container { width:100%; height:100vh; display:flex; flex-direction:column; }
.top-bar { height:60px; background:#fff; color:#333; padding:0 20px; display:flex; align-items:center; font-size:18px; font-weight:600; border-bottom:1px solid #eee; }
.main-container { display:flex; flex:1; overflow:hidden; }
.sidebar { width:220px; background:#f5f7fa; border-right:1px solid #e8e8e8; padding:16px; box-sizing:border-box; }
.sidebar h3 { margin:0 0 12px 0; font-size:14px; color:#333; }
.content-area { flex:1; padding:20px; overflow:auto; background:#fff; }
.content-header { display:flex; justify-content:flex-end; margin-bottom:15px; padding-bottom:10px; border-bottom:1px solid #e8e8e8; }
.filter-section { margin-bottom:20px; }
.filter-form { display:flex; align-items:center; flex-wrap:wrap; gap:12px; }
.filter-form label { font-size:14px; color:#333; margin-right:6px; }
.time-separator { margin:0 10px; }
.btn { display:inline-block; padding:8px 12px; border-radius:6px; border:0; cursor:pointer; }
.btn-primary { background:#1890ff; color:#fff; }
.btn-plain { background:#fff; border:1px solid #dcdcdc; color:#333; }
table { width:100%; border-collapse:collapse; background:#fff; }
th, td { padding:12px 8px; border-bottom:1px solid #f0f0f0; text-align:center; font-size:14px; }
th { background:#fafafa; font-weight:600; }
.pagination-container { display:flex; justify-content:flex-end; align-items:center; margin-top:10px; gap:8px; }
select, input[type="number"] { padding:6px; border-radius:4px; border:1px solid #dcdcdc; }
.loading { display:inline-block; margin-left:8px; color:#1890ff; font-size:13px; }
.msg-box { position:fixed; right:20px; bottom:20px; z-index:9999; padding:8px 12px; border-radius:6px; color:#fff; box-shadow:0 4px 12px rgba(0,0,0,0.1); display:none; }
.msg-success { background:#67c23a; }
.msg-error { background:#f56c6c; }
@media (max-width:700px) { .sidebar { display:none; } .content-area { padding:12px; } }
#msgBoxTop {
position: fixed;
top: 16px; /* 距离顶部距离,可调整 */
left: 50%;
transform: translateX(-50%) translateY(-8px);
z-index: 2147483647; /* 尽可能置顶 */
min-width: 220px;
max-width: 92%;
padding: 10px 16px;
border-radius: 8px;
color: #fff;
box-shadow: 0 8px 28px rgba(0,0,0,0.18);
display: none;
align-items: center;
justify-content: center;
font-size: 14px;
text-align: center;
white-space: pre-wrap;
pointer-events: none; /* 不阻塞下方交互;如果需要可改为 auto */
}
/* 显示时的动画 */
#msgBoxTop.show {
display: flex;
animation: msg-slide-down 220ms cubic-bezier(.2,.9,.2,1);
transform: translateX(-50%) translateY(0);
}
/* 轻微淡出(移除 show 时也会自然消失)*/
@keyframes msg-slide-down {
from { opacity: 0; transform: translateX(-50%) translateY(-8px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* 颜色主题 */
#msgBoxTop.msg-success { background: #67c23a; }
#msgBoxTop.msg-error { background: #f56c6c; }
</style>
</head>
<body>
<div class="page-container">
<div class="top-bar"><span>详情</span></div>
<div class="main-container">
<div class="sidebar">
<h3>管理菜单</h3>
<div>• 落地页管理</div>
</div>
<div class="content-area">
<div class="content-header">
<button id="btnBack" class="btn btn-plain">返回上一级页面</button>
</div>
<div class="filter-section">
<div class="filter-form" role="form" aria-label="筛选">
<div>
<label>打开网页时间:</label>
<input id="filterStart" type="datetime-local" style="width:180px" />
<span class="time-separator"></span>
<input id="filterEnd" type="datetime-local" style="width:180px" />
</div>
<div>
<label>收下状态:</label>
<select id="filterStatus" style="width:150px">
<option value="">全部</option>
<option value="1"></option>
<option value="0"></option>
</select>
</div>
<div>
<button id="btnQuery" class="btn btn-primary">查询</button>
</div>
<div>
<button id="btnExport" class="btn btn-primary btn-plain">导出</button>
</div>
</div>
</div>
<div id="tableWrap" style="min-height:200px">
<table aria-describedby="详情列表">
<thead>
<tr>
<th style="width:80px">序号</th>
<th style="min-width:180px">用户信息</th>
<th style="min-width:180px">打开网页时间</th>
<th style="min-width:120px">收下状态</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- JS 渲染 -->
</tbody>
</table>
<div id="loadingIndicator" style="padding:10px 0; display:none;"><span class="loading">加载中...</span></div>
<div id="noData" style="padding:20px 0; display:none; color:#666; text-align:center;">暂无数据</div>
</div>
<div class="pagination-container" style="margin-top:14px;">
<div><span id="totalCount">0</span></div>
<div style="margin-left:12px;">
每页
<select id="pageSizeSelect">
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div style="margin-left:12px;">
<button id="prevBtn" class="btn">上一页</button>
<span style="margin:0 8px"><span id="currentPage">1</span></span>
<button id="nextBtn" class="btn">下一页</button>
</div>
</div>
</div>
</div>
</div>
<!-- 消息提示 -->
<div id="msgBox" class="msg-box"></div>
<script type="module">
import { getLandingDetailApi, exportLandingDetailApi } from './src/api/member.js';;
(function () {
// ====== 状态 ======
let tableData = [];
let currentPage = 1;
let pageSize = 20;
let totalCount = 0;
// ====== DOM ======
const btnBack = document.getElementById('btnBack');
const btnQuery = document.getElementById('btnQuery');
const btnExport = document.getElementById('btnExport');
const filterStart = document.getElementById('filterStart');
const filterEnd = document.getElementById('filterEnd');
const filterStatus = document.getElementById('filterStatus');
const tableBody = document.getElementById('tableBody');
const totalCountEl = document.getElementById('totalCount');
const currentPageEl = document.getElementById('currentPage');
const pageSizeSelect = document.getElementById('pageSizeSelect');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const loadingIndicator = document.getElementById('loadingIndicator');
const noData = document.getElementById('noData');
const msgBox = document.getElementById('msgBox');
function showMessage(text, type = 'success', duration = 3000) {
let el = document.getElementById('msgBoxTop');
if (!el) {
el = document.createElement('div');
el.id = 'msgBoxTop';
el.setAttribute('role', 'status');
el.setAttribute('aria-live', 'polite');
document.body.appendChild(el);
}
// 更新内容与样式
el.textContent = text;
el.classList.remove('msg-success', 'msg-error', 'show');
el.classList.add(type === 'error' ? 'msg-error' : 'msg-success');
// 触发显示(重新触发动画)
// pointer-events: none 允许消息不阻塞下层交互;若想阻塞改为 auto
requestAnimationFrame(() => {
el.classList.add('show');
});
// 清除旧定时器并设置新定时器隐藏
if (el._timer) {
clearTimeout(el._timer);
}
el._timer = setTimeout(() => {
el.classList.remove('show');
// 可选:在动画结束后彻底隐藏(防止 display:flex 仍在)
setTimeout(() => {
if (!el.classList.contains('show')) {
el.style.display = 'none';
// 恢复 display 控制以便再次显示时通过 .show 控制显示
el.style.display = '';
}
}, 240);
}, Math.max(800, duration)); // 最少保留 800ms,避免闪烁
}
function formatDateTimeInput(v) {
// 输入 v: "2025-10-23T14:00" -> 输出 "YYYY-MM-DD HH:mm:ss"
if (!v) return '';
// v may be "2025-10-23T14:00" or "2025-10-23T14:00:00"
let s = v.replace('T', ' ');
if (s.length === 16) s += ':00';
return s;
}
function parseQueryParam(name) {
const sp = new URLSearchParams(location.search);
return sp.get(name);
}
function syncEndMinWithStart() {
if (!filterStart || !filterEnd) return;
// set min attribute on end input so browser picker enforces it
if (filterStart.value) {
filterEnd.min = filterStart.value;
} else {
filterEnd.removeAttribute('min');
}
}
function isTimeRangeValid() {
if (!filterStart.value || !filterEnd.value) return true; // 空值不做强制(按需求可改)
// Compare strings directly is OK for datetime-local "YYYY-MM-DDTHH:MM" format
return filterEnd.value >= filterStart.value;
}
// 绑定事件:开始时间变化时更新 end.min,并修正 end 小于 start 的情况
filterStart && filterStart.addEventListener('change', () => {
syncEndMinWithStart();
if (filterEnd.value && filterEnd.value < filterStart.value) {
// 把结束时间自动设为开始时间,提示用户
filterEnd.value = filterStart.value;
showMessage('结束时间已被调整为不早于开始时间', 'error');
}
});
// 绑定事件:结束时间变化时校验,如果不合法则阻止并提示(并回退到开始时间或清空)
filterEnd && filterEnd.addEventListener('change', () => {
if (!isTimeRangeValid()) {
showMessage('结束时间不能早于开始时间,请调整', 'error');
// 方案:把结束时间重置为开始时间(更直观),也可选择清空
if (filterStart.value) {
filterEnd.value = filterStart.value;
} else {
filterEnd.value = '';
}
}
});
// 构建列表查询参数对象
function buildQueryParams() {
const id = parseQueryParam('id');
const startTime = formatDateTimeInput(filterStart.value);
const endTime = formatDateTimeInput(filterEnd.value);
const state = (filterStatus.value === '' || filterStatus.value === null) ? -1 : filterStatus.value;
return { id, page: currentPage, page_size: pageSize, start_time: startTime, end_time: endTime, state };
}
// 将 params 对象转为查询字符串
function toQueryString(params) {
return Object.keys(params)
.filter(k => params[k] !== undefined && params[k] !== null && params[k] !== '')
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
.join('&');
}
// ====== 数据请求 ======
async function getLandingDetail() {
const id = parseQueryParam('id');
if (!id) {
showMessage('未获取到活动ID,无法加载详情', 'error');
return;
}
loadingIndicator.style.display = 'inline-block';
tableBody.innerHTML = '';
noData.style.display = 'none';
try {
const res = await getLandingDetailApi({
id: id,
page: currentPage,
page_size: pageSize,
start_time: formatDateTimeInput(filterStart.value),
end_time: formatDateTimeInput(filterEnd.value),
state: (filterStatus.value === '' || filterStatus.value === null) ? -1 : filterStatus.value
});
if(res.code == 200){
tableData = res.data.list;
totalCount = res.data.total ;
renderTable();
} else {
showMessage('获取活动详情失败', 'error');
}
} catch (err) {
console.error(err);
showMessage('网络异常,无法加载数据', 'error');
} finally {
loadingIndicator.style.display = 'none';
}
}
function renderTable() {
tableBody.innerHTML = '';
if (!tableData || tableData.length === 0) {
noData.style.display = 'block';
return
} else {
noData.style.display = 'none';
}
tableData.forEach((row, idx) => {
const tr = document.createElement('tr');
const tdIndex = document.createElement('td');
tdIndex.textContent = (currentPage - 1) * pageSize + idx + 1;
tr.appendChild(tdIndex);
const tdUser = document.createElement('td');
tdUser.textContent = row.user_info || '';
tr.appendChild(tdUser);
const tdCreated = document.createElement('td');
tdCreated.textContent = row.created_at || '';
tr.appendChild(tdCreated);
const tdState = document.createElement('td');
tdState.textContent = row.state === 1 ? '是' : '否';
tr.appendChild(tdState);
tableBody.appendChild(tr);
});
totalCountEl.textContent = totalCount;
currentPageEl.textContent = currentPage;
}
// ====== 导出功能 ======
async function handleExport() {
const id = parseQueryParam('id');
if (!id) { showMessage('未获取到活动ID,无法导出', 'error'); return; }
loadingIndicator.style.display = 'inline-block';
try {
const res = await exportLandingDetailApi({
id: id,
start_time: formatDateTimeInput(filterStart.value),
end_time: formatDateTimeInput(filterEnd.value),
state: (filterStatus.value === '' || filterStatus.value === null) ? -1 : filterStatus.value
});
const data = res.data !== undefined ? res.data : res;
const blob = new Blob([data], { type: res.headers?.['content-type'] || 'application/octet-stream' });
let filename = `活动数据.xlsx`;
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(blobUrl);
showMessage('导出成功');
} catch (err) {
console.error(err);
showMessage('导出失败,请重试', 'error');
} finally {
loadingIndicator.style.display = 'none';
}
}
// ====== 分页事件 ======
prevBtn.addEventListener('click', () => {
if (currentPage > 1) {
currentPage--;
getLandingDetail();
}
});
nextBtn.addEventListener('click', () => {
const maxPage = Math.max(1, Math.ceil((totalCount || 1) / pageSize));
if (currentPage < maxPage) {
currentPage++;
getLandingDetail();
}
});
pageSizeSelect.addEventListener('change', (e) => {
pageSize = Number(e.target.value);
currentPage = 1;
getLandingDetail();
});
// ====== 交互事件 ======
btnBack.addEventListener('click', () => {
history.back();
});
btnQuery.addEventListener('click', () => {
currentPage = 1;
getLandingDetail();
});
btnExport.addEventListener('click', () => {
handleExport();
});
// 页面加载时读取 id 并请求数据
(function init() {
const id = parseQueryParam('id');
if (!id) {
showMessage('缺少活动ID,请在 URL 里添加 ?id=xxx', 'error');
}
// use defaults from HTML (pageSizeSelect initial value)
pageSize = Number(pageSizeSelect.value || 20);
getLandingDetail();
})();
})();
</script>
</body>
</html>