国内市场双十一活动仓库
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

3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width,initial-scale=1" />
  6. <title>落地页详情</title>
  7. <style>
  8. body, html { margin:0; padding:0; height:100%; font-family: Arial, "Helvetica Neue", "PingFang SC", "Microsoft Yahei"; background:#f4f6f8; color:#333; }
  9. .page-container { width:100%; height:100vh; display:flex; flex-direction:column; }
  10. .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; }
  11. .main-container { display:flex; flex:1; overflow:hidden; }
  12. .sidebar { width:220px; background:#f5f7fa; border-right:1px solid #e8e8e8; padding:16px; box-sizing:border-box; }
  13. .sidebar h3 { margin:0 0 12px 0; font-size:14px; color:#333; }
  14. .content-area { flex:1; padding:20px; overflow:auto; background:#fff; }
  15. .content-header { display:flex; justify-content:flex-end; margin-bottom:15px; padding-bottom:10px; border-bottom:1px solid #e8e8e8; }
  16. .filter-section { margin-bottom:20px; }
  17. .filter-form { display:flex; align-items:center; flex-wrap:wrap; gap:12px; }
  18. .filter-form label { font-size:14px; color:#333; margin-right:6px; }
  19. .time-separator { margin:0 10px; }
  20. .btn { display:inline-block; padding:8px 12px; border-radius:6px; border:0; cursor:pointer; }
  21. .btn-primary { background:#1890ff; color:#fff; }
  22. .btn-plain { background:#fff; border:1px solid #dcdcdc; color:#333; }
  23. table { width:100%; border-collapse:collapse; background:#fff; }
  24. th, td { padding:12px 8px; border-bottom:1px solid #f0f0f0; text-align:center; font-size:14px; }
  25. th { background:#fafafa; font-weight:600; }
  26. .pagination-container { display:flex; justify-content:flex-end; align-items:center; margin-top:10px; gap:8px; }
  27. select, input[type="number"] { padding:6px; border-radius:4px; border:1px solid #dcdcdc; }
  28. .loading { display:inline-block; margin-left:8px; color:#1890ff; font-size:13px; }
  29. .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; }
  30. .msg-success { background:#67c23a; }
  31. .msg-error { background:#f56c6c; }
  32. @media (max-width:700px) { .sidebar { display:none; } .content-area { padding:12px; } }
  33. #msgBoxTop {
  34. position: fixed;
  35. top: 16px; /* 距离顶部距离,可调整 */
  36. left: 50%;
  37. transform: translateX(-50%) translateY(-8px);
  38. z-index: 2147483647; /* 尽可能置顶 */
  39. min-width: 220px;
  40. max-width: 92%;
  41. padding: 10px 16px;
  42. border-radius: 8px;
  43. color: #fff;
  44. box-shadow: 0 8px 28px rgba(0,0,0,0.18);
  45. display: none;
  46. align-items: center;
  47. justify-content: center;
  48. font-size: 14px;
  49. text-align: center;
  50. white-space: pre-wrap;
  51. pointer-events: none; /* 不阻塞下方交互;如果需要可改为 auto */
  52. }
  53. /* 显示时的动画 */
  54. #msgBoxTop.show {
  55. display: flex;
  56. animation: msg-slide-down 220ms cubic-bezier(.2,.9,.2,1);
  57. transform: translateX(-50%) translateY(0);
  58. }
  59. /* 轻微淡出(移除 show 时也会自然消失)*/
  60. @keyframes msg-slide-down {
  61. from { opacity: 0; transform: translateX(-50%) translateY(-8px); }
  62. to { opacity: 1; transform: translateX(-50%) translateY(0); }
  63. }
  64. /* 颜色主题 */
  65. #msgBoxTop.msg-success { background: #67c23a; }
  66. #msgBoxTop.msg-error { background: #f56c6c; }
  67. </style>
  68. </head>
  69. <body>
  70. <div class="page-container">
  71. <div class="top-bar"><span>详情</span></div>
  72. <div class="main-container">
  73. <div class="sidebar">
  74. <h3>管理菜单</h3>
  75. <div>• 落地页管理</div>
  76. </div>
  77. <div class="content-area">
  78. <div class="content-header">
  79. <button id="btnBack" class="btn btn-plain">返回上一级页面</button>
  80. </div>
  81. <div class="filter-section">
  82. <div class="filter-form" role="form" aria-label="筛选">
  83. <div>
  84. <label>打开网页时间:</label>
  85. <input id="filterStart" type="datetime-local" style="width:180px" />
  86. <span class="time-separator"></span>
  87. <input id="filterEnd" type="datetime-local" style="width:180px" />
  88. </div>
  89. <div>
  90. <label>收下状态:</label>
  91. <select id="filterStatus" style="width:150px">
  92. <option value="">全部</option>
  93. <option value="1"></option>
  94. <option value="0"></option>
  95. </select>
  96. </div>
  97. <div>
  98. <button id="btnQuery" class="btn btn-primary">查询</button>
  99. </div>
  100. <div>
  101. <button id="btnExport" class="btn btn-primary btn-plain">导出</button>
  102. </div>
  103. </div>
  104. </div>
  105. <div id="tableWrap" style="min-height:200px">
  106. <table aria-describedby="详情列表">
  107. <thead>
  108. <tr>
  109. <th style="width:80px">序号</th>
  110. <th style="min-width:180px">用户信息</th>
  111. <th style="min-width:180px">打开网页时间</th>
  112. <th style="min-width:120px">收下状态</th>
  113. </tr>
  114. </thead>
  115. <tbody id="tableBody">
  116. <!-- JS 渲染 -->
  117. </tbody>
  118. </table>
  119. <div id="loadingIndicator" style="padding:10px 0; display:none;"><span class="loading">加载中...</span></div>
  120. <div id="noData" style="padding:20px 0; display:none; color:#666; text-align:center;">暂无数据</div>
  121. </div>
  122. <div class="pagination-container" style="margin-top:14px;">
  123. <div><span id="totalCount">0</span></div>
  124. <div style="margin-left:12px;">
  125. 每页
  126. <select id="pageSizeSelect">
  127. <option value="20">20</option>
  128. <option value="50">50</option>
  129. <option value="100">100</option>
  130. </select>
  131. </div>
  132. <div style="margin-left:12px;">
  133. <button id="prevBtn" class="btn">上一页</button>
  134. <span style="margin:0 8px"><span id="currentPage">1</span></span>
  135. <button id="nextBtn" class="btn">下一页</button>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. <!-- 消息提示 -->
  142. <div id="msgBox" class="msg-box"></div>
  143. <script type="module">
  144. import { getLandingDetailApi, exportLandingDetailApi } from './src/api/member.js';;
  145. (function () {
  146. // ====== 状态 ======
  147. let tableData = [];
  148. let currentPage = 1;
  149. let pageSize = 20;
  150. let totalCount = 0;
  151. // ====== DOM ======
  152. const btnBack = document.getElementById('btnBack');
  153. const btnQuery = document.getElementById('btnQuery');
  154. const btnExport = document.getElementById('btnExport');
  155. const filterStart = document.getElementById('filterStart');
  156. const filterEnd = document.getElementById('filterEnd');
  157. const filterStatus = document.getElementById('filterStatus');
  158. const tableBody = document.getElementById('tableBody');
  159. const totalCountEl = document.getElementById('totalCount');
  160. const currentPageEl = document.getElementById('currentPage');
  161. const pageSizeSelect = document.getElementById('pageSizeSelect');
  162. const prevBtn = document.getElementById('prevBtn');
  163. const nextBtn = document.getElementById('nextBtn');
  164. const loadingIndicator = document.getElementById('loadingIndicator');
  165. const noData = document.getElementById('noData');
  166. const msgBox = document.getElementById('msgBox');
  167. function showMessage(text, type = 'success', duration = 3000) {
  168. let el = document.getElementById('msgBoxTop');
  169. if (!el) {
  170. el = document.createElement('div');
  171. el.id = 'msgBoxTop';
  172. el.setAttribute('role', 'status');
  173. el.setAttribute('aria-live', 'polite');
  174. document.body.appendChild(el);
  175. }
  176. // 更新内容与样式
  177. el.textContent = text;
  178. el.classList.remove('msg-success', 'msg-error', 'show');
  179. el.classList.add(type === 'error' ? 'msg-error' : 'msg-success');
  180. // 触发显示(重新触发动画)
  181. // pointer-events: none 允许消息不阻塞下层交互;若想阻塞改为 auto
  182. requestAnimationFrame(() => {
  183. el.classList.add('show');
  184. });
  185. // 清除旧定时器并设置新定时器隐藏
  186. if (el._timer) {
  187. clearTimeout(el._timer);
  188. }
  189. el._timer = setTimeout(() => {
  190. el.classList.remove('show');
  191. // 可选:在动画结束后彻底隐藏(防止 display:flex 仍在)
  192. setTimeout(() => {
  193. if (!el.classList.contains('show')) {
  194. el.style.display = 'none';
  195. // 恢复 display 控制以便再次显示时通过 .show 控制显示
  196. el.style.display = '';
  197. }
  198. }, 240);
  199. }, Math.max(800, duration)); // 最少保留 800ms,避免闪烁
  200. }
  201. function formatDateTimeInput(v) {
  202. // 输入 v: "2025-10-23T14:00" -> 输出 "YYYY-MM-DD HH:mm:ss"
  203. if (!v) return '';
  204. // v may be "2025-10-23T14:00" or "2025-10-23T14:00:00"
  205. let s = v.replace('T', ' ');
  206. if (s.length === 16) s += ':00';
  207. return s;
  208. }
  209. function parseQueryParam(name) {
  210. const sp = new URLSearchParams(location.search);
  211. return sp.get(name);
  212. }
  213. function syncEndMinWithStart() {
  214. if (!filterStart || !filterEnd) return;
  215. // set min attribute on end input so browser picker enforces it
  216. if (filterStart.value) {
  217. filterEnd.min = filterStart.value;
  218. } else {
  219. filterEnd.removeAttribute('min');
  220. }
  221. }
  222. function isTimeRangeValid() {
  223. if (!filterStart.value || !filterEnd.value) return true; // 空值不做强制(按需求可改)
  224. // Compare strings directly is OK for datetime-local "YYYY-MM-DDTHH:MM" format
  225. return filterEnd.value >= filterStart.value;
  226. }
  227. // 绑定事件:开始时间变化时更新 end.min,并修正 end 小于 start 的情况
  228. filterStart && filterStart.addEventListener('change', () => {
  229. syncEndMinWithStart();
  230. if (filterEnd.value && filterEnd.value < filterStart.value) {
  231. // 把结束时间自动设为开始时间,提示用户
  232. filterEnd.value = filterStart.value;
  233. showMessage('结束时间已被调整为不早于开始时间', 'error');
  234. }
  235. });
  236. // 绑定事件:结束时间变化时校验,如果不合法则阻止并提示(并回退到开始时间或清空)
  237. filterEnd && filterEnd.addEventListener('change', () => {
  238. if (!isTimeRangeValid()) {
  239. showMessage('结束时间不能早于开始时间,请调整', 'error');
  240. // 方案:把结束时间重置为开始时间(更直观),也可选择清空
  241. if (filterStart.value) {
  242. filterEnd.value = filterStart.value;
  243. } else {
  244. filterEnd.value = '';
  245. }
  246. }
  247. });
  248. // 构建列表查询参数对象
  249. function buildQueryParams() {
  250. const id = parseQueryParam('id');
  251. const startTime = formatDateTimeInput(filterStart.value);
  252. const endTime = formatDateTimeInput(filterEnd.value);
  253. const state = (filterStatus.value === '' || filterStatus.value === null) ? -1 : filterStatus.value;
  254. return { id, page: currentPage, page_size: pageSize, start_time: startTime, end_time: endTime, state };
  255. }
  256. // 将 params 对象转为查询字符串
  257. function toQueryString(params) {
  258. return Object.keys(params)
  259. .filter(k => params[k] !== undefined && params[k] !== null && params[k] !== '')
  260. .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
  261. .join('&');
  262. }
  263. // ====== 数据请求 ======
  264. async function getLandingDetail() {
  265. const id = parseQueryParam('id');
  266. if (!id) {
  267. showMessage('未获取到活动ID,无法加载详情', 'error');
  268. return;
  269. }
  270. loadingIndicator.style.display = 'inline-block';
  271. tableBody.innerHTML = '';
  272. noData.style.display = 'none';
  273. try {
  274. const res = await getLandingDetailApi({
  275. id: id,
  276. page: currentPage,
  277. page_size: pageSize,
  278. start_time: formatDateTimeInput(filterStart.value),
  279. end_time: formatDateTimeInput(filterEnd.value),
  280. state: (filterStatus.value === '' || filterStatus.value === null) ? -1 : filterStatus.value
  281. });
  282. if(res.code == 200){
  283. tableData = res.data.list;
  284. totalCount = res.data.total ;
  285. renderTable();
  286. } else {
  287. showMessage('获取活动详情失败', 'error');
  288. }
  289. } catch (err) {
  290. console.error(err);
  291. showMessage('网络异常,无法加载数据', 'error');
  292. } finally {
  293. loadingIndicator.style.display = 'none';
  294. }
  295. }
  296. function renderTable() {
  297. tableBody.innerHTML = '';
  298. if (!tableData || tableData.length === 0) {
  299. noData.style.display = 'block';
  300. return
  301. } else {
  302. noData.style.display = 'none';
  303. }
  304. tableData.forEach((row, idx) => {
  305. const tr = document.createElement('tr');
  306. const tdIndex = document.createElement('td');
  307. tdIndex.textContent = (currentPage - 1) * pageSize + idx + 1;
  308. tr.appendChild(tdIndex);
  309. const tdUser = document.createElement('td');
  310. tdUser.textContent = row.user_info || '';
  311. tr.appendChild(tdUser);
  312. const tdCreated = document.createElement('td');
  313. tdCreated.textContent = row.created_at || '';
  314. tr.appendChild(tdCreated);
  315. const tdState = document.createElement('td');
  316. tdState.textContent = row.state === 1 ? '是' : '否';
  317. tr.appendChild(tdState);
  318. tableBody.appendChild(tr);
  319. });
  320. totalCountEl.textContent = totalCount;
  321. currentPageEl.textContent = currentPage;
  322. }
  323. // ====== 导出功能 ======
  324. async function handleExport() {
  325. const id = parseQueryParam('id');
  326. if (!id) { showMessage('未获取到活动ID,无法导出', 'error'); return; }
  327. loadingIndicator.style.display = 'inline-block';
  328. try {
  329. const res = await exportLandingDetailApi({
  330. id: id,
  331. start_time: formatDateTimeInput(filterStart.value),
  332. end_time: formatDateTimeInput(filterEnd.value),
  333. state: (filterStatus.value === '' || filterStatus.value === null) ? -1 : filterStatus.value
  334. });
  335. const data = res.data !== undefined ? res.data : res;
  336. const blob = new Blob([data], { type: res.headers?.['content-type'] || 'application/octet-stream' });
  337. let filename = `活动数据.xlsx`;
  338. const blobUrl = URL.createObjectURL(blob);
  339. const a = document.createElement('a');
  340. a.href = blobUrl;
  341. a.download = filename;
  342. document.body.appendChild(a);
  343. a.click();
  344. a.remove();
  345. URL.revokeObjectURL(blobUrl);
  346. showMessage('导出成功');
  347. } catch (err) {
  348. console.error(err);
  349. showMessage('导出失败,请重试', 'error');
  350. } finally {
  351. loadingIndicator.style.display = 'none';
  352. }
  353. }
  354. // ====== 分页事件 ======
  355. prevBtn.addEventListener('click', () => {
  356. if (currentPage > 1) {
  357. currentPage--;
  358. getLandingDetail();
  359. }
  360. });
  361. nextBtn.addEventListener('click', () => {
  362. const maxPage = Math.max(1, Math.ceil((totalCount || 1) / pageSize));
  363. if (currentPage < maxPage) {
  364. currentPage++;
  365. getLandingDetail();
  366. }
  367. });
  368. pageSizeSelect.addEventListener('change', (e) => {
  369. pageSize = Number(e.target.value);
  370. currentPage = 1;
  371. getLandingDetail();
  372. });
  373. // ====== 交互事件 ======
  374. btnBack.addEventListener('click', () => {
  375. history.back();
  376. });
  377. btnQuery.addEventListener('click', () => {
  378. currentPage = 1;
  379. getLandingDetail();
  380. });
  381. btnExport.addEventListener('click', () => {
  382. handleExport();
  383. });
  384. // 页面加载时读取 id 并请求数据
  385. (function init() {
  386. const id = parseQueryParam('id');
  387. if (!id) {
  388. showMessage('缺少活动ID,请在 URL 里添加 ?id=xxx', 'error');
  389. }
  390. // use defaults from HTML (pageSizeSelect initial value)
  391. pageSize = Number(pageSizeSelect.value || 20);
  392. getLandingDetail();
  393. })();
  394. })();
  395. </script>
  396. </body>
  397. </html>