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

590 lines
23 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>
/* ---- 基本布局(源自你的 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>