|
|
<!DOCTYPE html><html lang="zh-CN">
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>管理后台 · Adam说港股导流</title> <style> .tab-container { max-width: 1200px; margin: 0 0 12px 0; padding: 0 16px; }
.tabs { display: flex; gap: 50px; padding: 4px; border-radius: 8px; width: fit-content; }
.tab { padding: 10px 20px; border-radius: 6px; border: none; background: transparent; color: #131212; font-weight: 700; cursor: pointer; transition: all 0.3s ease; font-size: 16px; white-space: nowrap; }
.tab:hover { background: rgba(46, 125, 50, 0.1); color: #25D366; }
.tab.active { background: #BAF7D0; color: #0E4322; box-shadow: 0 2px 4px rgba(46, 125, 50, 0.3); } * { box-sizing: border-box; }
body { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; padding: 24px; background: #f0f2f6; color: #1e293b; margin: 0; }
.card { background: #ffffff; padding: 20px 16px 24px 16px; border-radius: 20px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05); max-width: 100%; overflow-x: auto; }
/* 表格容器实现横向滚动(移动端友好),同时保证单元格内容溢出省略 */ .table-wrapper { overflow-x: auto; border-radius: 14px; margin-top: 8px; }
table { width: 100%; border-collapse: collapse; min-width: 1000px; font-size: 13.5px; }
th, td { padding: 12px 10px; border-bottom: 1px solid #e9edf2; text-align: center; vertical-align: middle; max-width: 0; /* 辅助溢出省略 */ }
/* 表头固定宽度 + 允许文字折行但优先省略 */ th { background: #f8fafc; font-weight: 600; color: #1e293b; letter-spacing: 0.3px; white-space: nowrap; }
/* 单元格内容超出宽度后隐藏文字,鼠标悬浮显示完整内容 */ td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; transition: background 0.1s; }
/* 针对特定列单独控制最大宽度,确保不超出表头宽度 */ td:nth-child(1) { max-width: 55px; }
/* 姓名 */ td:nth-child(2) { max-width: 100px; }
/* WhatsApp按钮列 */ td:nth-child(3) { max-width: 100px; overflow: visible; white-space: normal; }
/* 国家代码 */ td:nth-child(4) { max-width: 90px; }
/* 电话号码 */ td:nth-child(5) { max-width: 120px; }
/* 微信ID */ td:nth-child(6) { max-width: 120px; }
/* 邮箱 */ td:nth-child(7) { max-width: 150px; }
/* 反馈内容 */ td:nth-child(8) { max-width: 120px; }
/* 反馈类型 */ td:nth-child(9) { max-width: 100px; }
/* 添加时间 */ td:nth-child(10) { max-width: 160px; }
/* 是否联系 */ td:nth-child(11) { max-width: 100px; white-space: normal; }
/* 备注 */ td:nth-child(12) { max-width: 160px; }
/* 操作 */ td:nth-child(13) { max-width: 100px; white-space: normal; }
/* 悬浮显示完整内容(自定义title特性,但使用全局title亦可,增强体验)*/ td[title] { cursor: help; }
.controls { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; margin-top: 20px; background: #ffffff; padding: 8px 4px; }
.pagination { display: flex; gap: 6px; align-items: center; margin-left: auto; flex-wrap: wrap; }
.btn { padding: 6px 12px; border-radius: 30px; border: 1px solid #d4d9e2; background: #fff; cursor: pointer; font-size: 12.5px; font-weight: 500; transition: all 0.2s ease; }
.btn:hover:not(:disabled) { background-color: #f1f5f9; border-color: #94a3b8; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.primary { background: #2563eb; color: white; border-color: #2563eb; }
.btn.primary:hover:not(:disabled) { background: #1d4ed8; }
.btn.whatsapp { background: #25D366; color: #fff; border-color: #1da15a; }
.btn.whatsapp:hover { background: #20b859; }
.status-btn { padding: 5px 12px; border-radius: 24px; border: none; font-weight: 500; cursor: pointer; font-size: 12px; transition: 0.1s; }
.status-0 { background: #fee2e2; color: #b91c1c; border: 1px solid #fecaca; }
.status-1 { background: #dcfce7; color: #15803d; border: 1px solid #bbf7d0; }
.btn.active { background: #2563eb; color: white; border-color: #2563eb; }
select, input[type="number"] { padding: 6px 12px; border-radius: 30px; border: 1px solid #cbd5e1; background: white; font-size: 13px; }
.small { font-size: 13px; color: #475569; }
@media (max-width: 720px) { .controls { flex-direction: column; align-items: stretch; }
.pagination { margin-left: 0; justify-content: center; } }
/* 备注模态框 */ #noteModal { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(3px); align-items: center; justify-content: center; z-index: 10000; }
#noteModal .dialog { background: #fff; padding: 24px; border-radius: 28px; width: 90%; max-width: 520px; box-shadow: 0 25px 40px rgba(0, 0, 0, 0.2); }
#noteModal textarea { width: 100%; min-height: 130px; padding: 12px; border-radius: 18px; border: 1px solid #cbd5e1; font-size: 14px; font-family: inherit; resize: vertical; }
.toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%) translateY(-30px); background: #1e293b; color: white; padding: 10px 20px; border-radius: 60px; font-size: 14px; font-weight: 500; opacity: 0; transition: all 0.2s ease; z-index: 11000; pointer-events: none; box-shadow: 0 6px 14px rgba(0, 0, 0, 0.1); }
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* 登录遮罩 */ #loginCheckModal { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); align-items: center; justify-content: center; z-index: 99999; }
#loginCheckModal .dialog { background: #fff; padding: 32px; border-radius: 32px; width: 90%; max-width: 380px; text-align: center; }
#loginCheckModal h3 { margin: 0 0 12px; color: #e11d48; } </style></head>
<body>
<div id="loginCheckModal"> <div class="dialog"> <h3>🔐 访问被拒绝</h3> <p>您尚未登录或登录已过期,请重新登录系统。</p> <button class="btn primary" id="goToLoginBtn">前往登录页面</button> </div> </div> <div class="tab-container"> <div class="tabs"> <button class="tab active" data-tab="win-us-stock">Adam说港股导流</button> </div> </div> <div class="card"> <div class="table-wrapper"> <table aria-describedby="成员反馈列表"> <thead> <tr> <th style="width: 50px">序号</th> <th style="width: 100px">姓名</th> <th style="width: 100px">WhatsApp</th> <th style="width: 90px">国家/地区代码</th> <th style="width: 120px">电话号码</th> <th style="width: 120px">微信ID</th> <th style="width: 150px">邮箱</th> <th style="width: 130px">反馈内容</th> <th style="width: 100px">反馈类型</th> <th style="width: 160px">添加时间</th> <th style="width: 100px">是否联系</th> <th style="width: 160px">备注</th> <th style="width: 100px">操作</th> </tr> </thead> <tbody id="tableBody"></tbody> </table> </div>
<div class="controls"> <div class="small"> 每页显示 <select id="pageSizeSelect"> <option value="20" selected>20</option> <option value="50">50</option> <option value="100">100</option> <option value="200">200</option> </select> 条 </div> <div class="small">共 <span id="totalCount">0</span> 条</div> <div class="pagination" id="pagination"></div> </div> </div>
<div id="noteModal"> <div class="dialog"> <h3 style="margin-top: 0;">✏️ 编辑备注</h3> <textarea id="noteTextarea" rows="4" placeholder="输入备注信息..."></textarea> <div style="margin-top: 20px; display: flex; justify-content: flex-end; gap: 12px;"> <button class="btn" id="noteCancelBtn">取消</button> <button class="btn primary" id="noteSaveBtn">保存</button> </div> </div> </div> <div id="toast" class="toast"></div>
<script type="module"> import { getMemberListApi, updateMemberStateApi, editMemberNoteApi } from './src/api/hkmember.js'
// ---------- 登录验证 ---------- function checkLoginStatus() { const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; const loginTime = parseInt(localStorage.getItem('loginTime'), 10); if (!isLoggedIn || isNaN(loginTime)) return false; const hoursDiff = (new Date().getTime() - loginTime) / (1000 * 60 * 60); return hoursDiff < 24; }
function showLoginCheckModal() { const modal = document.getElementById('loginCheckModal'); if (modal) modal.style.display = 'flex'; } function hideLoginCheckModal() { const modal = document.getElementById('loginCheckModal'); if (modal) modal.style.display = 'none'; } function redirectToLogin() { window.location.href = 'hkLogiManagement.html'; }
// 全局前置守卫:如果未登录则显示弹窗,返回false function requireAuth() { if (!checkLoginStatus()) { showLoginCheckModal(); return false; } return true; }
document.getElementById('goToLoginBtn')?.addEventListener('click', redirectToLogin);
// ---------- 映射反馈类型文字 ---------- function formatFeedbackType(typeValue) { // 后端返回可能是字符串或数字 "1", 2, 3, 4 const val = parseInt(typeValue, 10); switch (val) { case 1: return '功能建议'; case 2: return '问题反馈'; case 3: return '体验优化'; case 4: return '其他建议'; default: return typeValue || '—'; } }
let state = { pageSize: 20, currentPage: 1, total: 0, items: [] };
const tableBody = document.getElementById("tableBody"); const paginationEl = document.getElementById("pagination"); const totalCountEl = document.getElementById("totalCount"); const pageSizeSelect = document.getElementById("pageSizeSelect");
const noteModal = document.getElementById("noteModal"); const noteTextarea = document.getElementById("noteTextarea"); const noteCancelBtn = document.getElementById("noteCancelBtn"); const noteSaveBtn = document.getElementById("noteSaveBtn"); const toastEl = document.getElementById("toast");
let editingRowId = null;
function showToast(msg, isError = false) { toastEl.textContent = msg; toastEl.style.background = isError ? '#e11d48' : '#1e293b'; toastEl.classList.add("show"); setTimeout(() => { toastEl.classList.remove("show"); toastEl.style.background = '#1e293b'; }, 1800); }
// 获取数据 async function fetchPage(page, pageSize) { if (!requireAuth()) return { items: [], total: 0 };
try { const res = await getMemberListApi({ page: page, page_size: pageSize, }); if (res && typeof res.code !== "undefined" && res.code !== 200) { throw new Error(res.msg || "接口异常"); } const payload = res?.data || {}; state.items = payload.list || []; state.total = payload.total || 0; return { items: state.items, total: state.total }; } catch (err) { console.error(err); showToast("获取数据失败: " + (err.message || '网络错误'), true); state.items = []; state.total = 0; return { items: [], total: 0 }; } }
// 辅助函数:给td增加title悬浮完整内容(自动处理每个单元格) function applyTitleToCells() { const rows = tableBody.querySelectorAll('tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach(cell => { // 跳过按钮列(第三列WhatsApp按钮,第11列状态按钮,第13列操作按钮)避免title干扰 const isButtonCell = cell.querySelector('button') && (cell.previousElementSibling?.innerText?.includes('WhatsApp') || cell.querySelector('.status-btn') || cell.querySelector('[data-action="editNote"]')); if (!isButtonCell && cell.innerText && cell.innerText.trim() !== '') { const rawText = cell.innerText; if (!cell.hasAttribute('data-has-title')) { cell.setAttribute('title', rawText); cell.setAttribute('data-has-title', 'true'); } else if (cell.getAttribute('title') !== rawText) { cell.setAttribute('title', rawText); } } else if (!isButtonCell && cell.innerText === '') { cell.removeAttribute('title'); } }); }); }
// 渲染表格(带溢出省略 + 悬浮显示完整内容) function renderTable() { const startIndex = (state.currentPage - 1) * state.pageSize; const items = state.items || [];
tableBody.innerHTML = items.map((item, idx) => { const serial = startIndex + idx + 1; const statusClass = item.isRelated ? "status-1" : "status-0"; const statusText = item.isRelated ? "已联系" : "未联系"; // 反馈类型文字转换 const feedbackTypeText = formatFeedbackType(item.feedback_type);
// 构建WhatsApp链接 (清理+号) const cleanCode = (item.code || '').replace(/\+/g, ''); const whatsappPhone = cleanCode + (item.telephone || ''); const whatsappUrl = `https://wa.me/85269518757?phone=${encodeURIComponent(whatsappPhone)}&text=${encodeURIComponent('hello欢迎来到Adam说港股')}`;
// 安全转义 const name = escapeHtml(item.name || ""); const code = escapeHtml(item.code || ""); const telephone = escapeHtml(item.telephone || ""); const wechat = escapeHtml(item.wechat || ""); const email = escapeHtml(item.email || ""); const feedbackContent = escapeHtml(item.feedback_content || ""); const createdAt = escapeHtml(item.createdAt || ""); const note = escapeHtml(item.note || "");
// 注意:表格结构保持13列,反馈类型用转换后的文字 return ` <tr> <td title="${serial}">${serial}</td> <td title="${name}">${name}</td> <td style="overflow: visible; white-space: normal;"> <button class="btn whatsapp" data-action="whatsapp" data-id="${item.id}">💬 WhatsApp</button> </td> <td title="${code}">${code}</td> <td title="${telephone}">${telephone}</td> <td title="${wechat}">${wechat}</td> <td title="${email}">${email}</td> <td title="${feedbackContent}">${feedbackContent}</td> <td title="${feedbackTypeText}">${feedbackTypeText}</td> <td title="${createdAt}">${createdAt}</td> <td style="white-space: normal;"> <button class="status-btn ${statusClass}" data-action="toggle" data-id="${item.id}">${statusText}</button> </td> <td title="${note}">${note}</td> <td> <button class="btn" data-action="editNote" data-id="${item.id}" style="font-size:12px;">✎ 备注</button> </td> </tr> `; }).join("");
// 为所有普通单元格添加完整文本悬浮(动态渲染后) const rows = tableBody.querySelectorAll('tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach(cell => { // 跳过按钮占位列(第三列、操作列以及状态列,这些列已经有点击元素,不干扰title) const hasImportantBtn = cell.querySelector('[data-action="whatsapp"]') || cell.querySelector('[data-action="toggle"]') || cell.querySelector('[data-action="editNote"]'); if (!hasImportantBtn) { const txt = cell.innerText.trim(); if (txt) cell.setAttribute('title', txt); else cell.removeAttribute('title'); } else if (cell.querySelector('[data-action="toggle"]')) { // 状态按钮列保留其自身文字悬浮展示状态文字 const statusBtn = cell.querySelector('.status-btn'); if (statusBtn) cell.setAttribute('title', statusBtn.innerText); } else if (cell.querySelector('[data-action="editNote"]')) { // 操作列一般不需要悬浮,但为了统一,展示当前按钮文字 const btnText = cell.innerText.trim(); if (btnText) cell.setAttribute('title', btnText); } }); }); totalCountEl.textContent = state.total; }
// 事件监听(委托) tableBody.addEventListener("click", async (e) => { if (!requireAuth()) return;
const whatsappBtn = e.target.closest('[data-action="whatsapp"]'); if (whatsappBtn) { const id = whatsappBtn.getAttribute("data-id"); handleWhatsApp(id); return; }
const toggler = e.target.closest('[data-action="toggle"]'); if (toggler) { const id = toggler.getAttribute("data-id"); await handleToggle(id, toggler); return; }
const editBtn = e.target.closest('[data-action="editNote"]'); if (editBtn) { const id = editBtn.getAttribute("data-id"); openNoteModal(id); return; } });
function handleWhatsApp(id) { const item = state.items.find((it) => String(it.id) === String(id)); if (!item) return; const cleanCode = (item.code || '').replace(/\+/g, ''); const whatsappPhone = cleanCode + (item.telephone || ''); if (!whatsappPhone) { showToast("电话号码无效", true); return; } const whatsappUrl = `https://wa.me/85269518757?phone=${encodeURIComponent(whatsappPhone)}&text=${encodeURIComponent('hello欢迎来到Adam说港股')}`; window.open(whatsappUrl, '_blank'); }
async function handleToggle(id, btnEl) { const item = state.items.find((it) => String(it.id) === String(id)); if (!item) return; const prev = item.isRelated; const newVal = prev ? 0 : 1; // 乐观更新UI item.isRelated = newVal; btnEl.textContent = newVal ? "已联系" : "未联系"; btnEl.classList.toggle("status-1", !!newVal); btnEl.classList.toggle("status-0", !newVal); try { const res = await updateMemberStateApi({ id: item.id, state: newVal }); if (res && typeof res.code !== "undefined" && res.code !== 200) { throw new Error(res.msg || "更新失败"); } showToast("状态已更新"); } catch (err) { // 回滚 item.isRelated = prev; btnEl.textContent = prev ? "已联系" : "未联系"; btnEl.classList.toggle("status-1", !!prev); btnEl.classList.toggle("status-0", !prev); showToast("更新失败: " + (err.message || '网络错误'), true); } }
function openNoteModal(id) { if (!requireAuth()) return; const item = state.items.find((it) => String(it.id) === String(id)); if (!item) return; editingRowId = id; noteTextarea.value = item.note || ""; noteModal.style.display = "flex"; noteTextarea.focus(); }
function closeNoteModal() { editingRowId = null; noteTextarea.value = ""; noteModal.style.display = "none"; }
noteCancelBtn.addEventListener("click", closeNoteModal); noteSaveBtn.addEventListener("click", async () => { if (!requireAuth()) return; if (!editingRowId) return closeNoteModal(); const newNote = noteTextarea.value.trim(); const item = state.items.find((it) => String(it.id) === String(editingRowId)); if (!item) return closeNoteModal();
const oldNote = item.note; item.note = newNote; // 乐观刷新当前行备注显示 (可局部更新但简单重新渲染) renderTable(); try { const res = await editMemberNoteApi({ id: item.id, note: newNote }); if (res && typeof res.code !== "undefined" && res.code !== 200) { throw new Error(res.msg || "保存失败"); } showToast("备注保存成功"); } catch (err) { item.note = oldNote; renderTable(); showToast("保存备注失败: " + (err.message || '接口错误'), true); } finally { closeNoteModal(); } });
// 分页渲染 function renderPagination() { const totalPages = Math.max(1, Math.ceil(state.total / state.pageSize)); let current = Math.min(Math.max(1, state.currentPage), totalPages); state.currentPage = current;
const pages = buildPageList(current, totalPages, 2); let html = ''; html += `<button class="btn" data-action="prev" ${current === 1 ? 'disabled' : ''}>◀ 上一页</button>`; pages.forEach(p => { if (p === '...') { html += `<span class="small" style="padding:0 6px;">...</span>`; } else { html += `<button class="btn ${p === current ? 'active' : ''}" data-page="${p}">${p}</button>`; } }); html += `<button class="btn" data-action="next" ${current === totalPages ? 'disabled' : ''}>下一页 ▶</button>`; paginationEl.innerHTML = html; }
function buildPageList(current, total, delta) { const pages = []; const left = Math.max(1, current - delta); const right = Math.min(total, current + delta); if (left > 1) { pages.push(1); if (left > 2) pages.push('...'); } for (let i = left; i <= right; i++) pages.push(i); if (right < total) { if (right < total - 1) pages.push('...'); pages.push(total); } return pages; }
paginationEl.addEventListener("click", (e) => { if (!requireAuth()) return; const btn = e.target.closest("button"); if (!btn) return; const action = btn.getAttribute("data-action"); if (action === "prev") { if (state.currentPage > 1) state.currentPage--; } else if (action === "next") { const totalPages = Math.max(1, Math.ceil(state.total / state.pageSize)); if (state.currentPage < totalPages) state.currentPage++; } else { const p = Number(btn.getAttribute("data-page")); if (!isNaN(p) && p > 0) state.currentPage = p; } update(); });
pageSizeSelect.addEventListener("change", () => { if (!requireAuth()) return; const newSize = parseInt(pageSizeSelect.value, 10); state.pageSize = newSize; state.currentPage = 1; update(); });
async function update() { if (!requireAuth()) return; await fetchPage(state.currentPage, state.pageSize); renderTable(); renderPagination(); }
function escapeHtml(s) { if (s === undefined || s === null) return ''; return String(s).replace(/[&<>]/g, function (m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function (c) { return c; }); }
// 初始化:如果已登录则加载,否则展示登录遮罩 if (checkLoginStatus()) { update(); } else { showLoginCheckModal(); }
// 监听localStorage变化或者其他页面可能登录跳转后刷新(简单处理) window.addEventListener('focus', () => { if (checkLoginStatus() && document.getElementById('loginCheckModal')?.style.display === 'flex') { hideLoginCheckModal(); update(); } else if (!checkLoginStatus() && document.getElementById('loginCheckModal')?.style.display !== 'flex') { showLoginCheckModal(); } }); </script></body>
</html>
|