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

673 lines
18 KiB

3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
2 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
2 months ago
2 months ago
  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>管理后台</title>
  7. <style>
  8. body {
  9. font-family: Arial, sans-serif;
  10. padding: 24px;
  11. background: #f7f8fb;
  12. color: #222;
  13. }
  14. .card {
  15. background: #fff;
  16. padding: 16px;
  17. border-radius: 8px;
  18. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  19. margin: auto;
  20. }
  21. table {
  22. width: 100%;
  23. border-collapse: collapse;
  24. margin-top: 12px;
  25. }
  26. th,
  27. td {
  28. padding: 10px 12px;
  29. border-bottom: 1px solid #eee;
  30. text-align: left;
  31. font-size: 14px;
  32. }
  33. th {
  34. background: #fafafa;
  35. font-weight: 600;
  36. }
  37. .controls {
  38. display: flex;
  39. gap: 12px;
  40. align-items: center;
  41. flex-wrap: wrap;
  42. }
  43. .pagination {
  44. display: flex;
  45. gap: 6px;
  46. align-items: center;
  47. margin-left: auto;
  48. flex-wrap: wrap;
  49. }
  50. .btn {
  51. padding: 6px 10px;
  52. border-radius: 6px;
  53. border: 1px solid #ddd;
  54. background: #fff;
  55. cursor: pointer;
  56. user-select: none;
  57. }
  58. .btn:disabled {
  59. opacity: 0.5;
  60. cursor: default;
  61. }
  62. .btn.primary {
  63. background: #007bff;
  64. color: #fff;
  65. border-color: #007bff;
  66. }
  67. .btn.whatsapp {
  68. background: #25D366;
  69. color: #fff;
  70. border-color: #25D366;
  71. }
  72. .status-btn {
  73. padding: 4px 8px;
  74. border-radius: 6px;
  75. border: 1px solid #ccc;
  76. cursor: pointer;
  77. }
  78. .status-0 {
  79. background: red;
  80. color: #fff;
  81. }
  82. .status-1 {
  83. background: #2f9e44;
  84. color: #fff;
  85. border-color: #2f9e44;
  86. }
  87. .btn.active {
  88. background: #007bff;
  89. color: #fff;
  90. border-color: #007bff;
  91. }
  92. select,
  93. input[type="number"] {
  94. padding: 6px;
  95. border-radius: 6px;
  96. border: 1px solid #ddd;
  97. }
  98. .small {
  99. font-size: 13px;
  100. color: #666;
  101. }
  102. @media (max-width: 640px) {
  103. .controls {
  104. flex-direction: column;
  105. align-items: flex-start;
  106. }
  107. .pagination {
  108. margin-left: 0;
  109. }
  110. }
  111. #noteModal {
  112. display: none;
  113. position: fixed;
  114. inset: 0;
  115. background: rgba(0, 0, 0, 0.45);
  116. align-items: center;
  117. justify-content: center;
  118. z-index: 9999;
  119. }
  120. #noteModal .dialog {
  121. background: #fff;
  122. padding: 16px;
  123. border-radius: 8px;
  124. width: 90%;
  125. max-width: 520px;
  126. box-sizing: border-box;
  127. }
  128. #noteModal textarea {
  129. width: 100%;
  130. min-width: 60%;
  131. max-width: 100%;
  132. min-height: 150px;
  133. box-sizing: border-box;
  134. padding: 8px;
  135. border-radius: 6px;
  136. border: 1px solid #ddd;
  137. font-size: 14px;
  138. }
  139. .toast {
  140. position: fixed;
  141. top: -20px;
  142. left: 50%;
  143. transform: translateX(-50%);
  144. background: #4caf50;
  145. color: #fff;
  146. padding: 10px 16px;
  147. border-radius: 6px;
  148. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  149. font-size: 16px;
  150. opacity: 0;
  151. transition: all 0.3s ease;
  152. z-index: 10000;
  153. pointer-events: none;
  154. }
  155. .toast.show {
  156. opacity: 1;
  157. top: 20px;
  158. }
  159. table th,
  160. table td {
  161. text-align: center;
  162. }
  163. /* 登录验证样式 */
  164. #loginCheckModal {
  165. display: none;
  166. position: fixed;
  167. inset: 0;
  168. background: rgba(0, 0, 0, 0.7);
  169. align-items: center;
  170. justify-content: center;
  171. z-index: 99999;
  172. }
  173. #loginCheckModal .dialog {
  174. background: #fff;
  175. padding: 30px;
  176. border-radius: 12px;
  177. width: 90%;
  178. max-width: 400px;
  179. text-align: center;
  180. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  181. }
  182. #loginCheckModal h3 {
  183. margin: 0 0 16px;
  184. color: #e74c3c;
  185. font-size: 20px;
  186. }
  187. #loginCheckModal p {
  188. margin: 0 0 24px;
  189. color: #666;
  190. line-height: 1.5;
  191. }
  192. #loginCheckModal .btn {
  193. padding: 10px 20px;
  194. font-size: 16px;
  195. }
  196. </style>
  197. </head>
  198. <body>
  199. <!-- 登录验证模态框 -->
  200. <div id="loginCheckModal">
  201. <div class="dialog">
  202. <h3>访问被拒绝</h3>
  203. <p>您尚未登录或登录已过期,请先登录系统。</p>
  204. <button class="btn primary" id="goToLoginBtn">前往登录页面</button>
  205. </div>
  206. </div>
  207. <div class="card">
  208. <table aria-describedby="tableDesc">
  209. <thead>
  210. <tr>
  211. <th style="width: 30px">#</th>
  212. <th style="width: 60px">姓名</th>
  213. <th style="width: 80px">WhatsApp</th>
  214. <th style="width: 90px">国家/地区代码</th>
  215. <th style="width: 110px">电话号码</th>
  216. <th style="width: 110px">微信ID</th>
  217. <th style="width: 110px">邮箱</th>
  218. <th style="width: 70px">获客来源</th>
  219. <th style="width: 130px">来源地址</th>
  220. <th style="width: 150px">添加时间</th>
  221. <th style="width: 90px">是否联系</th>
  222. <th style="width: 90px">备注</th>
  223. <th style="width: 100px">操作</th>
  224. </tr>
  225. </thead>
  226. <tbody id="tableBody"></tbody>
  227. </table>
  228. <div class="controls" style="margin-bottom: 8px; margin-top: 12px">
  229. <div class="small">
  230. 每页显示
  231. <select id="pageSizeSelect">
  232. <option value="20" selected>20</option>
  233. <option value="50">50</option>
  234. <option value="100">100</option>
  235. <option value="200">200</option>
  236. </select>
  237. </div>
  238. <div class="small"><span id="totalCount">0</span></div>
  239. <div class="pagination" id="pagination"></div>
  240. </div>
  241. <!-- 备注编辑模态 -->
  242. <div id="noteModal">
  243. <div class="dialog">
  244. <h3 style="margin: 0 0 8px">编辑备注</h3>
  245. <textarea id="noteTextarea" rows="6" placeholder="输入备注..."></textarea>
  246. <div style="
  247. margin-top: 10px;
  248. display: flex;
  249. justify-content: flex-end;
  250. gap: 8px;
  251. ">
  252. <button class="btn" id="noteCancelBtn">取消</button>
  253. <button class="btn primary" id="noteSaveBtn">保存</button>
  254. </div>
  255. </div>
  256. </div>
  257. </div>
  258. <div id="toast" class="toast"></div>
  259. <script type="module">
  260. import { getMemberListApi, updateMemberStateApi, editMemberNoteApi } from './src/api/member.js'
  261. // 登录验证函数
  262. function checkLoginStatus() {
  263. const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
  264. const loginTime = parseInt(localStorage.getItem('loginTime'));
  265. const currentTime = new Date().getTime();
  266. const hoursDiff = (currentTime - loginTime) / (1000 * 60 * 60);
  267. // 检查是否登录且登录时间在24小时内
  268. if (!isLoggedIn || hoursDiff >= 24) {
  269. // 清除过期的登录状态
  270. localStorage.removeItem('isLoggedIn');
  271. localStorage.removeItem('loginTime');
  272. return false;
  273. }
  274. return true;
  275. }
  276. // 显示登录验证模态框
  277. function showLoginCheckModal() {
  278. const modal = document.getElementById('loginCheckModal');
  279. modal.style.display = 'flex';
  280. }
  281. // 隐藏登录验证模态框
  282. function hideLoginCheckModal() {
  283. const modal = document.getElementById('loginCheckModal');
  284. modal.style.display = 'none';
  285. }
  286. // 跳转到登录页面
  287. function redirectToLogin() {
  288. window.location.href = 'login-management.html';
  289. }
  290. // 检查登录状态
  291. if (!checkLoginStatus()) {
  292. showLoginCheckModal();
  293. }
  294. // 绑定前往登录页面按钮事件
  295. document.getElementById('goToLoginBtn').addEventListener('click', redirectToLogin);
  296. let state = {
  297. pageSize: 20,
  298. currentPage: 1,
  299. total: 0,
  300. items: []
  301. };
  302. // DOM
  303. const tableBody = document.getElementById("tableBody");
  304. const paginationEl = document.getElementById("pagination");
  305. const totalCountEl = document.getElementById("totalCount");
  306. const currentPageEl = document.getElementById("currentPage");
  307. const showingRangeEl = document.getElementById("showingRange");
  308. const pageSizeSelect = document.getElementById("pageSizeSelect");
  309. const gotoInput = document.getElementById("gotoInput");
  310. const noteModal = document.getElementById("noteModal");
  311. const noteTextarea = document.getElementById("noteTextarea");
  312. const noteCancelBtn = document.getElementById("noteCancelBtn");
  313. const noteSaveBtn = document.getElementById("noteSaveBtn");
  314. const toastEl = document.getElementById("toast");
  315. let editingRowId = null;
  316. function showToast(msg) {
  317. toastEl.textContent = msg;
  318. toastEl.classList.add("show");
  319. setTimeout(() => toastEl.classList.remove("show"), 1000);
  320. }
  321. async function fetchPage(page, pageSize) {
  322. // 每次请求数据前都检查登录状态
  323. if (!checkLoginStatus()) {
  324. showLoginCheckModal();
  325. return;
  326. }
  327. try {
  328. const res = await getMemberListApi({
  329. page: page,
  330. page_size: pageSize,
  331. })
  332. if (typeof res.code !== "undefined" && res.code !== 200) {
  333. throw new Error(res.msg || "接口返回错误");
  334. }
  335. console.log(res)
  336. const payload = res.data || {};
  337. state.items = payload.list;
  338. state.total = payload.total;
  339. } catch (err) {
  340. alert("获取数据失败");
  341. state.total = 0;
  342. }
  343. }
  344. // 渲染表格行
  345. function renderTable() {
  346. const startIndex = (state.currentPage - 1) * state.pageSize;
  347. const items = state.items || [];
  348. tableBody.innerHTML = items
  349. .map((item, idx) => {
  350. const serial = startIndex + idx + 1;
  351. const statusClass = item.isRelated ? "status-1" : "status-0";
  352. const statusText = item.isRelated ? "已联系" : "未联系";
  353. // 构建WhatsApp链接 - 移除国家代码中的+号
  354. const cleanCode = (item.code || '').replace(/\+/g, '');
  355. const whatsappPhone = cleanCode + (item.telephone || '');
  356. const whatsappUrl = `https://api.whatsapp.com/send?phone=${encodeURIComponent(whatsappPhone)}&text=${encodeURIComponent('hello欢迎来到赢在美股')}`;
  357. return `
  358. <tr>
  359. <td>${serial}</td>
  360. <td>${escapeHtml(item.name || "")}</td>
  361. <td>
  362. <button class="btn whatsapp" data-action="whatsapp" data-id="${item.id}">WhatsApp</button>
  363. </td>
  364. <td>${escapeHtml(item.code || "")}</td>
  365. <td>${escapeHtml(item.telephone || "")}</td>
  366. <td>${escapeHtml(item.wechat || "")}</td>
  367. <td>${escapeHtml(item.email || "")}</td>
  368. <td>${escapeHtml(item.type === 0 ? "其它" : item.type === 1 ? "视频" : item.type === 2 ? "直播" : item.type === 3 ? "帖子" : "其它")}</td>
  369. <td>${escapeHtml(item.url || "")}</td>
  370. <td>${escapeHtml(item.createdAt || "")}</td>
  371. <td>
  372. <button class="status-btn ${statusClass}" data-action="toggle" data-id="${item.id
  373. }">${statusText}</button>
  374. </td>
  375. <td>${escapeHtml(item.note || "")}</td>
  376. <td>
  377. <button class="btn" data-action="editNote" data-id="${item.id
  378. }">编辑备注</button>
  379. </td>
  380. </tr>
  381. `;
  382. })
  383. .join("");
  384. totalCountEl.textContent = state.total;
  385. }
  386. tableBody.addEventListener("click", async (e) => {
  387. // 每次操作前检查登录状态
  388. if (!checkLoginStatus()) {
  389. showLoginCheckModal();
  390. return;
  391. }
  392. // WhatsApp跳转
  393. const whatsappBtn = e.target.closest('[data-action="whatsapp"]');
  394. if (whatsappBtn) {
  395. const id = whatsappBtn.getAttribute("data-id");
  396. handleWhatsApp(id);
  397. return;
  398. }
  399. // 切换状态
  400. const toggler = e.target.closest('[data-action="toggle"]');
  401. if (toggler) {
  402. const id = toggler.getAttribute("data-id");
  403. await handleToggle(id, toggler);
  404. return;
  405. }
  406. // 编辑备注
  407. const editBtn = e.target.closest('[data-action="editNote"]');
  408. if (editBtn) {
  409. const id = editBtn.getAttribute("data-id");
  410. openNoteModal(id);
  411. return;
  412. }
  413. });
  414. // ---------- WhatsApp跳转 ----------
  415. function handleWhatsApp(id) {
  416. const item = state.items.find((it) => String(it.id) === String(id));
  417. if (!item) return;
  418. // 移除国家代码中的+号
  419. const cleanCode = (item.code || '').replace(/\+/g, '');
  420. const whatsappPhone = cleanCode + (item.telephone || '');
  421. const whatsappUrl = `https://api.whatsapp.com/send?phone=${encodeURIComponent(whatsappPhone)}&text=${encodeURIComponent('hello欢迎来到赢在美股')}`;
  422. window.open(whatsappUrl, '_blank');
  423. }
  424. // ---------- 切换联系状态 ----------
  425. async function handleToggle(id, btnEl) {
  426. const item = state.items.find((it) => String(it.id) === String(id));
  427. if (!item) return;
  428. const prev = item.isRelated;
  429. const newVal = prev ? 0 : 1;
  430. item.isRelated = newVal;
  431. btnEl.textContent = newVal ? "已联系" : "未联系";
  432. btnEl.classList.toggle("status-1", !!newVal);
  433. btnEl.classList.toggle("status-0", !newVal);
  434. try {
  435. const res = await updateMemberStateApi({ id: item.id, state: newVal })
  436. if (typeof res.code !== "undefined" && res.code !== 200) {
  437. throw new Error(res.msg || "更新失败");
  438. }
  439. showToast("状态修改成功");
  440. } catch (err) {
  441. item.isRelated = prev;
  442. btnEl.textContent = prev ? "已联系" : "未联系";
  443. btnEl.classList.toggle("status-1", !!prev);
  444. btnEl.classList.toggle("status-0", !prev);
  445. alert("更新失败");
  446. }
  447. }
  448. // ---------- 备注模态相关 ----------
  449. function openNoteModal(id) {
  450. // 打开模态框前检查登录状态
  451. if (!checkLoginStatus()) {
  452. showLoginCheckModal();
  453. return;
  454. }
  455. const item = state.items.find((it) => String(it.id) === String(id));
  456. if (!item) return;
  457. editingRowId = id;
  458. noteTextarea.value = item.note || "";
  459. noteModal.style.display = "flex";
  460. noteTextarea.focus();
  461. }
  462. function closeNoteModal() {
  463. editingRowId = null;
  464. noteTextarea.value = "";
  465. noteModal.style.display = "none";
  466. }
  467. noteCancelBtn.addEventListener("click", closeNoteModal);
  468. noteSaveBtn.addEventListener("click", async () => {
  469. // 保存前检查登录状态
  470. if (!checkLoginStatus()) {
  471. showLoginCheckModal();
  472. return;
  473. }
  474. if (!editingRowId) return closeNoteModal();
  475. const newNote = noteTextarea.value.trim();
  476. const item = state.items.find((it) => String(it.id) === String(editingRowId));
  477. if (!item) return closeNoteModal();
  478. try {
  479. const res = await editMemberNoteApi({ id: item.id, note: newNote })
  480. if (typeof res.code !== "undefined" && res.code !== 200) {
  481. throw new Error(res.msg || "保存失败");
  482. }
  483. const prevNote = item.note;
  484. item.note = newNote;
  485. renderTable();
  486. showToast("备注保存成功");
  487. } catch (err) {
  488. alert("保存备注失败");
  489. } finally {
  490. closeNoteModal();
  491. }
  492. });
  493. // 渲染分页控件(页码、上一页、下一页)
  494. function renderPagination() {
  495. const totalPages = Math.max(1, Math.ceil(state.total / state.pageSize));
  496. const current = Math.min(Math.max(1, state.currentPage), totalPages);
  497. state.currentPage = current;
  498. const pages = buildPageList(current, totalPages, 2);
  499. let html = "";
  500. html += `<button class="btn" data-action="prev" ${current === 1 ? "disabled" : ""
  501. }>上一页</button>`;
  502. pages.forEach((p) => {
  503. if (p === "...") {
  504. html += `<span class="small" style="padding:6px 8px">...</span>`;
  505. } else {
  506. html += `<button class="btn ${p === current ? "active" : ""
  507. }" data-page="${p}">${p}</button>`;
  508. }
  509. });
  510. html += `<button class="btn" data-action="next" ${current === totalPages ? "disabled" : ""
  511. }>下一页</button>`;
  512. paginationEl.innerHTML = html;
  513. }
  514. function buildPageList(current, total, delta) {
  515. const pages = [];
  516. const left = Math.max(1, current - delta);
  517. const right = Math.min(total, current + delta);
  518. if (left > 1) {
  519. pages.push(1);
  520. if (left > 2) pages.push("...");
  521. }
  522. for (let i = left; i <= right; i++) pages.push(i);
  523. if (right < total) {
  524. if (right < total - 1) pages.push("...");
  525. pages.push(total);
  526. }
  527. return pages;
  528. }
  529. paginationEl.addEventListener("click", (e) => {
  530. // 分页操作前检查登录状态
  531. if (!checkLoginStatus()) {
  532. showLoginCheckModal();
  533. return;
  534. }
  535. const btn = e.target.closest("button");
  536. if (!btn) return;
  537. const action = btn.getAttribute("data-action");
  538. if (action === "prev") {
  539. if (state.currentPage > 1) state.currentPage--;
  540. } else if (action === "next") {
  541. const totalPages = Math.max(
  542. 1,
  543. Math.ceil(state.total / state.pageSize)
  544. );
  545. if (state.currentPage < totalPages) state.currentPage++;
  546. } else {
  547. const p = Number(btn.getAttribute("data-page"));
  548. if (!isNaN(p)) state.currentPage = p;
  549. }
  550. update();
  551. });
  552. // 每页条数变更
  553. pageSizeSelect.addEventListener("change", () => {
  554. // 操作前检查登录状态
  555. if (!checkLoginStatus()) {
  556. showLoginCheckModal();
  557. return;
  558. }
  559. const newSize = parseInt(pageSizeSelect.value, 10);
  560. state.pageSize = newSize;
  561. state.currentPage = 1;
  562. update();
  563. gotoInput.value = "";
  564. });
  565. async function update() {
  566. // 更新数据前检查登录状态
  567. if (!checkLoginStatus()) {
  568. showLoginCheckModal();
  569. return;
  570. }
  571. const p = Math.max(1, state.currentPage);
  572. state.currentPage = p;
  573. await fetchPage(state.currentPage, state.pageSize);
  574. renderTable();
  575. renderPagination();
  576. }
  577. // 简单安全的文本转义(防 XSS)
  578. function escapeHtml(s) {
  579. return String(s)
  580. .replace(/&/g, "&amp;")
  581. .replace(/</g, "&lt;")
  582. .replace(/>/g, "&gt;")
  583. .replace(/"/g, "&quot;")
  584. .replace(/'/g, "&#39;");
  585. }
  586. // 首次渲染 - 检查登录状态
  587. if (checkLoginStatus()) {
  588. update();
  589. } else {
  590. showLoginCheckModal();
  591. }
  592. </script>
  593. </body>
  594. </html>