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
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 '&';
|
|
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>
|