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

760 lines
21 KiB

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