|
|
<!doctype html><html lang="zh-CN"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>落地页管理</title> <style> /* ---- 基本布局(源自你的 vue 样式) ---- */ 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-color:#1890ff; color:#fff; padding:0 20px; display:flex; align-items:center; font-size:18px; font-weight:500; box-shadow:0 2px 4px rgba(0,0,0,0.1); z-index:10; } .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:space-between; align-items:center; margin-bottom:20px; padding-bottom:10px; border-bottom:1px solid #e8e8e8; } .btn { display:inline-block; padding:8px 12px; border-radius:6px; border:0; cursor:pointer; } .btn-primary { background:#1890ff; color:#fff; } .btn-text { background:transparent; color:#1890ff; border:1px solid transparent; } 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; } /* 弹窗(模态) */ .modal-mask { position:fixed; left:0; top:0; right:0; bottom:0; background:rgba(0,0,0,0.4); display:none; align-items:center; justify-content:center; z-index:999; } .modal { width:620px; background:#fff; border-radius:6px; padding:18px; box-shadow:0 8px 24px rgba(0,0,0,0.2); max-height:90vh; overflow:auto; } .form-row { display:flex; gap:12px; margin-bottom:12px; align-items:center; } .form-row label { width:110px; text-align:right; margin-right:8px; font-size:14px; color:#333; } .form-row .field { flex:1; } input[type="text"], textarea, input[type="datetime-local"] { width:100%; padding:8px; border:1px solid #dcdcdc; border-radius:4px; box-sizing:border-box; } textarea { min-height:70px; resize:vertical; } .upload-tip, .intro-tip { font-size:12px; color:#999; margin-top:6px; } .dialog-footer { display:flex; justify-content:flex-end; gap:8px; padding-top:8px; border-top:1px solid #f0f0f0; margin-top:10px; } .img-preview { display:inline-block; max-width:120px; max-height:80px; margin-right:8px; vertical-align:middle; border:1px solid #eee; padding:4px; border-radius:4px; } .loading { display:inline-block; margin-left:8px; color:#1890ff; font-size:13px; } .actions button { margin-right:6px; } #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"> <div> <strong>活动列表</strong> </div> <div> <button id="btnAdd" class="btn btn-primary">+ 新增落地页</button> </div> </div>
<div id="tableWrap"> <table id="landingTable" aria-describedby="活动列表"> <thead> <tr> <th style="width:80px">序号</th> <th>活动名称</th> <th style="min-width:220px">活动简介</th> <th style="min-width:220px">活动时间</th> <th style="min-width:180px">编辑时间</th> <th style="width:280px">操作</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="10">10</option> <option value="20">20</option> <option value="50">50</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 style="margin-left:12px;">
跳到 <input id="gotoInput" type="number" min="1" style="width:70px" /> 页 <button id="gotoBtn" class="btn">跳转</button> </div> --> </div>
<!-- 模态框:新增 / 编辑 --> <div id="modalMask" class="modal-mask" role="dialog" aria-modal="true"> <div class="modal" role="document"> <h3 id="modalTitle">添加活动</h3>
<div style="padding-top:8px;"> <div class="form-row"> <label>活动名称</label> <div class="field"><input id="fldName" type="text" /></div> </div>
<div class="form-row"> <label>活动简介</label> <div class="field"> <textarea id="fldIntro" maxlength="80" placeholder="活动简介(用于分享展示)"></textarea> <div class="intro-tip">活动简介会给分享后客户展示,请注意填写内容(最多80字)。</div> </div> </div>
<div class="form-row"> <label>活动时间</label> <div class="field"> <input id="fldStart" type="datetime-local" style="width:200px" /> <span style="margin:0 8px">至</span> <input id="fldEnd" type="datetime-local" style="width:200px" /> </div> </div>
<div class="form-row"> <label>活动落地页</label> <div class="field"> <div id="landingPreviewContainer" style="margin-top:8px;"></div> <input id="inputLanding" type="file" accept="image/*" /> <div class="upload-tip">支持 PNG / JPG / GIF。</div> </div> </div>
<div class="form-row"> <label>落地页弹窗</label> <div class="field"> <div id="popupPreviewContainer" style="margin-top:8px;"></div> <input id="inputPopup" type="file" accept="image/*" /> <div class="upload-tip">支持 PNG / JPG / GIF。</div> </div> </div> </div>
<div class="dialog-footer"> <button id="cancelBtn" class="btn">取消</button> <button id="saveBtn" class="btn btn-primary">确定</button> </div> </div> </div>
</div> </div> </div>
<script type="module"> import { getLandingListApi, addLandingApi } from './src/api/member.js';(function () { const FILE_UPLOAD_URL = 'https://tjapi.hlquant.com/hljwgo/api/file/upload'; // 状态 let tableData = []; let currentPage = 1; let pageSize = 10; let totalCount = 0;
// 上传图片临时状态 let landingFile = null; let popupFile = null; let landingUploadedUrl = ''; // 上传后得到的 URL(或编辑模式中已有的 url) let popupUploadedUrl = '';
// 编辑上下文 let editId = null;
// DOM 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 gotoInput = document.getElementById('gotoInput'); // const gotoBtn = document.getElementById('gotoBtn'); const btnAdd = document.getElementById('btnAdd'); const modalMask = document.getElementById('modalMask'); const modalTitle = document.getElementById('modalTitle'); const cancelBtn = document.getElementById('cancelBtn'); const saveBtn = document.getElementById('saveBtn'); const fldName = document.getElementById('fldName'); const fldIntro = document.getElementById('fldIntro'); const fldStart = document.getElementById('fldStart'); const fldEnd = document.getElementById('fldEnd'); const inputLanding = document.getElementById('inputLanding'); const inputPopup = document.getElementById('inputPopup'); const landingPreviewContainer = document.getElementById('landingPreviewContainer'); const popupPreviewContainer = document.getElementById('popupPreviewContainer'); const loadingIndicator = document.getElementById('loadingIndicator'); const noData = document.getElementById('noData'); // 工具:显示消息(简单替代 ElMessage)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,避免闪烁} // 列表加载 async function getLandingList() { loadingIndicator.style.display = 'inline-block'; noData.style.display = 'none'; tableBody.innerHTML = ''; try { // 如果你已有后端接口,解除下面注释并使用真实接口(带 page & pageSize) const res =await getLandingListApi({ page: currentPage, page_size: pageSize }) // const json = await res.json(); if (res.code == 200 && res.data) { tableData = res.data.list totalCount = res.data.total renderTable(); } else { showMessage('获取数据失败', 'error'); } } catch (err) { console.error(err); showMessage('网络异常,请稍后重试', 'error'); } finally { loadingIndicator.style.display = 'none'; } }
// 更严格的判断(使用 Date) function isDialogTimeRangeValid() { if (!fldStart.value || !fldEnd.value) return true; // 有空值不强制 const s = new Date(fldStart.value); const e = new Date(fldEnd.value); return e >= s; } function renderTable() { tableBody.innerHTML = ''; if (!tableData || tableData.length === 0) { noData.style.display = 'block'; } 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 tdName = document.createElement('td'); tdName.textContent = row.name || ''; tr.appendChild(tdName);
const tdIntro = document.createElement('td'); tdIntro.textContent = row.introduction || ''; tr.appendChild(tdIntro);
const tdTime = document.createElement('td'); tdTime.textContent = (row.start_time || '') + ' - ' + (row.end_time || ''); tr.appendChild(tdTime);
const tdUpdated = document.createElement('td'); tdUpdated.textContent = row.updated_at || ''; tr.appendChild(tdUpdated);
const tdOps = document.createElement('td'); tdOps.className = 'actions'; const btnEdit = document.createElement('button'); btnEdit.className = 'btn btn-text'; btnEdit.textContent = '编辑'; btnEdit.addEventListener('click', () => openEditDialog(row)); tdOps.appendChild(btnEdit);
const btnDetail = document.createElement('button'); btnDetail.className = 'btn'; btnDetail.textContent = '详情'; btnDetail.addEventListener('click', () => { window.location.href = `/parkActivity/adminDetail.html?id=${row.id}`; }); tdOps.appendChild(btnDetail);
tr.appendChild(tdOps);
tableBody.appendChild(tr); });
totalCountEl.textContent = totalCount; currentPageEl.textContent = currentPage; }
// 分页事件 prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; getLandingList(); } }); nextBtn.addEventListener('click', () => { // 简单判断:如果能跳到下一页,则允许(若你有 totalCount 可严格限制) const maxPage = Math.max(1, Math.ceil((totalCount || 1) / pageSize)); if (currentPage < maxPage) { currentPage++; getLandingList(); } }); pageSizeSelect.addEventListener('change', (e) => { pageSize = Number(e.target.value); currentPage = 1; getLandingList(); }); // 打开新增弹窗 btnAdd.addEventListener('click', () => { openAddDialog(); });
// 弹窗打开 / 关闭 function openAddDialog() { editId = null; modalTitle.textContent = '添加活动'; fldName.value = ''; fldIntro.value = ''; fldStart.value = ''; fldEnd.value = ''; landingFile = null; popupFile = null; landingUploadedUrl = ''; popupUploadedUrl = ''; landingPreviewContainer.innerHTML = ''; popupPreviewContainer.innerHTML = ''; inputLanding.value = ''; inputPopup.value = ''; showModal(); }
function openEditDialog(row) { editId = row.id; modalTitle.textContent = '编辑活动'; fldName.value = row.name || ''; fldIntro.value = row.introduction || ''; // 尝试解析 row.start_time / end_time 为 datetime-local 格式(YYYY-MM-DDTHH:MM) fldStart.value = toInputDatetime(row.start_time); fldEnd.value = toInputDatetime(row.end_time); landingUploadedUrl = row.landing_page || ''; popupUploadedUrl = row.landing_page_popup || ''; landingPreviewContainer.innerHTML = landingUploadedUrl ? `<img class="img-preview" src="${landingUploadedUrl}" alt="landing"> <button data-remove="landing">删除</button>` : ''; popupPreviewContainer.innerHTML = popupUploadedUrl ? `<img class="img-preview" src="${popupUploadedUrl}" alt="popup"> <button data-remove="popup">删除</button>` : ''; showModal(); }
function toInputDatetime(str) { if (!str) return ''; // 如果 str 是 "2025-11-01 00:00" 或带秒,替换空格为 T // 如果已经是 ISO 可直接 substring const s = str.replace(' ', 'T'); // 截取到分钟 return s.length >= 16 ? s.substring(0,16) : s; }
function showModal() { modalMask.style.display = 'flex'; } function closeModal() { modalMask.style.display = 'none'; } cancelBtn.addEventListener('click', () => { closeModal(); });
// 删除预览的图片(编辑时的删除) landingPreviewContainer.addEventListener('click', (e) => { if (e.target && e.target.dataset.remove === 'landing') { landingUploadedUrl = ''; landingFile = null; landingPreviewContainer.innerHTML = ''; } }); popupPreviewContainer.addEventListener('click', (e) => { if (e.target && e.target.dataset.remove === 'popup') { popupUploadedUrl = ''; popupFile = null; popupPreviewContainer.innerHTML = ''; } });
// 文件选择处理(预览 + 校验) inputLanding.addEventListener('change', (e) => { const f = e.target.files[0]; if (!f) return; if (f.size > 2 * 1024 * 1024) { showMessage('图片大小不能超过 2MB', 'error'); e.target.value = ''; if (typeof landingFile !== 'undefined') landingFile = null; if (landingPreviewContainer) landingPreviewContainer.innerHTML = ''; return; } const reader = new FileReader(); reader.onload = function (ev) { const img = new Image(); img.onload = function () { // if (img.width > 375) { // showMessage('落地页图片宽度应 ≤ 375px,请使用合适宽度的图片。', 'error'); // inputLanding.value = ''; // landingFile = null; // landingPreviewContainer.innerHTML = ''; // return; // } landingFile = f; landingPreviewContainer.innerHTML = ''; const imgEl = document.createElement('img'); imgEl.src = ev.target.result; imgEl.className = 'img-preview'; landingPreviewContainer.appendChild(imgEl); }; img.src = ev.target.result; }; reader.readAsDataURL(f); });
inputPopup.addEventListener('change', (e) => { const f = e.target.files[0]; if (!f) return; if (f.size > 2 * 1024 * 1024) { showMessage('图片大小不能超过 2MB', 'error'); e.target.value = ''; if (typeof popupFile !== 'undefined') popupFile = null; if (popupPreviewContainer) popupPreviewContainer.innerHTML = ''; return; } const reader = new FileReader(); reader.onload = function (ev) { popupFile = f; popupPreviewContainer.innerHTML = ''; const imgEl = document.createElement('img'); imgEl.src = ev.target.result; imgEl.className = 'img-preview'; popupPreviewContainer.appendChild(imgEl); }; reader.readAsDataURL(f); });
saveBtn.addEventListener('click', async () => { // 简单校验 const name = fldName.value.trim(); const introduction = fldIntro.value.trim(); const startTime = fldStart.value; const endTime = fldEnd.value; if (!name) { showMessage('请输入活动名称', 'error'); return; } if (!introduction) { showMessage('请输入活动简介', 'error'); return; } if (!startTime) { showMessage('请选择开始时间', 'error'); return; } if (!endTime) { showMessage('请选择结束时间', 'error'); return; } if (!isDialogTimeRangeValid()) { showMessage('结束时间不能早于开始时间,请调整后保存', 'error'); return; } // 如果既没有已上传的 url,也没有本地文件,则提示 if (!landingUploadedUrl && !landingFile) { showMessage('请上传活动落地页', 'error'); return; } if (!popupUploadedUrl && !popupFile) { showMessage('请上传落地页弹窗', 'error'); return; }
saveBtn.disabled = true; saveBtn.textContent = '提交中...';
try { // 1) 若用户选择了新的图片文件则先上传图片 if (landingFile) { const uploaded = await uploadFile(landingFile); if (!uploaded.success) { showMessage('落地页图片上传失败', 'error'); saveBtn.disabled = false; saveBtn.textContent = '确定'; return; } landingUploadedUrl = uploaded.url; } if (popupFile) { const uploaded = await uploadFile(popupFile); if (!uploaded.success) { showMessage('弹窗图片上传失败', 'error'); saveBtn.disabled = false; saveBtn.textContent = '确定'; return; } popupUploadedUrl = uploaded.url; }
const res = await addLandingApi({ id: editId, name:name, introduction:introduction, start_time: formatDatetimeLocal(startTime), end_time: formatDatetimeLocal(endTime), landing_page: landingUploadedUrl, landing_page_popup: popupUploadedUrl }) if (res.code == 200) { showMessage('操作成功'); closeModal(); getLandingList(); } else { showMessage(json.msg || '操作失败', 'error'); }
} catch (err) { console.error(err); showMessage('网络异常', 'error'); } finally { saveBtn.disabled = false; saveBtn.textContent = '确定'; } });
async function uploadFile(file) { const fd = new FormData(); fd.append('file', file); fd.append('type', 'image'); fd.append('app_from', 'toujiao'); try { const resp = await fetch(FILE_UPLOAD_URL, { method: 'POST', body: fd }); const json = await resp.json(); if (json && ((json.code && json.code === 200) || json.data)) { const url = (json.data && (json.data.url || json.data.file_name)) || json.url || ''; return { success: true, url: url }; } else { return { success: false }; } } catch (err) { console.error('uploadFile error', err); return { success: false }; } }
function formatDatetimeLocal(v) { // v like "2025-10-23T14:00" -> "2025-10-23 14:00" return v ? v.replace('T', ' ') : ''; }
// 初始加载 getLandingList();
})();</script></body></html>
|