4 changed files with 931 additions and 1068 deletions
-
1085adminConfig.html
-
887adminDetail.html
-
23src/api/member.js
-
4vite.config.js
1085
adminConfig.html
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,549 +1,400 @@ |
|||||
<!DOCTYPE html> |
|
||||
|
<!doctype html> |
||||
<html lang="zh-CN"> |
<html lang="zh-CN"> |
||||
<head> |
<head> |
||||
<meta charset="UTF-8"> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
<title>管理后台</title> |
|
||||
|
<meta charset="utf-8" /> |
||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" /> |
||||
|
<title>落地页详情</title> |
||||
<style> |
<style> |
||||
/* 基础样式 */ |
|
||||
* { |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
box-sizing: border-box; |
|
||||
font-family: "Microsoft YaHei", sans-serif; |
|
||||
} |
|
||||
body { |
|
||||
background-color: #f5f7fa; |
|
||||
color: #333; |
|
||||
} |
|
||||
.container { |
|
||||
display: flex; |
|
||||
min-height: 100vh; |
|
||||
} |
|
||||
|
|
||||
/* 侧边栏样式 */ |
|
||||
.sidebar { |
|
||||
width: 220px; |
|
||||
background-color: #2c3e50; |
|
||||
color: #fff; |
|
||||
padding: 20px 0; |
|
||||
} |
|
||||
.sidebar-logo { |
|
||||
text-align: center; |
|
||||
padding: 0 20px 20px; |
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1); |
|
||||
margin-bottom: 20px; |
|
||||
} |
|
||||
.sidebar-logo h2 { |
|
||||
font-size: 18px; |
|
||||
margin-top: 10px; |
|
||||
} |
|
||||
.sidebar-menu { |
|
||||
list-style: none; |
|
||||
} |
|
||||
.sidebar-menu li a { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
padding: 12px 20px; |
|
||||
color: rgba(255,255,255,0.8); |
|
||||
text-decoration: none; |
|
||||
transition: all 0.3s; |
|
||||
} |
|
||||
.sidebar-menu li a:hover, |
|
||||
.sidebar-menu li a.active { |
|
||||
background-color: #34495e; |
|
||||
color: #fff; |
|
||||
} |
|
||||
.sidebar-menu li a i { |
|
||||
margin-right: 10px; |
|
||||
font-size: 16px; |
|
||||
} |
|
||||
|
|
||||
/* 主内容区样式 */ |
|
||||
.main-content { |
|
||||
flex: 1; |
|
||||
padding: 20px; |
|
||||
overflow-y: auto; |
|
||||
} |
|
||||
.page-header { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
margin-bottom: 20px; |
|
||||
padding-bottom: 15px; |
|
||||
border-bottom: 1px solid #eee; |
|
||||
} |
|
||||
.page-header h2 { |
|
||||
font-size: 20px; |
|
||||
font-weight: 600; |
|
||||
color: #333; |
|
||||
} |
|
||||
.btn { |
|
||||
padding: 6px 12px; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 4px; |
|
||||
background-color: #fff; |
|
||||
cursor: pointer; |
|
||||
color: #333; |
|
||||
text-decoration: none; |
|
||||
} |
|
||||
.btn-primary { |
|
||||
background-color: #3498db; |
|
||||
color: #fff; |
|
||||
border-color: #3498db; |
|
||||
} |
|
||||
.btn-danger { |
|
||||
background-color: #e74c3c; |
|
||||
color: #fff; |
|
||||
border-color: #e74c3c; |
|
||||
} |
|
||||
|
|
||||
/* 查询区域样式 */ |
|
||||
.search-area { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
gap: 10px; |
|
||||
margin-bottom: 20px; |
|
||||
flex-wrap: wrap; /* 适配小屏幕 */ |
|
||||
} |
|
||||
.search-area input, |
|
||||
.search-area select { |
|
||||
padding: 6px 10px; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 4px; |
|
||||
outline: none; |
|
||||
} |
|
||||
.search-area .date-picker { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
gap: 5px; |
|
||||
} |
|
||||
|
|
||||
/* 表格样式 */ |
|
||||
.table-container { |
|
||||
background-color: #fff; |
|
||||
border-radius: 4px; |
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1); |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
.data-table { |
|
||||
width: 100%; |
|
||||
border-collapse: collapse; |
|
||||
} |
|
||||
.data-table th, |
|
||||
.data-table td { |
|
||||
padding: 12px 15px; |
|
||||
text-align: left; |
|
||||
border-bottom: 1px solid #f0f0f0; |
|
||||
} |
|
||||
.data-table th { |
|
||||
background-color: #f9fafb; |
|
||||
font-weight: 600; |
|
||||
color: #666; |
|
||||
} |
|
||||
.data-table tbody tr:hover { |
|
||||
background-color: #f5f7fa; |
|
||||
} |
|
||||
|
|
||||
/* 分页样式 */ |
|
||||
.pagination { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
padding: 15px; |
|
||||
border-top: 1px solid #f0f0f0; |
|
||||
} |
|
||||
.pagination-info { |
|
||||
color: #666; |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
.pagination-list { |
|
||||
display: flex; |
|
||||
list-style: none; |
|
||||
padding: 0; |
|
||||
margin: 0; |
|
||||
} |
|
||||
.pagination-list li { |
|
||||
margin: 0 2px; |
|
||||
} |
|
||||
.pagination-list a, |
|
||||
.pagination-list span { |
|
||||
display: inline-block; |
|
||||
width: 45px; |
|
||||
height: 45px; |
|
||||
line-height: 45px; |
|
||||
text-align: center; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 4px; |
|
||||
text-decoration: none; |
|
||||
color: #333; |
|
||||
font-size: 14px; |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
.pagination-list a { |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
.pagination-list a:hover, |
|
||||
.pagination-list a.active { |
|
||||
background-color: #3498db; |
|
||||
color: #fff; |
|
||||
border-color: #3498db; |
|
||||
} |
|
||||
.pagination-list span { |
|
||||
background-color: #f5f5f5; |
|
||||
color: #999; |
|
||||
cursor: not-allowed; |
|
||||
} |
|
||||
.pagination-jump { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
font-size: 14px; |
|
||||
gap: 5px; |
|
||||
} |
|
||||
.pagination-jump input { |
|
||||
width: 50px; |
|
||||
height: 32px; |
|
||||
margin: 0 5px; |
|
||||
padding: 0 5px; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 4px; |
|
||||
text-align: center; |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
.pagination-jump button { |
|
||||
padding: 6px 12px; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 4px; |
|
||||
background-color: #fff; |
|
||||
cursor: pointer; |
|
||||
height: 32px; |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
|
|
||||
/* 加载中样式 */ |
|
||||
.loading { |
|
||||
text-align: center; |
|
||||
padding: 50px 0; |
|
||||
color: #666; |
|
||||
} |
|
||||
.loading i { |
|
||||
font-size: 24px; |
|
||||
margin-bottom: 10px; |
|
||||
animation: spin 1s linear infinite; |
|
||||
} |
|
||||
@keyframes spin { |
|
||||
from { transform: rotate(0deg); } |
|
||||
to { transform: rotate(360deg); } |
|
||||
} |
|
||||
|
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> |
</style> |
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> |
|
||||
</head> |
</head> |
||||
<body> |
<body> |
||||
<div class="container"> |
|
||||
<div class="sidebar"> |
|
||||
<div class="sidebar-logo"> |
|
||||
<i class="fa fa-line-chart" style="font-size: 24px;"></i> |
|
||||
<h2>后台管理系统</h2> |
|
||||
|
<div class="page-container"> |
||||
|
<div class="top-bar"><span>详情</span></div> |
||||
|
<div class="main-container"> |
||||
|
<div class="sidebar"> |
||||
|
<h3>管理菜单</h3> |
||||
|
<div>• 落地页管理</div> |
||||
</div> |
</div> |
||||
<ul class="sidebar-menu"> |
|
||||
<li><a href="#" class="active"><i class="fa fa-file-text-o"></i> 落地页管理</a></li> |
|
||||
</ul> |
|
||||
</div> |
|
||||
|
|
||||
<!-- 主内容区 --> |
|
||||
<div class="main-content"> |
|
||||
<div class="page-header"> |
|
||||
<div class="page-header h2">详情</div> |
|
||||
<a href="adminConfig.html" class="btn">返回上一级页面</a> |
|
||||
</div> |
|
||||
|
<div class="content-area"> |
||||
|
<div class="content-header"> |
||||
|
<button id="btnBack" class="btn btn-plain">返回上一级页面</button> |
||||
|
</div> |
||||
|
|
||||
<!-- 查询区域 --> |
|
||||
<div class="search-area"> |
|
||||
<label class="date-picker"> |
|
||||
打开网页时间 |
|
||||
<input type="date" id="startTime" placeholder="请选择开始时间" /> |
|
||||
至 |
|
||||
<input type="date" id="endTime" placeholder="请选择结束时间" /> |
|
||||
</label> |
|
||||
<div> |
|
||||
<label for="statusSelect">收下状态</label> |
|
||||
<select id="statusSelect"> |
|
||||
<option value="">请选择状态</option> |
|
||||
<option value="1">是</option> |
|
||||
<option value="0">否</option> |
|
||||
</select> |
|
||||
|
<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> |
||||
<button id="searchBtn" class="btn btn-primary">查询</button> |
|
||||
<button class="btn btn-danger">导出</button> |
|
||||
</div> |
|
||||
|
|
||||
<!-- 数据表格 --> |
|
||||
<div class="table-container"> |
|
||||
<table class="data-table"> |
|
||||
<thead> |
|
||||
<tr> |
|
||||
<th>序号</th> |
|
||||
<th>打开网页时间</th> |
|
||||
<th>收下状态</th> |
|
||||
</tr> |
|
||||
</thead> |
|
||||
<tbody id="tableBody"> |
|
||||
<tr> |
|
||||
<td colspan="3" class="loading"> <!-- 注意:表格列数是3,colspan改为3 --> |
|
||||
<i class="fa fa-spinner"></i> |
|
||||
<p>加载中...</p> |
|
||||
</td> |
|
||||
</tr> |
|
||||
</tbody> |
|
||||
</table> |
|
||||
|
|
||||
<!-- 分页控件 --> |
|
||||
<div class="pagination" id="paginationContainer"> |
|
||||
|
<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> |
</div> |
||||
</div> |
</div> |
||||
|
|
||||
<script type="module"> |
|
||||
// 导入API函数(假设接口定义) |
|
||||
import { getLandingDetailApi } from './src/api/member.js'; |
|
||||
|
|
||||
// DOM元素 |
|
||||
const tableBody = document.getElementById('tableBody'); |
|
||||
const paginationContainer = document.getElementById('paginationContainer'); |
|
||||
const searchBtn = document.getElementById('searchBtn'); |
|
||||
const startTimeInput = document.getElementById('startTime'); |
|
||||
const endTimeInput = document.getElementById('endTime'); |
|
||||
const statusSelect = document.getElementById('statusSelect'); |
|
||||
|
|
||||
// 分页参数 |
|
||||
let currentPage = 1; |
|
||||
const pageSize = 20; |
|
||||
let totalCount = 0; |
|
||||
let totalPages = 0; |
|
||||
|
|
||||
// URL获取的id参数 |
|
||||
let landingId = ''; |
|
||||
|
|
||||
// 初始化页面:从URL获取页码,请求后端数据 |
|
||||
async function initPage() { |
|
||||
const urlParams = new URLSearchParams(window.location.search); |
|
||||
landingId = urlParams.get('id'); |
|
||||
|
|
||||
if (!landingId) { |
|
||||
renderEmptyState('缺少必要的参数'); |
|
||||
return; |
|
||||
|
<!-- 消息提示 --> |
||||
|
<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 = ''; |
||||
} |
} |
||||
|
|
||||
// 从后端请求数据 |
|
||||
await fetchDataFromBackend(); |
|
||||
} |
|
||||
|
|
||||
// 核心:从后端API请求数据 |
|
||||
async function fetchDataFromBackend() { |
|
||||
// 显示加载状态 |
|
||||
tableBody.innerHTML = ` |
|
||||
<tr> |
|
||||
<td colspan="3" class="loading"> |
|
||||
<i class="fa fa-spinner"></i> |
|
||||
<p>加载中...</p> |
|
||||
</td> |
|
||||
</tr> |
|
||||
`; |
|
||||
paginationContainer.innerHTML = ''; |
|
||||
|
|
||||
try { |
|
||||
// 1. 构造请求参数(分页+查询条件) |
|
||||
const requestParams = { |
|
||||
id: landingId, |
|
||||
page: currentPage, // 当前页码 |
|
||||
size: pageSize, // 每页条数 |
|
||||
startTime: startTimeInput.value || '', // 开始时间(查询条件) |
|
||||
endTime: endTimeInput.value || '', // 结束时间(查询条件) |
|
||||
receiveStatus: statusSelect.value || '' // 收下状态(查询条件) |
|
||||
}; |
|
||||
|
|
||||
// 2. 调用导入的API函数 |
|
||||
const result = await getLandingDetailApi(requestParams); |
|
||||
|
|
||||
// 3. 处理API返回结果 |
|
||||
if (result.code === 200) { |
|
||||
const { list, total } = result.data; |
|
||||
totalCount = total; |
|
||||
totalPages = Math.ceil(totalCount / pageSize); |
|
||||
|
|
||||
renderTable(list); |
|
||||
renderPagination(); |
|
||||
|
}, 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 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 { |
} else { |
||||
renderEmptyState('获取数据失败'); |
|
||||
} |
|
||||
|
|
||||
} catch (error) { |
|
||||
// 捕获异常 |
|
||||
console.error('请求数据异常:', error); |
|
||||
renderEmptyState('网络错误,请稍后重试'); |
|
||||
|
showMessage('获取活动详情失败', 'error'); |
||||
} |
} |
||||
|
} catch (err) { |
||||
|
console.error(err); |
||||
|
showMessage('网络异常,无法加载数据', 'error'); |
||||
|
} finally { |
||||
|
loadingIndicator.style.display = 'none'; |
||||
} |
} |
||||
|
} |
||||
|
|
||||
// 渲染表格数据 |
|
||||
function renderTable(dataList) { |
|
||||
if (!dataList || dataList.length === 0) { |
|
||||
renderEmptyState('暂无符合条件的数据'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
let html = ''; |
|
||||
dataList.forEach((item, index) => { |
|
||||
// 计算序号:(当前页-1)*每页条数 + 索引+1 |
|
||||
const serialNumber = (currentPage - 1) * pageSize + index + 1; |
|
||||
|
|
||||
// 注意:item的字段名需与后端返回的字段一致 |
|
||||
html += ` |
|
||||
<tr> |
|
||||
<td>${serialNumber}</td> |
|
||||
<td>${formatDate(item.openTime || '')}</td> |
|
||||
<td>${item.receiveStatus === '1' ? '是' : item.receiveStatus === '0' ? '否' : ''}</td> |
|
||||
</tr> |
|
||||
`; |
|
||||
}); |
|
||||
|
|
||||
tableBody.innerHTML = html; |
|
||||
|
function renderTable() { |
||||
|
tableBody.innerHTML = ''; |
||||
|
if (!tableData || tableData.length === 0) { |
||||
|
noData.style.display = 'block'; |
||||
|
} else { |
||||
|
noData.style.display = 'none'; |
||||
} |
} |
||||
|
|
||||
// 渲染空状态 |
|
||||
function renderEmptyState(message) { |
|
||||
tableBody.innerHTML = ` |
|
||||
<tr> |
|
||||
<td colspan="3" style="text-align: center; padding: 30px;"> |
|
||||
<i class="fa fa-inbox" style="font-size: 24px; color: #ddd; margin-bottom: 10px;"></i> |
|
||||
<p>${message}</p> |
|
||||
</td> |
|
||||
</tr> |
|
||||
`; |
|
||||
paginationContainer.innerHTML = ''; |
|
||||
} |
|
||||
|
tableData.forEach((row, idx) => { |
||||
|
const tr = document.createElement('tr'); |
||||
|
|
||||
// 渲染分页控件 |
|
||||
function renderPagination() { |
|
||||
if (totalCount === 0) return; |
|
||||
|
|
||||
// 生成页码列表 |
|
||||
const pageNumbers = []; |
|
||||
let startPage = Math.max(1, currentPage - 2); |
|
||||
let endPage = Math.min(totalPages, currentPage + 2); |
|
||||
|
|
||||
if (endPage - startPage < 4 && totalPages >= 5) { |
|
||||
if (startPage === 1) endPage = 5; |
|
||||
else if (endPage === totalPages) startPage = totalPages - 4; |
|
||||
} |
|
||||
|
|
||||
for (let i = startPage; i <= endPage; i++) { |
|
||||
pageNumbers.push(i); |
|
||||
} |
|
||||
|
const tdIndex = document.createElement('td'); |
||||
|
tdIndex.textContent = (currentPage - 1) * pageSize + idx + 1; |
||||
|
tr.appendChild(tdIndex); |
||||
|
|
||||
// 分页HTML |
|
||||
const html = ` |
|
||||
<div class="pagination-info"> |
|
||||
共 ${totalCount} 条记录,当前第 ${currentPage} / ${totalPages} 页 |
|
||||
</div> |
|
||||
<ul class="pagination-list"> |
|
||||
<li> |
|
||||
${currentPage === 1 |
|
||||
? `<span>首页</span>` |
|
||||
: `<a href="?page=1">首页</a>` |
|
||||
} |
|
||||
</li> |
|
||||
<li> |
|
||||
${currentPage > 1 |
|
||||
? `<a href="?page=${currentPage - 1}">上一页</a>` |
|
||||
: `<span>上一页</span>` |
|
||||
} |
|
||||
</li> |
|
||||
${pageNumbers.map(num => ` |
|
||||
<li> |
|
||||
<a href="?page=${num}" class="${num === currentPage ? 'active' : ''}"> |
|
||||
${num} |
|
||||
</a> |
|
||||
</li> |
|
||||
`).join('')} |
|
||||
<li> |
|
||||
${currentPage < totalPages |
|
||||
? `<a href="?page=${currentPage + 1}">下一页</a>` |
|
||||
: `<span>下一页</span>` |
|
||||
} |
|
||||
</li> |
|
||||
<li> |
|
||||
${currentPage === totalPages |
|
||||
? `<span>尾页</span>` |
|
||||
: `<a href="?page=${totalPages}">尾页</a>` |
|
||||
} |
|
||||
</li> |
|
||||
</ul> |
|
||||
<div class="pagination-jump"> |
|
||||
<span>跳至</span> |
|
||||
<input type="number" min="1" max="${totalPages}" value="${currentPage}" id="jumpPageInput"> |
|
||||
<span>页</span> |
|
||||
<button id="jumpBtn">确定</button> |
|
||||
</div> |
|
||||
`; |
|
||||
|
const tdUser = document.createElement('td'); |
||||
|
tdUser.textContent = row.user_info || ''; |
||||
|
tr.appendChild(tdUser); |
||||
|
|
||||
paginationContainer.innerHTML = html; |
|
||||
|
const tdCreated = document.createElement('td'); |
||||
|
tdCreated.textContent = row.created_at || ''; |
||||
|
tr.appendChild(tdCreated); |
||||
|
|
||||
// 绑定跳转事件 |
|
||||
document.getElementById('jumpBtn').addEventListener('click', handlePageJump); |
|
||||
document.getElementById('jumpPageInput').addEventListener('keypress', (e) => { |
|
||||
if (e.key === 'Enter') handlePageJump(); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 处理页码跳转 |
|
||||
function handlePageJump() { |
|
||||
const pageInput = document.getElementById('jumpPageInput'); |
|
||||
const page = parseInt(pageInput.value); |
|
||||
|
|
||||
if (page && !isNaN(page) && page >= 1 && page <= totalPages && page !== currentPage) { |
|
||||
window.location.href = `?page=${page}`; |
|
||||
} else { |
|
||||
alert('请输入有效的页码'); |
|
||||
} |
|
||||
} |
|
||||
|
const tdState = document.createElement('td'); |
||||
|
tdState.textContent = row.state === 1 ? '是' : '否'; |
||||
|
tr.appendChild(tdState); |
||||
|
|
||||
// 查询按钮事件:点击后重新请求数据 |
|
||||
searchBtn.addEventListener('click', async () => { |
|
||||
currentPage = 1; // 重置为第一页 |
|
||||
await fetchDataFromBackend(); // 重新请求后端数据 |
|
||||
|
tableBody.appendChild(tr); |
||||
}); |
}); |
||||
|
|
||||
// 工具函数:格式化日期(适配后端返回的时间格式) |
|
||||
function formatDate(dateString) { |
|
||||
if (!dateString) return ''; |
|
||||
const date = new Date(dateString); |
|
||||
// 处理无效日期 |
|
||||
if (isNaN(date.getTime())) return dateString; |
|
||||
// 格式化显示:年-月-日 时:分:秒 |
|
||||
return date.toLocaleString('zh-CN', { |
|
||||
year: 'numeric', |
|
||||
month: '2-digit', |
|
||||
day: '2-digit', |
|
||||
hour: '2-digit', |
|
||||
minute: '2-digit', |
|
||||
second: '2-digit' |
|
||||
}).replace(/\//g, '-'); |
|
||||
} |
|
||||
|
|
||||
// 工具函数:HTML转义(防止XSS攻击) |
|
||||
function escapeHtml(str) { |
|
||||
if (!str) return ''; |
|
||||
return str |
|
||||
.replace(/&/g, '&') |
|
||||
.replace(/</g, '<') |
|
||||
.replace(/>/g, '>') |
|
||||
.replace(/"/g, '"') |
|
||||
.replace(/'/g, '''); |
|
||||
} |
|
||||
|
|
||||
// 页面加载时初始化 |
|
||||
window.addEventListener('DOMContentLoaded', initPage); |
|
||||
</script> |
|
||||
|
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, |
||||
|
page: currentPage, |
||||
|
page_size: pageSize, |
||||
|
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> |
</body> |
||||
</html> |
|
||||
|
</html> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue