市场夺宝奇兵
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.
 
 

860 lines
30 KiB

<!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://api.whatsapp.com/send?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://api.whatsapp.com/send?phone=${encodeURIComponent(whatsappPhone)}&text=${encodeURIComponent('hello欢迎来到赢在美股')}`;
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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
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>