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

574 lines
22 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
  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 = 'http://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. function renderTable() {
  282. tableBody.innerHTML = '';
  283. if (!tableData || tableData.length === 0) {
  284. noData.style.display = 'block';
  285. } else {
  286. noData.style.display = 'none';
  287. }
  288. tableData.forEach((row, idx) => {
  289. const tr = document.createElement('tr');
  290. const tdIndex = document.createElement('td'); tdIndex.textContent = (currentPage - 1) * pageSize + idx + 1; tr.appendChild(tdIndex);
  291. const tdName = document.createElement('td'); tdName.textContent = row.name || ''; tr.appendChild(tdName);
  292. const tdIntro = document.createElement('td'); tdIntro.textContent = row.introduction || ''; tr.appendChild(tdIntro);
  293. const tdTime = document.createElement('td'); tdTime.textContent = (row.start_time || '') + ' - ' + (row.end_time || ''); tr.appendChild(tdTime);
  294. const tdUpdated = document.createElement('td'); tdUpdated.textContent = row.updated_at || ''; tr.appendChild(tdUpdated);
  295. const tdOps = document.createElement('td');
  296. tdOps.className = 'actions';
  297. const btnEdit = document.createElement('button'); btnEdit.className = 'btn btn-text'; btnEdit.textContent = '编辑';
  298. btnEdit.addEventListener('click', () => openEditDialog(row));
  299. tdOps.appendChild(btnEdit);
  300. const btnDetail = document.createElement('button'); btnDetail.className = 'btn'; btnDetail.textContent = '详情';
  301. btnDetail.addEventListener('click', () => {
  302. window.location.href = `/adminDetail.html?id=${row.id}`;
  303. });
  304. tdOps.appendChild(btnDetail);
  305. tr.appendChild(tdOps);
  306. tableBody.appendChild(tr);
  307. });
  308. totalCountEl.textContent = totalCount;
  309. currentPageEl.textContent = currentPage;
  310. }
  311. // 分页事件
  312. prevBtn.addEventListener('click', () => {
  313. if (currentPage > 1) {
  314. currentPage--;
  315. getLandingList();
  316. }
  317. });
  318. nextBtn.addEventListener('click', () => {
  319. // 简单判断:如果能跳到下一页,则允许(若你有 totalCount 可严格限制)
  320. const maxPage = Math.max(1, Math.ceil((totalCount || 1) / pageSize));
  321. if (currentPage < maxPage) {
  322. currentPage++;
  323. getLandingList();
  324. }
  325. });
  326. pageSizeSelect.addEventListener('change', (e) => {
  327. pageSize = Number(e.target.value);
  328. currentPage = 1;
  329. getLandingList();
  330. });
  331. // gotoBtn.addEventListener('click', () => {
  332. // const v = parseInt(gotoInput.value, 10);
  333. // if (!isNaN(v) && v >= 1) {
  334. // currentPage = v;
  335. // getLandingList();
  336. // }
  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. const reader = new FileReader();
  405. reader.onload = function (ev) {
  406. const img = new Image();
  407. img.onload = function () {
  408. // if (img.width > 375) {
  409. // showMessage('落地页图片宽度应 ≤ 375px,请使用合适宽度的图片。', 'error');
  410. // inputLanding.value = '';
  411. // landingFile = null;
  412. // landingPreviewContainer.innerHTML = '';
  413. // return;
  414. // }
  415. landingFile = f;
  416. landingPreviewContainer.innerHTML = '';
  417. const imgEl = document.createElement('img');
  418. imgEl.src = ev.target.result;
  419. imgEl.className = 'img-preview';
  420. landingPreviewContainer.appendChild(imgEl);
  421. };
  422. img.src = ev.target.result;
  423. };
  424. reader.readAsDataURL(f);
  425. });
  426. inputPopup.addEventListener('change', (e) => {
  427. const f = e.target.files[0];
  428. if (!f) return;
  429. const reader = new FileReader();
  430. reader.onload = function (ev) {
  431. popupFile = f;
  432. popupPreviewContainer.innerHTML = '';
  433. const imgEl = document.createElement('img');
  434. imgEl.src = ev.target.result;
  435. imgEl.className = 'img-preview';
  436. popupPreviewContainer.appendChild(imgEl);
  437. };
  438. reader.readAsDataURL(f);
  439. });
  440. saveBtn.addEventListener('click', async () => {
  441. // 简单校验
  442. const name = fldName.value.trim();
  443. const introduction = fldIntro.value.trim();
  444. const startTime = fldStart.value;
  445. const endTime = fldEnd.value;
  446. if (!name) { showMessage('请输入活动名称', 'error'); return; }
  447. if (!introduction) { showMessage('请输入活动简介', 'error'); return; }
  448. if (!startTime) { showMessage('请选择开始时间', 'error'); return; }
  449. if (!endTime) { showMessage('请选择结束时间', 'error'); return; }
  450. // 如果既没有已上传的 url,也没有本地文件,则提示
  451. if (!landingUploadedUrl && !landingFile) { showMessage('请上传活动落地页', 'error'); return; }
  452. if (!popupUploadedUrl && !popupFile) { showMessage('请上传落地页弹窗', 'error'); return; }
  453. saveBtn.disabled = true;
  454. saveBtn.textContent = '提交中...';
  455. try {
  456. // 1) 若用户选择了新的图片文件则先上传图片
  457. if (landingFile) {
  458. const uploaded = await uploadFile(landingFile);
  459. if (!uploaded.success) { showMessage('落地页图片上传失败', 'error'); saveBtn.disabled = false; saveBtn.textContent = '确定'; return; }
  460. landingUploadedUrl = uploaded.url;
  461. }
  462. if (popupFile) {
  463. const uploaded = await uploadFile(popupFile);
  464. if (!uploaded.success) { showMessage('弹窗图片上传失败', 'error'); saveBtn.disabled = false; saveBtn.textContent = '确定'; return; }
  465. popupUploadedUrl = uploaded.url;
  466. }
  467. const res = await addLandingApi({
  468. id: editId,
  469. name:name,
  470. introduction:introduction,
  471. start_time: formatDatetimeLocal(startTime),
  472. end_time: formatDatetimeLocal(endTime),
  473. landing_page: landingUploadedUrl,
  474. landing_page_popup: popupUploadedUrl
  475. })
  476. if (res.code == 200) {
  477. showMessage('操作成功');
  478. closeModal();
  479. getLandingList();
  480. } else {
  481. showMessage(json.msg || '操作失败', 'error');
  482. }
  483. } catch (err) {
  484. console.error(err);
  485. showMessage('网络异常', 'error');
  486. } finally {
  487. saveBtn.disabled = false;
  488. saveBtn.textContent = '确定';
  489. }
  490. });
  491. async function uploadFile(file) {
  492. const fd = new FormData();
  493. fd.append('file', file);
  494. fd.append('type', 'image');
  495. fd.append('app_from', 'toujiao');
  496. try {
  497. const resp = await fetch(FILE_UPLOAD_URL, {
  498. method: 'POST',
  499. body: fd
  500. });
  501. const json = await resp.json();
  502. if (json && ((json.code && json.code === 200) || json.data)) {
  503. const url = (json.data && (json.data.url || json.data.file_name)) || json.url || '';
  504. return { success: true, url: url };
  505. } else {
  506. return { success: false };
  507. }
  508. } catch (err) {
  509. console.error('uploadFile error', err);
  510. return { success: false };
  511. }
  512. }
  513. function formatDatetimeLocal(v) {
  514. // v like "2025-10-23T14:00" -> "2025-10-23 14:00"
  515. return v ? v.replace('T', ' ') : '';
  516. }
  517. // 初始加载
  518. getLandingList();
  519. })();
  520. </script>
  521. </body>
  522. </html>