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

859 lines
30 KiB

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width,initial-scale=1" />
  6. <title>管理后台 · Adam说港股导流</title>
  7. <style>
  8. .tab-container {
  9. max-width: 1200px;
  10. margin: 0 0 12px 0;
  11. padding: 0 16px;
  12. }
  13. .tabs {
  14. display: flex;
  15. gap: 50px;
  16. padding: 4px;
  17. border-radius: 8px;
  18. width: fit-content;
  19. }
  20. .tab {
  21. padding: 10px 20px;
  22. border-radius: 6px;
  23. border: none;
  24. background: transparent;
  25. color: #131212;
  26. font-weight: 700;
  27. cursor: pointer;
  28. transition: all 0.3s ease;
  29. font-size: 16px;
  30. white-space: nowrap;
  31. }
  32. .tab:hover {
  33. background: rgba(46, 125, 50, 0.1);
  34. color: #25D366;
  35. }
  36. .tab.active {
  37. background: #BAF7D0;
  38. color: #0E4322;
  39. box-shadow: 0 2px 4px rgba(46, 125, 50, 0.3);
  40. }
  41. * {
  42. box-sizing: border-box;
  43. }
  44. body {
  45. font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  46. padding: 24px;
  47. background: #f0f2f6;
  48. color: #1e293b;
  49. margin: 0;
  50. }
  51. .card {
  52. background: #ffffff;
  53. padding: 20px 16px 24px 16px;
  54. border-radius: 20px;
  55. box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
  56. max-width: 100%;
  57. overflow-x: auto;
  58. }
  59. /* 表格容器实现横向滚动(移动端友好),同时保证单元格内容溢出省略 */
  60. .table-wrapper {
  61. overflow-x: auto;
  62. border-radius: 14px;
  63. margin-top: 8px;
  64. }
  65. table {
  66. width: 100%;
  67. border-collapse: collapse;
  68. min-width: 1000px;
  69. font-size: 13.5px;
  70. }
  71. th,
  72. td {
  73. padding: 12px 10px;
  74. border-bottom: 1px solid #e9edf2;
  75. text-align: center;
  76. vertical-align: middle;
  77. max-width: 0;
  78. /* 辅助溢出省略 */
  79. }
  80. /* 表头固定宽度 + 允许文字折行但优先省略 */
  81. th {
  82. background: #f8fafc;
  83. font-weight: 600;
  84. color: #1e293b;
  85. letter-spacing: 0.3px;
  86. white-space: nowrap;
  87. }
  88. /* 单元格内容超出宽度后隐藏文字,鼠标悬浮显示完整内容 */
  89. td {
  90. white-space: nowrap;
  91. overflow: hidden;
  92. text-overflow: ellipsis;
  93. max-width: 180px;
  94. transition: background 0.1s;
  95. }
  96. /* 针对特定列单独控制最大宽度,确保不超出表头宽度 */
  97. td:nth-child(1) {
  98. max-width: 55px;
  99. }
  100. /* 姓名 */
  101. td:nth-child(2) {
  102. max-width: 100px;
  103. }
  104. /* WhatsApp按钮列 */
  105. td:nth-child(3) {
  106. max-width: 100px;
  107. overflow: visible;
  108. white-space: normal;
  109. }
  110. /* 国家代码 */
  111. td:nth-child(4) {
  112. max-width: 90px;
  113. }
  114. /* 电话号码 */
  115. td:nth-child(5) {
  116. max-width: 120px;
  117. }
  118. /* 微信ID */
  119. td:nth-child(6) {
  120. max-width: 120px;
  121. }
  122. /* 邮箱 */
  123. td:nth-child(7) {
  124. max-width: 150px;
  125. }
  126. /* 反馈内容 */
  127. td:nth-child(8) {
  128. max-width: 120px;
  129. }
  130. /* 反馈类型 */
  131. td:nth-child(9) {
  132. max-width: 100px;
  133. }
  134. /* 添加时间 */
  135. td:nth-child(10) {
  136. max-width: 160px;
  137. }
  138. /* 是否联系 */
  139. td:nth-child(11) {
  140. max-width: 100px;
  141. white-space: normal;
  142. }
  143. /* 备注 */
  144. td:nth-child(12) {
  145. max-width: 160px;
  146. }
  147. /* 操作 */
  148. td:nth-child(13) {
  149. max-width: 100px;
  150. white-space: normal;
  151. }
  152. /* 悬浮显示完整内容(自定义title特性,但使用全局title亦可,增强体验)*/
  153. td[title] {
  154. cursor: help;
  155. }
  156. .controls {
  157. display: flex;
  158. gap: 16px;
  159. align-items: center;
  160. flex-wrap: wrap;
  161. margin-top: 20px;
  162. background: #ffffff;
  163. padding: 8px 4px;
  164. }
  165. .pagination {
  166. display: flex;
  167. gap: 6px;
  168. align-items: center;
  169. margin-left: auto;
  170. flex-wrap: wrap;
  171. }
  172. .btn {
  173. padding: 6px 12px;
  174. border-radius: 30px;
  175. border: 1px solid #d4d9e2;
  176. background: #fff;
  177. cursor: pointer;
  178. font-size: 12.5px;
  179. font-weight: 500;
  180. transition: all 0.2s ease;
  181. }
  182. .btn:hover:not(:disabled) {
  183. background-color: #f1f5f9;
  184. border-color: #94a3b8;
  185. }
  186. .btn:disabled {
  187. opacity: 0.5;
  188. cursor: not-allowed;
  189. }
  190. .btn.primary {
  191. background: #2563eb;
  192. color: white;
  193. border-color: #2563eb;
  194. }
  195. .btn.primary:hover:not(:disabled) {
  196. background: #1d4ed8;
  197. }
  198. .btn.whatsapp {
  199. background: #25D366;
  200. color: #fff;
  201. border-color: #1da15a;
  202. }
  203. .btn.whatsapp:hover {
  204. background: #20b859;
  205. }
  206. .status-btn {
  207. padding: 5px 12px;
  208. border-radius: 24px;
  209. border: none;
  210. font-weight: 500;
  211. cursor: pointer;
  212. font-size: 12px;
  213. transition: 0.1s;
  214. }
  215. .status-0 {
  216. background: #fee2e2;
  217. color: #b91c1c;
  218. border: 1px solid #fecaca;
  219. }
  220. .status-1 {
  221. background: #dcfce7;
  222. color: #15803d;
  223. border: 1px solid #bbf7d0;
  224. }
  225. .btn.active {
  226. background: #2563eb;
  227. color: white;
  228. border-color: #2563eb;
  229. }
  230. select,
  231. input[type="number"] {
  232. padding: 6px 12px;
  233. border-radius: 30px;
  234. border: 1px solid #cbd5e1;
  235. background: white;
  236. font-size: 13px;
  237. }
  238. .small {
  239. font-size: 13px;
  240. color: #475569;
  241. }
  242. @media (max-width: 720px) {
  243. .controls {
  244. flex-direction: column;
  245. align-items: stretch;
  246. }
  247. .pagination {
  248. margin-left: 0;
  249. justify-content: center;
  250. }
  251. }
  252. /* 备注模态框 */
  253. #noteModal {
  254. display: none;
  255. position: fixed;
  256. inset: 0;
  257. background: rgba(0, 0, 0, 0.5);
  258. backdrop-filter: blur(3px);
  259. align-items: center;
  260. justify-content: center;
  261. z-index: 10000;
  262. }
  263. #noteModal .dialog {
  264. background: #fff;
  265. padding: 24px;
  266. border-radius: 28px;
  267. width: 90%;
  268. max-width: 520px;
  269. box-shadow: 0 25px 40px rgba(0, 0, 0, 0.2);
  270. }
  271. #noteModal textarea {
  272. width: 100%;
  273. min-height: 130px;
  274. padding: 12px;
  275. border-radius: 18px;
  276. border: 1px solid #cbd5e1;
  277. font-size: 14px;
  278. font-family: inherit;
  279. resize: vertical;
  280. }
  281. .toast {
  282. position: fixed;
  283. top: 24px;
  284. left: 50%;
  285. transform: translateX(-50%) translateY(-30px);
  286. background: #1e293b;
  287. color: white;
  288. padding: 10px 20px;
  289. border-radius: 60px;
  290. font-size: 14px;
  291. font-weight: 500;
  292. opacity: 0;
  293. transition: all 0.2s ease;
  294. z-index: 11000;
  295. pointer-events: none;
  296. box-shadow: 0 6px 14px rgba(0, 0, 0, 0.1);
  297. }
  298. .toast.show {
  299. opacity: 1;
  300. transform: translateX(-50%) translateY(0);
  301. }
  302. /* 登录遮罩 */
  303. #loginCheckModal {
  304. display: none;
  305. position: fixed;
  306. inset: 0;
  307. background: rgba(0, 0, 0, 0.75);
  308. align-items: center;
  309. justify-content: center;
  310. z-index: 99999;
  311. }
  312. #loginCheckModal .dialog {
  313. background: #fff;
  314. padding: 32px;
  315. border-radius: 32px;
  316. width: 90%;
  317. max-width: 380px;
  318. text-align: center;
  319. }
  320. #loginCheckModal h3 {
  321. margin: 0 0 12px;
  322. color: #e11d48;
  323. }
  324. </style>
  325. </head>
  326. <body>
  327. <div id="loginCheckModal">
  328. <div class="dialog">
  329. <h3>🔐 访问被拒绝</h3>
  330. <p>您尚未登录或登录已过期,请重新登录系统。</p>
  331. <button class="btn primary" id="goToLoginBtn">前往登录页面</button>
  332. </div>
  333. </div>
  334. <div class="tab-container">
  335. <div class="tabs">
  336. <button class="tab active" data-tab="win-us-stock">Adam说港股导流</button>
  337. </div>
  338. </div>
  339. <div class="card">
  340. <div class="table-wrapper">
  341. <table aria-describedby="成员反馈列表">
  342. <thead>
  343. <tr>
  344. <th style="width: 50px">序号</th>
  345. <th style="width: 100px">姓名</th>
  346. <th style="width: 100px">WhatsApp</th>
  347. <th style="width: 90px">国家/地区代码</th>
  348. <th style="width: 120px">电话号码</th>
  349. <th style="width: 120px">微信ID</th>
  350. <th style="width: 150px">邮箱</th>
  351. <th style="width: 130px">反馈内容</th>
  352. <th style="width: 100px">反馈类型</th>
  353. <th style="width: 160px">添加时间</th>
  354. <th style="width: 100px">是否联系</th>
  355. <th style="width: 160px">备注</th>
  356. <th style="width: 100px">操作</th>
  357. </tr>
  358. </thead>
  359. <tbody id="tableBody"></tbody>
  360. </table>
  361. </div>
  362. <div class="controls">
  363. <div class="small">
  364. 每页显示
  365. <select id="pageSizeSelect">
  366. <option value="20" selected>20</option>
  367. <option value="50">50</option>
  368. <option value="100">100</option>
  369. <option value="200">200</option>
  370. </select>
  371. </div>
  372. <div class="small"><span id="totalCount">0</span></div>
  373. <div class="pagination" id="pagination"></div>
  374. </div>
  375. </div>
  376. <div id="noteModal">
  377. <div class="dialog">
  378. <h3 style="margin-top: 0;">✏️ 编辑备注</h3>
  379. <textarea id="noteTextarea" rows="4" placeholder="输入备注信息..."></textarea>
  380. <div style="margin-top: 20px; display: flex; justify-content: flex-end; gap: 12px;">
  381. <button class="btn" id="noteCancelBtn">取消</button>
  382. <button class="btn primary" id="noteSaveBtn">保存</button>
  383. </div>
  384. </div>
  385. </div>
  386. <div id="toast" class="toast"></div>
  387. <script type="module">
  388. import { getMemberListApi, updateMemberStateApi, editMemberNoteApi } from './src/api/hkmember.js'
  389. // ---------- 登录验证 ----------
  390. function checkLoginStatus() {
  391. const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
  392. const loginTime = parseInt(localStorage.getItem('loginTime'), 10);
  393. if (!isLoggedIn || isNaN(loginTime)) return false;
  394. const hoursDiff = (new Date().getTime() - loginTime) / (1000 * 60 * 60);
  395. return hoursDiff < 24;
  396. }
  397. function showLoginCheckModal() {
  398. const modal = document.getElementById('loginCheckModal');
  399. if (modal) modal.style.display = 'flex';
  400. }
  401. function hideLoginCheckModal() {
  402. const modal = document.getElementById('loginCheckModal');
  403. if (modal) modal.style.display = 'none';
  404. }
  405. function redirectToLogin() {
  406. window.location.href = 'hkLogiManagement.html';
  407. }
  408. // 全局前置守卫:如果未登录则显示弹窗,返回false
  409. function requireAuth() {
  410. if (!checkLoginStatus()) {
  411. showLoginCheckModal();
  412. return false;
  413. }
  414. return true;
  415. }
  416. document.getElementById('goToLoginBtn')?.addEventListener('click', redirectToLogin);
  417. // ---------- 映射反馈类型文字 ----------
  418. function formatFeedbackType(typeValue) {
  419. // 后端返回可能是字符串或数字 "1", 2, 3, 4
  420. const val = parseInt(typeValue, 10);
  421. switch (val) {
  422. case 1: return '功能建议';
  423. case 2: return '问题反馈';
  424. case 3: return '体验优化';
  425. case 4: return '其他建议';
  426. default: return typeValue || '—';
  427. }
  428. }
  429. let state = {
  430. pageSize: 20,
  431. currentPage: 1,
  432. total: 0,
  433. items: []
  434. };
  435. const tableBody = document.getElementById("tableBody");
  436. const paginationEl = document.getElementById("pagination");
  437. const totalCountEl = document.getElementById("totalCount");
  438. const pageSizeSelect = document.getElementById("pageSizeSelect");
  439. const noteModal = document.getElementById("noteModal");
  440. const noteTextarea = document.getElementById("noteTextarea");
  441. const noteCancelBtn = document.getElementById("noteCancelBtn");
  442. const noteSaveBtn = document.getElementById("noteSaveBtn");
  443. const toastEl = document.getElementById("toast");
  444. let editingRowId = null;
  445. function showToast(msg, isError = false) {
  446. toastEl.textContent = msg;
  447. toastEl.style.background = isError ? '#e11d48' : '#1e293b';
  448. toastEl.classList.add("show");
  449. setTimeout(() => {
  450. toastEl.classList.remove("show");
  451. toastEl.style.background = '#1e293b';
  452. }, 1800);
  453. }
  454. // 获取数据
  455. async function fetchPage(page, pageSize) {
  456. if (!requireAuth()) return { items: [], total: 0 };
  457. try {
  458. const res = await getMemberListApi({
  459. page: page,
  460. page_size: pageSize,
  461. });
  462. if (res && typeof res.code !== "undefined" && res.code !== 200) {
  463. throw new Error(res.msg || "接口异常");
  464. }
  465. const payload = res?.data || {};
  466. state.items = payload.list || [];
  467. state.total = payload.total || 0;
  468. return { items: state.items, total: state.total };
  469. } catch (err) {
  470. console.error(err);
  471. showToast("获取数据失败: " + (err.message || '网络错误'), true);
  472. state.items = [];
  473. state.total = 0;
  474. return { items: [], total: 0 };
  475. }
  476. }
  477. // 辅助函数:给td增加title悬浮完整内容(自动处理每个单元格)
  478. function applyTitleToCells() {
  479. const rows = tableBody.querySelectorAll('tr');
  480. rows.forEach(row => {
  481. const cells = row.querySelectorAll('td');
  482. cells.forEach(cell => {
  483. // 跳过按钮列(第三列WhatsApp按钮,第11列状态按钮,第13列操作按钮)避免title干扰
  484. const isButtonCell = cell.querySelector('button') &&
  485. (cell.previousElementSibling?.innerText?.includes('WhatsApp') ||
  486. cell.querySelector('.status-btn') ||
  487. cell.querySelector('[data-action="editNote"]'));
  488. if (!isButtonCell && cell.innerText && cell.innerText.trim() !== '') {
  489. const rawText = cell.innerText;
  490. if (!cell.hasAttribute('data-has-title')) {
  491. cell.setAttribute('title', rawText);
  492. cell.setAttribute('data-has-title', 'true');
  493. } else if (cell.getAttribute('title') !== rawText) {
  494. cell.setAttribute('title', rawText);
  495. }
  496. } else if (!isButtonCell && cell.innerText === '') {
  497. cell.removeAttribute('title');
  498. }
  499. });
  500. });
  501. }
  502. // 渲染表格(带溢出省略 + 悬浮显示完整内容)
  503. function renderTable() {
  504. const startIndex = (state.currentPage - 1) * state.pageSize;
  505. const items = state.items || [];
  506. tableBody.innerHTML = items.map((item, idx) => {
  507. const serial = startIndex + idx + 1;
  508. const statusClass = item.isRelated ? "status-1" : "status-0";
  509. const statusText = item.isRelated ? "已联系" : "未联系";
  510. // 反馈类型文字转换
  511. const feedbackTypeText = formatFeedbackType(item.feedback_type);
  512. // 构建WhatsApp链接 (清理+号)
  513. const cleanCode = (item.code || '').replace(/\+/g, '');
  514. const whatsappPhone = cleanCode + (item.telephone || '');
  515. const whatsappUrl = `https://wa.me/85269518757?phone=${encodeURIComponent(whatsappPhone)}&text=${encodeURIComponent('hello欢迎来到Adam说港股')}`;
  516. // 安全转义
  517. const name = escapeHtml(item.name || "");
  518. const code = escapeHtml(item.code || "");
  519. const telephone = escapeHtml(item.telephone || "");
  520. const wechat = escapeHtml(item.wechat || "");
  521. const email = escapeHtml(item.email || "");
  522. const feedbackContent = escapeHtml(item.feedback_content || "");
  523. const createdAt = escapeHtml(item.createdAt || "");
  524. const note = escapeHtml(item.note || "");
  525. // 注意:表格结构保持13列,反馈类型用转换后的文字
  526. return `
  527. <tr>
  528. <td title="${serial}">${serial}</td>
  529. <td title="${name}">${name}</td>
  530. <td style="overflow: visible; white-space: normal;">
  531. <button class="btn whatsapp" data-action="whatsapp" data-id="${item.id}">💬 WhatsApp</button>
  532. </td>
  533. <td title="${code}">${code}</td>
  534. <td title="${telephone}">${telephone}</td>
  535. <td title="${wechat}">${wechat}</td>
  536. <td title="${email}">${email}</td>
  537. <td title="${feedbackContent}">${feedbackContent}</td>
  538. <td title="${feedbackTypeText}">${feedbackTypeText}</td>
  539. <td title="${createdAt}">${createdAt}</td>
  540. <td style="white-space: normal;">
  541. <button class="status-btn ${statusClass}" data-action="toggle" data-id="${item.id}">${statusText}</button>
  542. </td>
  543. <td title="${note}">${note}</td>
  544. <td>
  545. <button class="btn" data-action="editNote" data-id="${item.id}" style="font-size:12px;">✎ 备注</button>
  546. </td>
  547. </tr>
  548. `;
  549. }).join("");
  550. // 为所有普通单元格添加完整文本悬浮(动态渲染后)
  551. const rows = tableBody.querySelectorAll('tr');
  552. rows.forEach(row => {
  553. const cells = row.querySelectorAll('td');
  554. cells.forEach(cell => {
  555. // 跳过按钮占位列(第三列、操作列以及状态列,这些列已经有点击元素,不干扰title)
  556. const hasImportantBtn = cell.querySelector('[data-action="whatsapp"]') ||
  557. cell.querySelector('[data-action="toggle"]') ||
  558. cell.querySelector('[data-action="editNote"]');
  559. if (!hasImportantBtn) {
  560. const txt = cell.innerText.trim();
  561. if (txt) cell.setAttribute('title', txt);
  562. else cell.removeAttribute('title');
  563. } else if (cell.querySelector('[data-action="toggle"]')) {
  564. // 状态按钮列保留其自身文字悬浮展示状态文字
  565. const statusBtn = cell.querySelector('.status-btn');
  566. if (statusBtn) cell.setAttribute('title', statusBtn.innerText);
  567. } else if (cell.querySelector('[data-action="editNote"]')) {
  568. // 操作列一般不需要悬浮,但为了统一,展示当前按钮文字
  569. const btnText = cell.innerText.trim();
  570. if (btnText) cell.setAttribute('title', btnText);
  571. }
  572. });
  573. });
  574. totalCountEl.textContent = state.total;
  575. }
  576. // 事件监听(委托)
  577. tableBody.addEventListener("click", async (e) => {
  578. if (!requireAuth()) return;
  579. const whatsappBtn = e.target.closest('[data-action="whatsapp"]');
  580. if (whatsappBtn) {
  581. const id = whatsappBtn.getAttribute("data-id");
  582. handleWhatsApp(id);
  583. return;
  584. }
  585. const toggler = e.target.closest('[data-action="toggle"]');
  586. if (toggler) {
  587. const id = toggler.getAttribute("data-id");
  588. await handleToggle(id, toggler);
  589. return;
  590. }
  591. const editBtn = e.target.closest('[data-action="editNote"]');
  592. if (editBtn) {
  593. const id = editBtn.getAttribute("data-id");
  594. openNoteModal(id);
  595. return;
  596. }
  597. });
  598. function handleWhatsApp(id) {
  599. const item = state.items.find((it) => String(it.id) === String(id));
  600. if (!item) return;
  601. const cleanCode = (item.code || '').replace(/\+/g, '');
  602. const whatsappPhone = cleanCode + (item.telephone || '');
  603. if (!whatsappPhone) {
  604. showToast("电话号码无效", true);
  605. return;
  606. }
  607. const whatsappUrl = `https://wa.me/85269518757?phone=${encodeURIComponent(whatsappPhone)}&text=${encodeURIComponent('hello欢迎来到Adam说港股')}`;
  608. window.open(whatsappUrl, '_blank');
  609. }
  610. async function handleToggle(id, btnEl) {
  611. const item = state.items.find((it) => String(it.id) === String(id));
  612. if (!item) return;
  613. const prev = item.isRelated;
  614. const newVal = prev ? 0 : 1;
  615. // 乐观更新UI
  616. item.isRelated = newVal;
  617. btnEl.textContent = newVal ? "已联系" : "未联系";
  618. btnEl.classList.toggle("status-1", !!newVal);
  619. btnEl.classList.toggle("status-0", !newVal);
  620. try {
  621. const res = await updateMemberStateApi({ id: item.id, state: newVal });
  622. if (res && typeof res.code !== "undefined" && res.code !== 200) {
  623. throw new Error(res.msg || "更新失败");
  624. }
  625. showToast("状态已更新");
  626. } catch (err) {
  627. // 回滚
  628. item.isRelated = prev;
  629. btnEl.textContent = prev ? "已联系" : "未联系";
  630. btnEl.classList.toggle("status-1", !!prev);
  631. btnEl.classList.toggle("status-0", !prev);
  632. showToast("更新失败: " + (err.message || '网络错误'), true);
  633. }
  634. }
  635. function openNoteModal(id) {
  636. if (!requireAuth()) return;
  637. const item = state.items.find((it) => String(it.id) === String(id));
  638. if (!item) return;
  639. editingRowId = id;
  640. noteTextarea.value = item.note || "";
  641. noteModal.style.display = "flex";
  642. noteTextarea.focus();
  643. }
  644. function closeNoteModal() {
  645. editingRowId = null;
  646. noteTextarea.value = "";
  647. noteModal.style.display = "none";
  648. }
  649. noteCancelBtn.addEventListener("click", closeNoteModal);
  650. noteSaveBtn.addEventListener("click", async () => {
  651. if (!requireAuth()) return;
  652. if (!editingRowId) return closeNoteModal();
  653. const newNote = noteTextarea.value.trim();
  654. const item = state.items.find((it) => String(it.id) === String(editingRowId));
  655. if (!item) return closeNoteModal();
  656. const oldNote = item.note;
  657. item.note = newNote;
  658. // 乐观刷新当前行备注显示 (可局部更新但简单重新渲染)
  659. renderTable();
  660. try {
  661. const res = await editMemberNoteApi({ id: item.id, note: newNote });
  662. if (res && typeof res.code !== "undefined" && res.code !== 200) {
  663. throw new Error(res.msg || "保存失败");
  664. }
  665. showToast("备注保存成功");
  666. } catch (err) {
  667. item.note = oldNote;
  668. renderTable();
  669. showToast("保存备注失败: " + (err.message || '接口错误'), true);
  670. } finally {
  671. closeNoteModal();
  672. }
  673. });
  674. // 分页渲染
  675. function renderPagination() {
  676. const totalPages = Math.max(1, Math.ceil(state.total / state.pageSize));
  677. let current = Math.min(Math.max(1, state.currentPage), totalPages);
  678. state.currentPage = current;
  679. const pages = buildPageList(current, totalPages, 2);
  680. let html = '';
  681. html += `<button class="btn" data-action="prev" ${current === 1 ? 'disabled' : ''}>◀ 上一页</button>`;
  682. pages.forEach(p => {
  683. if (p === '...') {
  684. html += `<span class="small" style="padding:0 6px;">...</span>`;
  685. } else {
  686. html += `<button class="btn ${p === current ? 'active' : ''}" data-page="${p}">${p}</button>`;
  687. }
  688. });
  689. html += `<button class="btn" data-action="next" ${current === totalPages ? 'disabled' : ''}>下一页 ▶</button>`;
  690. paginationEl.innerHTML = html;
  691. }
  692. function buildPageList(current, total, delta) {
  693. const pages = [];
  694. const left = Math.max(1, current - delta);
  695. const right = Math.min(total, current + delta);
  696. if (left > 1) {
  697. pages.push(1);
  698. if (left > 2) pages.push('...');
  699. }
  700. for (let i = left; i <= right; i++) pages.push(i);
  701. if (right < total) {
  702. if (right < total - 1) pages.push('...');
  703. pages.push(total);
  704. }
  705. return pages;
  706. }
  707. paginationEl.addEventListener("click", (e) => {
  708. if (!requireAuth()) return;
  709. const btn = e.target.closest("button");
  710. if (!btn) return;
  711. const action = btn.getAttribute("data-action");
  712. if (action === "prev") {
  713. if (state.currentPage > 1) state.currentPage--;
  714. } else if (action === "next") {
  715. const totalPages = Math.max(1, Math.ceil(state.total / state.pageSize));
  716. if (state.currentPage < totalPages) state.currentPage++;
  717. } else {
  718. const p = Number(btn.getAttribute("data-page"));
  719. if (!isNaN(p) && p > 0) state.currentPage = p;
  720. }
  721. update();
  722. });
  723. pageSizeSelect.addEventListener("change", () => {
  724. if (!requireAuth()) return;
  725. const newSize = parseInt(pageSizeSelect.value, 10);
  726. state.pageSize = newSize;
  727. state.currentPage = 1;
  728. update();
  729. });
  730. async function update() {
  731. if (!requireAuth()) return;
  732. await fetchPage(state.currentPage, state.pageSize);
  733. renderTable();
  734. renderPagination();
  735. }
  736. function escapeHtml(s) {
  737. if (s === undefined || s === null) return '';
  738. return String(s).replace(/[&<>]/g, function (m) {
  739. if (m === '&') return '&amp;';
  740. if (m === '<') return '&lt;';
  741. if (m === '>') return '&gt;';
  742. return m;
  743. }).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function (c) {
  744. return c;
  745. });
  746. }
  747. // 初始化:如果已登录则加载,否则展示登录遮罩
  748. if (checkLoginStatus()) {
  749. update();
  750. } else {
  751. showLoginCheckModal();
  752. }
  753. // 监听localStorage变化或者其他页面可能登录跳转后刷新(简单处理)
  754. window.addEventListener('focus', () => {
  755. if (checkLoginStatus() && document.getElementById('loginCheckModal')?.style.display === 'flex') {
  756. hideLoginCheckModal();
  757. update();
  758. } else if (!checkLoginStatus() && document.getElementById('loginCheckModal')?.style.display !== 'flex') {
  759. showLoginCheckModal();
  760. }
  761. });
  762. </script>
  763. </body>
  764. </html>