国内市场双十一活动仓库
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.

590 lines
23 KiB

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
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
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
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
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
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
3 months ago
3 months ago
3 months ago
3 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. /* ---- 基本布局(源自你的 vue 样式) ---- */
  9. body, html { margin:0; padding:0; height:100%; font-family: Arial, "Helvetica Neue", "PingFang SC", "Microsoft Yahei"; background:#f4f6f8; color:#333; }
  10. .page-container { width:100%; height:100vh; display:flex; flex-direction:column; }
  11. .top-bar { height:60px; background-color:#1890ff; color:#fff; padding:0 20px; display:flex; align-items:center; font-size:18px; font-weight:500; box-shadow:0 2px 4px rgba(0,0,0,0.1); z-index:10; }
  12. .main-container { display:flex; flex:1; overflow:hidden; }
  13. .sidebar { width:220px; background:#f5f7fa; border-right:1px solid #e8e8e8; padding:16px; box-sizing:border-box; }
  14. .sidebar h3 { margin:0 0 12px 0; font-size:14px; color:#333; }
  15. .content-area { flex:1; padding:20px; overflow:auto; background:#fff; }
  16. .content-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; padding-bottom:10px; border-bottom:1px solid #e8e8e8; }
  17. .btn { display:inline-block; padding:8px 12px; border-radius:6px; border:0; cursor:pointer; }
  18. .btn-primary { background:#1890ff; color:#fff; }
  19. .btn-text { background:transparent; color:#1890ff; border:1px solid transparent; }
  20. table { width:100%; border-collapse:collapse; background:#fff; }
  21. th, td { padding:12px 8px; border-bottom:1px solid #f0f0f0; text-align:center; font-size:14px; }
  22. th { background:#fafafa; font-weight:600; }
  23. .pagination-container { display:flex; justify-content:flex-end; align-items:center; margin-top:10px; gap:8px; }
  24. select, input[type="number"] { padding:6px; border-radius:4px; border:1px solid #dcdcdc; }
  25. /* 弹窗(模态) */
  26. .modal-mask { position:fixed; left:0; top:0; right:0; bottom:0; background:rgba(0,0,0,0.4); display:none; align-items:center; justify-content:center; z-index:999; }
  27. .modal { width:620px; background:#fff; border-radius:6px; padding:18px; box-shadow:0 8px 24px rgba(0,0,0,0.2); max-height:90vh; overflow:auto; }
  28. .form-row { display:flex; gap:12px; margin-bottom:12px; align-items:center; }
  29. .form-row label { width:110px; text-align:right; margin-right:8px; font-size:14px; color:#333; }
  30. .form-row .field { flex:1; }
  31. input[type="text"], textarea, input[type="datetime-local"] { width:100%; padding:8px; border:1px solid #dcdcdc; border-radius:4px; box-sizing:border-box; }
  32. textarea { min-height:70px; resize:vertical; }
  33. .upload-tip, .intro-tip { font-size:12px; color:#999; margin-top:6px; }
  34. .dialog-footer { display:flex; justify-content:flex-end; gap:8px; padding-top:8px; border-top:1px solid #f0f0f0; margin-top:10px; }
  35. .img-preview { display:inline-block; max-width:120px; max-height:80px; margin-right:8px; vertical-align:middle; border:1px solid #eee; padding:4px; border-radius:4px; }
  36. .loading { display:inline-block; margin-left:8px; color:#1890ff; font-size:13px; }
  37. .actions button { margin-right:6px; }
  38. #msgBoxTop {
  39. position: fixed;
  40. top: 16px; /* 距离顶部距离,可调整 */
  41. left: 50%;
  42. transform: translateX(-50%) translateY(-8px);
  43. z-index: 2147483647; /* 尽可能置顶 */
  44. min-width: 220px;
  45. max-width: 92%;
  46. padding: 10px 16px;
  47. border-radius: 8px;
  48. color: #fff;
  49. box-shadow: 0 8px 28px rgba(0,0,0,0.18);
  50. display: none;
  51. align-items: center;
  52. justify-content: center;
  53. font-size: 14px;
  54. text-align: center;
  55. white-space: pre-wrap;
  56. pointer-events: none; /* 不阻塞下方交互;如果需要可改为 auto */
  57. }
  58. /* 显示时的动画 */
  59. #msgBoxTop.show {
  60. display: flex;
  61. animation: msg-slide-down 220ms cubic-bezier(.2,.9,.2,1);
  62. transform: translateX(-50%) translateY(0);
  63. }
  64. /* 轻微淡出(移除 show 时也会自然消失)*/
  65. @keyframes msg-slide-down {
  66. from { opacity: 0; transform: translateX(-50%) translateY(-8px); }
  67. to { opacity: 1; transform: translateX(-50%) translateY(0); }
  68. }
  69. /* 颜色主题 */
  70. #msgBoxTop.msg-success { background: #67c23a; }
  71. #msgBoxTop.msg-error { background: #f56c6c; }
  72. </style>
  73. </head>
  74. <body>
  75. <div class="page-container">
  76. <div class="top-bar"><span>落地页管理</span></div>
  77. <div class="main-container">
  78. <div class="sidebar">
  79. <h3>管理菜单</h3>
  80. <div>• 落地页管理</div>
  81. </div>
  82. <div class="content-area">
  83. <div class="content-header">
  84. <div>
  85. <strong>活动列表</strong>
  86. </div>
  87. <div>
  88. <button id="btnAdd" class="btn btn-primary">+ 新增落地页</button>
  89. </div>
  90. </div>
  91. <div id="tableWrap">
  92. <table id="landingTable" aria-describedby="活动列表">
  93. <thead>
  94. <tr>
  95. <th style="width:80px">序号</th>
  96. <th>活动名称</th>
  97. <th style="min-width:220px">活动简介</th>
  98. <th style="min-width:220px">活动时间</th>
  99. <th style="min-width:180px">编辑时间</th>
  100. <th style="width:280px">操作</th>
  101. </tr>
  102. </thead>
  103. <tbody id="tableBody">
  104. <!-- JS 渲染 -->
  105. </tbody>
  106. </table>
  107. <div id="loadingIndicator" style="padding:10px 0; display:none;"><span class="loading">加载中...</span></div>
  108. <div id="noData" style="padding:20px 0; display:none; color:#666; text-align:center;">暂无数据</div>
  109. </div>
  110. <div class="pagination-container" style="margin-top:14px;">
  111. <div><span id="totalCount">0</span></div>
  112. <div style="margin-left:12px;">
  113. 每页
  114. <select id="pageSizeSelect">
  115. <option value="10">10</option>
  116. <option value="20">20</option>
  117. <option value="50">50</option>
  118. </select>
  119. </div>
  120. <div style="margin-left:12px;">
  121. <button id="prevBtn" class="btn">上一页</button>
  122. <span style="margin:0 8px"><span id="currentPage">1</span></span>
  123. <button id="nextBtn" class="btn">下一页</button>
  124. </div>
  125. <!-- <div style="margin-left:12px;">
  126. 跳到 <input id="gotoInput" type="number" min="1" style="width:70px" />
  127. <button id="gotoBtn" class="btn">跳转</button>
  128. </div> -->
  129. </div>
  130. <!-- 模态框:新增 / 编辑 -->
  131. <div id="modalMask" class="modal-mask" role="dialog" aria-modal="true">
  132. <div class="modal" role="document">
  133. <h3 id="modalTitle">添加活动</h3>
  134. <div style="padding-top:8px;">
  135. <div class="form-row">
  136. <label>活动名称</label>
  137. <div class="field"><input id="fldName" type="text" /></div>
  138. </div>
  139. <div class="form-row">
  140. <label>活动简介</label>
  141. <div class="field">
  142. <textarea id="fldIntro" maxlength="80" placeholder="活动简介(用于分享展示)"></textarea>
  143. <div class="intro-tip">活动简介会给分享后客户展示,请注意填写内容(最多80字)。</div>
  144. </div>
  145. </div>
  146. <div class="form-row">
  147. <label>活动时间</label>
  148. <div class="field">
  149. <input id="fldStart" type="datetime-local" style="width:200px" />
  150. <span style="margin:0 8px"></span>
  151. <input id="fldEnd" type="datetime-local" style="width:200px" />
  152. </div>
  153. </div>
  154. <div class="form-row">
  155. <label>活动落地页</label>
  156. <div class="field">
  157. <div id="landingPreviewContainer" style="margin-top:8px;"></div>
  158. <input id="inputLanding" type="file" accept="image/*" />
  159. <div class="upload-tip">支持 PNG / JPG / GIF。</div>
  160. </div>
  161. </div>
  162. <div class="form-row">
  163. <label>落地页弹窗</label>
  164. <div class="field">
  165. <div id="popupPreviewContainer" style="margin-top:8px;"></div>
  166. <input id="inputPopup" type="file" accept="image/*" />
  167. <div class="upload-tip">支持 PNG / JPG / GIF。</div>
  168. </div>
  169. </div>
  170. </div>
  171. <div class="dialog-footer">
  172. <button id="cancelBtn" class="btn">取消</button>
  173. <button id="saveBtn" class="btn btn-primary">确定</button>
  174. </div>
  175. </div>
  176. </div>
  177. </div>
  178. </div>
  179. </div>
  180. <script type="module">
  181. import { getLandingListApi, addLandingApi } from './src/api/member.js';
  182. (function () {
  183. const FILE_UPLOAD_URL = 'https://tjapi.hlquant.com/hljwgo/api/file/upload';
  184. // 状态
  185. let tableData = [];
  186. let currentPage = 1;
  187. let pageSize = 10;
  188. let totalCount = 0;
  189. // 上传图片临时状态
  190. let landingFile = null;
  191. let popupFile = null;
  192. let landingUploadedUrl = ''; // 上传后得到的 URL(或编辑模式中已有的 url)
  193. let popupUploadedUrl = '';
  194. // 编辑上下文
  195. let editId = null;
  196. // DOM
  197. const tableBody = document.getElementById('tableBody');
  198. const totalCountEl = document.getElementById('totalCount');
  199. const currentPageEl = document.getElementById('currentPage');
  200. const pageSizeSelect = document.getElementById('pageSizeSelect');
  201. const prevBtn = document.getElementById('prevBtn');
  202. const nextBtn = document.getElementById('nextBtn');
  203. // const gotoInput = document.getElementById('gotoInput');
  204. // const gotoBtn = document.getElementById('gotoBtn');
  205. const btnAdd = document.getElementById('btnAdd');
  206. const modalMask = document.getElementById('modalMask');
  207. const modalTitle = document.getElementById('modalTitle');
  208. const cancelBtn = document.getElementById('cancelBtn');
  209. const saveBtn = document.getElementById('saveBtn');
  210. const fldName = document.getElementById('fldName');
  211. const fldIntro = document.getElementById('fldIntro');
  212. const fldStart = document.getElementById('fldStart');
  213. const fldEnd = document.getElementById('fldEnd');
  214. const inputLanding = document.getElementById('inputLanding');
  215. const inputPopup = document.getElementById('inputPopup');
  216. const landingPreviewContainer = document.getElementById('landingPreviewContainer');
  217. const popupPreviewContainer = document.getElementById('popupPreviewContainer');
  218. const loadingIndicator = document.getElementById('loadingIndicator');
  219. const noData = document.getElementById('noData');
  220. // 工具:显示消息(简单替代 ElMessage)
  221. function showMessage(text, type = 'success', duration = 3000) {
  222. let el = document.getElementById('msgBoxTop');
  223. if (!el) {
  224. el = document.createElement('div');
  225. el.id = 'msgBoxTop';
  226. el.setAttribute('role', 'status');
  227. el.setAttribute('aria-live', 'polite');
  228. document.body.appendChild(el);
  229. }
  230. // 更新内容与样式
  231. el.textContent = text;
  232. el.classList.remove('msg-success', 'msg-error', 'show');
  233. el.classList.add(type === 'error' ? 'msg-error' : 'msg-success');
  234. // 触发显示(重新触发动画)
  235. // pointer-events: none 允许消息不阻塞下层交互;若想阻塞改为 auto
  236. requestAnimationFrame(() => {
  237. el.classList.add('show');
  238. });
  239. // 清除旧定时器并设置新定时器隐藏
  240. if (el._timer) {
  241. clearTimeout(el._timer);
  242. }
  243. el._timer = setTimeout(() => {
  244. el.classList.remove('show');
  245. // 可选:在动画结束后彻底隐藏(防止 display:flex 仍在)
  246. setTimeout(() => {
  247. if (!el.classList.contains('show')) {
  248. el.style.display = 'none';
  249. // 恢复 display 控制以便再次显示时通过 .show 控制显示
  250. el.style.display = '';
  251. }
  252. }, 240);
  253. }, Math.max(800, duration)); // 最少保留 800ms,避免闪烁
  254. }
  255. // 列表加载
  256. async function getLandingList() {
  257. loadingIndicator.style.display = 'inline-block';
  258. noData.style.display = 'none';
  259. tableBody.innerHTML = '';
  260. try {
  261. // 如果你已有后端接口,解除下面注释并使用真实接口(带 page & pageSize)
  262. const res =await getLandingListApi({
  263. page: currentPage,
  264. page_size: pageSize
  265. })
  266. // const json = await res.json();
  267. if (res.code == 200 && res.data) {
  268. tableData = res.data.list
  269. totalCount = res.data.total
  270. renderTable();
  271. } else {
  272. showMessage('获取数据失败', 'error');
  273. }
  274. } catch (err) {
  275. console.error(err);
  276. showMessage('网络异常,请稍后重试', 'error');
  277. } finally {
  278. loadingIndicator.style.display = 'none';
  279. }
  280. }
  281. // 更严格的判断(使用 Date)
  282. function isDialogTimeRangeValid() {
  283. if (!fldStart.value || !fldEnd.value) return true; // 有空值不强制
  284. const s = new Date(fldStart.value);
  285. const e = new Date(fldEnd.value);
  286. return e >= s;
  287. }
  288. function renderTable() {
  289. tableBody.innerHTML = '';
  290. if (!tableData || tableData.length === 0) {
  291. noData.style.display = 'block';
  292. } else {
  293. noData.style.display = 'none';
  294. }
  295. tableData.forEach((row, idx) => {
  296. const tr = document.createElement('tr');
  297. const tdIndex = document.createElement('td'); tdIndex.textContent = (currentPage - 1) * pageSize + idx + 1; tr.appendChild(tdIndex);
  298. const tdName = document.createElement('td'); tdName.textContent = row.name || ''; tr.appendChild(tdName);
  299. const tdIntro = document.createElement('td'); tdIntro.textContent = row.introduction || ''; tr.appendChild(tdIntro);
  300. const tdTime = document.createElement('td'); tdTime.textContent = (row.start_time || '') + ' - ' + (row.end_time || ''); tr.appendChild(tdTime);
  301. const tdUpdated = document.createElement('td'); tdUpdated.textContent = row.updated_at || ''; tr.appendChild(tdUpdated);
  302. const tdOps = document.createElement('td');
  303. tdOps.className = 'actions';
  304. const btnEdit = document.createElement('button'); btnEdit.className = 'btn btn-text'; btnEdit.textContent = '编辑';
  305. btnEdit.addEventListener('click', () => openEditDialog(row));
  306. tdOps.appendChild(btnEdit);
  307. const btnDetail = document.createElement('button'); btnDetail.className = 'btn'; btnDetail.textContent = '详情';
  308. btnDetail.addEventListener('click', () => {
  309. window.location.href = `/parkActivity/adminDetail.html?id=${row.id}`;
  310. });
  311. tdOps.appendChild(btnDetail);
  312. tr.appendChild(tdOps);
  313. tableBody.appendChild(tr);
  314. });
  315. totalCountEl.textContent = totalCount;
  316. currentPageEl.textContent = currentPage;
  317. }
  318. // 分页事件
  319. prevBtn.addEventListener('click', () => {
  320. if (currentPage > 1) {
  321. currentPage--;
  322. getLandingList();
  323. }
  324. });
  325. nextBtn.addEventListener('click', () => {
  326. // 简单判断:如果能跳到下一页,则允许(若你有 totalCount 可严格限制)
  327. const maxPage = Math.max(1, Math.ceil((totalCount || 1) / pageSize));
  328. if (currentPage < maxPage) {
  329. currentPage++;
  330. getLandingList();
  331. }
  332. });
  333. pageSizeSelect.addEventListener('change', (e) => {
  334. pageSize = Number(e.target.value);
  335. currentPage = 1;
  336. getLandingList();
  337. });
  338. // 打开新增弹窗
  339. btnAdd.addEventListener('click', () => {
  340. openAddDialog();
  341. });
  342. // 弹窗打开 / 关闭
  343. function openAddDialog() {
  344. editId = null;
  345. modalTitle.textContent = '添加活动';
  346. fldName.value = '';
  347. fldIntro.value = '';
  348. fldStart.value = '';
  349. fldEnd.value = '';
  350. landingFile = null; popupFile = null;
  351. landingUploadedUrl = ''; popupUploadedUrl = '';
  352. landingPreviewContainer.innerHTML = '';
  353. popupPreviewContainer.innerHTML = '';
  354. inputLanding.value = '';
  355. inputPopup.value = '';
  356. showModal();
  357. }
  358. function openEditDialog(row) {
  359. editId = row.id;
  360. modalTitle.textContent = '编辑活动';
  361. fldName.value = row.name || '';
  362. fldIntro.value = row.introduction || '';
  363. // 尝试解析 row.start_time / end_time 为 datetime-local 格式(YYYY-MM-DDTHH:MM)
  364. fldStart.value = toInputDatetime(row.start_time);
  365. fldEnd.value = toInputDatetime(row.end_time);
  366. landingUploadedUrl = row.landing_page || '';
  367. popupUploadedUrl = row.landing_page_popup || '';
  368. landingPreviewContainer.innerHTML = landingUploadedUrl ? `<img class="img-preview" src="${landingUploadedUrl}" alt="landing"> <button data-remove="landing">删除</button>` : '';
  369. popupPreviewContainer.innerHTML = popupUploadedUrl ? `<img class="img-preview" src="${popupUploadedUrl}" alt="popup"> <button data-remove="popup">删除</button>` : '';
  370. showModal();
  371. }
  372. function toInputDatetime(str) {
  373. if (!str) return '';
  374. // 如果 str 是 "2025-11-01 00:00" 或带秒,替换空格为 T
  375. // 如果已经是 ISO 可直接 substring
  376. const s = str.replace(' ', 'T');
  377. // 截取到分钟
  378. return s.length >= 16 ? s.substring(0,16) : s;
  379. }
  380. function showModal() {
  381. modalMask.style.display = 'flex';
  382. }
  383. function closeModal() {
  384. modalMask.style.display = 'none';
  385. }
  386. cancelBtn.addEventListener('click', () => {
  387. closeModal();
  388. });
  389. // 删除预览的图片(编辑时的删除)
  390. landingPreviewContainer.addEventListener('click', (e) => {
  391. if (e.target && e.target.dataset.remove === 'landing') {
  392. landingUploadedUrl = ''; landingFile = null; landingPreviewContainer.innerHTML = '';
  393. }
  394. });
  395. popupPreviewContainer.addEventListener('click', (e) => {
  396. if (e.target && e.target.dataset.remove === 'popup') {
  397. popupUploadedUrl = ''; popupFile = null; popupPreviewContainer.innerHTML = '';
  398. }
  399. });
  400. // 文件选择处理(预览 + 校验)
  401. inputLanding.addEventListener('change', (e) => {
  402. const f = e.target.files[0];
  403. if (!f) return;
  404. if (f.size > 2 * 1024 * 1024) {
  405. showMessage('图片大小不能超过 2MB', 'error');
  406. e.target.value = '';
  407. if (typeof landingFile !== 'undefined') landingFile = null;
  408. if (landingPreviewContainer) landingPreviewContainer.innerHTML = '';
  409. return;
  410. }
  411. const reader = new FileReader();
  412. reader.onload = function (ev) {
  413. const img = new Image();
  414. img.onload = function () {
  415. // if (img.width > 375) {
  416. // showMessage('落地页图片宽度应 ≤ 375px,请使用合适宽度的图片。', 'error');
  417. // inputLanding.value = '';
  418. // landingFile = null;
  419. // landingPreviewContainer.innerHTML = '';
  420. // return;
  421. // }
  422. landingFile = f;
  423. landingPreviewContainer.innerHTML = '';
  424. const imgEl = document.createElement('img');
  425. imgEl.src = ev.target.result;
  426. imgEl.className = 'img-preview';
  427. landingPreviewContainer.appendChild(imgEl);
  428. };
  429. img.src = ev.target.result;
  430. };
  431. reader.readAsDataURL(f);
  432. });
  433. inputPopup.addEventListener('change', (e) => {
  434. const f = e.target.files[0];
  435. if (!f) return;
  436. if (f.size > 2 * 1024 * 1024) {
  437. showMessage('图片大小不能超过 2MB', 'error');
  438. e.target.value = '';
  439. if (typeof popupFile !== 'undefined') popupFile = null;
  440. if (popupPreviewContainer) popupPreviewContainer.innerHTML = '';
  441. return;
  442. }
  443. const reader = new FileReader();
  444. reader.onload = function (ev) {
  445. popupFile = f;
  446. popupPreviewContainer.innerHTML = '';
  447. const imgEl = document.createElement('img');
  448. imgEl.src = ev.target.result;
  449. imgEl.className = 'img-preview';
  450. popupPreviewContainer.appendChild(imgEl);
  451. };
  452. reader.readAsDataURL(f);
  453. });
  454. saveBtn.addEventListener('click', async () => {
  455. // 简单校验
  456. const name = fldName.value.trim();
  457. const introduction = fldIntro.value.trim();
  458. const startTime = fldStart.value;
  459. const endTime = fldEnd.value;
  460. if (!name) { showMessage('请输入活动名称', 'error'); return; }
  461. if (!introduction) { showMessage('请输入活动简介', 'error'); return; }
  462. if (!startTime) { showMessage('请选择开始时间', 'error'); return; }
  463. if (!endTime) { showMessage('请选择结束时间', 'error'); return; }
  464. if (!isDialogTimeRangeValid()) {
  465. showMessage('结束时间不能早于开始时间,请调整后保存', 'error');
  466. return;
  467. }
  468. // 如果既没有已上传的 url,也没有本地文件,则提示
  469. if (!landingUploadedUrl && !landingFile) { showMessage('请上传活动落地页', 'error'); return; }
  470. if (!popupUploadedUrl && !popupFile) { showMessage('请上传落地页弹窗', 'error'); return; }
  471. saveBtn.disabled = true;
  472. saveBtn.textContent = '提交中...';
  473. try {
  474. // 1) 若用户选择了新的图片文件则先上传图片
  475. if (landingFile) {
  476. const uploaded = await uploadFile(landingFile);
  477. if (!uploaded.success) { showMessage('落地页图片上传失败', 'error'); saveBtn.disabled = false; saveBtn.textContent = '确定'; return; }
  478. landingUploadedUrl = uploaded.url;
  479. }
  480. if (popupFile) {
  481. const uploaded = await uploadFile(popupFile);
  482. if (!uploaded.success) { showMessage('弹窗图片上传失败', 'error'); saveBtn.disabled = false; saveBtn.textContent = '确定'; return; }
  483. popupUploadedUrl = uploaded.url;
  484. }
  485. const res = await addLandingApi({
  486. id: editId,
  487. name:name,
  488. introduction:introduction,
  489. start_time: formatDatetimeLocal(startTime),
  490. end_time: formatDatetimeLocal(endTime),
  491. landing_page: landingUploadedUrl,
  492. landing_page_popup: popupUploadedUrl
  493. })
  494. if (res.code == 200) {
  495. showMessage('操作成功');
  496. closeModal();
  497. getLandingList();
  498. } else {
  499. showMessage(json.msg || '操作失败', 'error');
  500. }
  501. } catch (err) {
  502. console.error(err);
  503. showMessage('网络异常', 'error');
  504. } finally {
  505. saveBtn.disabled = false;
  506. saveBtn.textContent = '确定';
  507. }
  508. });
  509. async function uploadFile(file) {
  510. const fd = new FormData();
  511. fd.append('file', file);
  512. fd.append('type', 'image');
  513. fd.append('app_from', 'toujiao');
  514. try {
  515. const resp = await fetch(FILE_UPLOAD_URL, {
  516. method: 'POST',
  517. body: fd
  518. });
  519. const json = await resp.json();
  520. if (json && ((json.code && json.code === 200) || json.data)) {
  521. const url = (json.data && (json.data.url || json.data.file_name)) || json.url || '';
  522. return { success: true, url: url };
  523. } else {
  524. return { success: false };
  525. }
  526. } catch (err) {
  527. console.error('uploadFile error', err);
  528. return { success: false };
  529. }
  530. }
  531. function formatDatetimeLocal(v) {
  532. // v like "2025-10-23T14:00" -> "2025-10-23 14:00"
  533. return v ? v.replace('T', ' ') : '';
  534. }
  535. // 初始加载
  536. getLandingList();
  537. })();
  538. </script>
  539. </body>
  540. </html>