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
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>
|