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

575 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
  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. // 配置(按需修改)
  184. const FILE_UPLOAD_URL = 'http://39.101.133.168:8828/hljwgo/api/file/upload';
  185. // 状态
  186. let tableData = [];
  187. let currentPage = 1;
  188. let pageSize = 10;
  189. let totalCount = 0;
  190. // 上传图片临时状态
  191. let landingFile = null;
  192. let popupFile = null;
  193. let landingUploadedUrl = ''; // 上传后得到的 URL(或编辑模式中已有的 url)
  194. let popupUploadedUrl = '';
  195. // 编辑上下文
  196. let editId = null;
  197. // DOM
  198. const tableBody = document.getElementById('tableBody');
  199. const totalCountEl = document.getElementById('totalCount');
  200. const currentPageEl = document.getElementById('currentPage');
  201. const pageSizeSelect = document.getElementById('pageSizeSelect');
  202. const prevBtn = document.getElementById('prevBtn');
  203. const nextBtn = document.getElementById('nextBtn');
  204. // const gotoInput = document.getElementById('gotoInput');
  205. // const gotoBtn = document.getElementById('gotoBtn');
  206. const btnAdd = document.getElementById('btnAdd');
  207. const modalMask = document.getElementById('modalMask');
  208. const modalTitle = document.getElementById('modalTitle');
  209. const cancelBtn = document.getElementById('cancelBtn');
  210. const saveBtn = document.getElementById('saveBtn');
  211. const fldName = document.getElementById('fldName');
  212. const fldIntro = document.getElementById('fldIntro');
  213. const fldStart = document.getElementById('fldStart');
  214. const fldEnd = document.getElementById('fldEnd');
  215. const inputLanding = document.getElementById('inputLanding');
  216. const inputPopup = document.getElementById('inputPopup');
  217. const landingPreviewContainer = document.getElementById('landingPreviewContainer');
  218. const popupPreviewContainer = document.getElementById('popupPreviewContainer');
  219. const loadingIndicator = document.getElementById('loadingIndicator');
  220. const noData = document.getElementById('noData');
  221. // 工具:显示消息(简单替代 ElMessage)
  222. function showMessage(text, type = 'success', duration = 3000) {
  223. let el = document.getElementById('msgBoxTop');
  224. if (!el) {
  225. el = document.createElement('div');
  226. el.id = 'msgBoxTop';
  227. el.setAttribute('role', 'status');
  228. el.setAttribute('aria-live', 'polite');
  229. document.body.appendChild(el);
  230. }
  231. // 更新内容与样式
  232. el.textContent = text;
  233. el.classList.remove('msg-success', 'msg-error', 'show');
  234. el.classList.add(type === 'error' ? 'msg-error' : 'msg-success');
  235. // 触发显示(重新触发动画)
  236. // pointer-events: none 允许消息不阻塞下层交互;若想阻塞改为 auto
  237. requestAnimationFrame(() => {
  238. el.classList.add('show');
  239. });
  240. // 清除旧定时器并设置新定时器隐藏
  241. if (el._timer) {
  242. clearTimeout(el._timer);
  243. }
  244. el._timer = setTimeout(() => {
  245. el.classList.remove('show');
  246. // 可选:在动画结束后彻底隐藏(防止 display:flex 仍在)
  247. setTimeout(() => {
  248. if (!el.classList.contains('show')) {
  249. el.style.display = 'none';
  250. // 恢复 display 控制以便再次显示时通过 .show 控制显示
  251. el.style.display = '';
  252. }
  253. }, 240);
  254. }, Math.max(800, duration)); // 最少保留 800ms,避免闪烁
  255. }
  256. // 列表加载
  257. async function getLandingList() {
  258. loadingIndicator.style.display = 'inline-block';
  259. noData.style.display = 'none';
  260. tableBody.innerHTML = '';
  261. try {
  262. // 如果你已有后端接口,解除下面注释并使用真实接口(带 page & pageSize)
  263. const res =await getLandingListApi({
  264. page: currentPage,
  265. page_size: pageSize
  266. })
  267. // const json = await res.json();
  268. if (res.code == 200 && res.data) {
  269. tableData = res.data.list
  270. totalCount = res.data.total
  271. renderTable();
  272. } else {
  273. showMessage('获取数据失败', 'error');
  274. }
  275. } catch (err) {
  276. console.error(err);
  277. showMessage('网络异常,请稍后重试', 'error');
  278. } finally {
  279. loadingIndicator.style.display = 'none';
  280. }
  281. }
  282. function renderTable() {
  283. tableBody.innerHTML = '';
  284. if (!tableData || tableData.length === 0) {
  285. noData.style.display = 'block';
  286. } else {
  287. noData.style.display = 'none';
  288. }
  289. tableData.forEach((row, idx) => {
  290. const tr = document.createElement('tr');
  291. const tdIndex = document.createElement('td'); tdIndex.textContent = (currentPage - 1) * pageSize + idx + 1; tr.appendChild(tdIndex);
  292. const tdName = document.createElement('td'); tdName.textContent = row.name || ''; tr.appendChild(tdName);
  293. const tdIntro = document.createElement('td'); tdIntro.textContent = row.introduction || ''; tr.appendChild(tdIntro);
  294. const tdTime = document.createElement('td'); tdTime.textContent = (row.start_time || '') + ' - ' + (row.end_time || ''); tr.appendChild(tdTime);
  295. const tdUpdated = document.createElement('td'); tdUpdated.textContent = row.updated_at || ''; tr.appendChild(tdUpdated);
  296. const tdOps = document.createElement('td');
  297. tdOps.className = 'actions';
  298. const btnEdit = document.createElement('button'); btnEdit.className = 'btn btn-text'; btnEdit.textContent = '编辑';
  299. btnEdit.addEventListener('click', () => openEditDialog(row));
  300. tdOps.appendChild(btnEdit);
  301. const btnDetail = document.createElement('button'); btnDetail.className = 'btn'; btnDetail.textContent = '详情';
  302. btnDetail.addEventListener('click', () => {
  303. window.location.href = `/adminDetail?id=${row.id}`;
  304. });
  305. tdOps.appendChild(btnDetail);
  306. tr.appendChild(tdOps);
  307. tableBody.appendChild(tr);
  308. });
  309. totalCountEl.textContent = totalCount;
  310. currentPageEl.textContent = currentPage;
  311. }
  312. // 分页事件
  313. prevBtn.addEventListener('click', () => {
  314. if (currentPage > 1) {
  315. currentPage--;
  316. getLandingList();
  317. }
  318. });
  319. nextBtn.addEventListener('click', () => {
  320. // 简单判断:如果能跳到下一页,则允许(若你有 totalCount 可严格限制)
  321. const maxPage = Math.max(1, Math.ceil((totalCount || 1) / pageSize));
  322. if (currentPage < maxPage) {
  323. currentPage++;
  324. getLandingList();
  325. }
  326. });
  327. pageSizeSelect.addEventListener('change', (e) => {
  328. pageSize = Number(e.target.value);
  329. currentPage = 1;
  330. getLandingList();
  331. });
  332. // gotoBtn.addEventListener('click', () => {
  333. // const v = parseInt(gotoInput.value, 10);
  334. // if (!isNaN(v) && v >= 1) {
  335. // currentPage = v;
  336. // getLandingList();
  337. // }
  338. // });
  339. // 打开新增弹窗
  340. btnAdd.addEventListener('click', () => {
  341. openAddDialog();
  342. });
  343. // 弹窗打开 / 关闭
  344. function openAddDialog() {
  345. editId = null;
  346. modalTitle.textContent = '添加活动';
  347. fldName.value = '';
  348. fldIntro.value = '';
  349. fldStart.value = '';
  350. fldEnd.value = '';
  351. landingFile = null; popupFile = null;
  352. landingUploadedUrl = ''; popupUploadedUrl = '';
  353. landingPreviewContainer.innerHTML = '';
  354. popupPreviewContainer.innerHTML = '';
  355. inputLanding.value = '';
  356. inputPopup.value = '';
  357. showModal();
  358. }
  359. function openEditDialog(row) {
  360. editId = row.id;
  361. modalTitle.textContent = '编辑活动';
  362. fldName.value = row.name || '';
  363. fldIntro.value = row.introduction || '';
  364. // 尝试解析 row.start_time / end_time 为 datetime-local 格式(YYYY-MM-DDTHH:MM)
  365. fldStart.value = toInputDatetime(row.start_time);
  366. fldEnd.value = toInputDatetime(row.end_time);
  367. landingUploadedUrl = row.landing_page || '';
  368. popupUploadedUrl = row.landing_page_popup || '';
  369. landingPreviewContainer.innerHTML = landingUploadedUrl ? `<img class="img-preview" src="${landingUploadedUrl}" alt="landing"> <button data-remove="landing">删除</button>` : '';
  370. popupPreviewContainer.innerHTML = popupUploadedUrl ? `<img class="img-preview" src="${popupUploadedUrl}" alt="popup"> <button data-remove="popup">删除</button>` : '';
  371. showModal();
  372. }
  373. function toInputDatetime(str) {
  374. if (!str) return '';
  375. // 如果 str 是 "2025-11-01 00:00" 或带秒,替换空格为 T
  376. // 如果已经是 ISO 可直接 substring
  377. const s = str.replace(' ', 'T');
  378. // 截取到分钟
  379. return s.length >= 16 ? s.substring(0,16) : s;
  380. }
  381. function showModal() {
  382. modalMask.style.display = 'flex';
  383. }
  384. function closeModal() {
  385. modalMask.style.display = 'none';
  386. }
  387. cancelBtn.addEventListener('click', () => {
  388. closeModal();
  389. });
  390. // 删除预览的图片(编辑时的删除)
  391. landingPreviewContainer.addEventListener('click', (e) => {
  392. if (e.target && e.target.dataset.remove === 'landing') {
  393. landingUploadedUrl = ''; landingFile = null; landingPreviewContainer.innerHTML = '';
  394. }
  395. });
  396. popupPreviewContainer.addEventListener('click', (e) => {
  397. if (e.target && e.target.dataset.remove === 'popup') {
  398. popupUploadedUrl = ''; popupFile = null; popupPreviewContainer.innerHTML = '';
  399. }
  400. });
  401. // 文件选择处理(预览 + 校验)
  402. inputLanding.addEventListener('change', (e) => {
  403. const f = e.target.files[0];
  404. if (!f) return;
  405. const reader = new FileReader();
  406. reader.onload = function (ev) {
  407. const img = new Image();
  408. img.onload = function () {
  409. // if (img.width > 375) {
  410. // showMessage('落地页图片宽度应 ≤ 375px,请使用合适宽度的图片。', 'error');
  411. // inputLanding.value = '';
  412. // landingFile = null;
  413. // landingPreviewContainer.innerHTML = '';
  414. // return;
  415. // }
  416. landingFile = f;
  417. landingPreviewContainer.innerHTML = '';
  418. const imgEl = document.createElement('img');
  419. imgEl.src = ev.target.result;
  420. imgEl.className = 'img-preview';
  421. landingPreviewContainer.appendChild(imgEl);
  422. };
  423. img.src = ev.target.result;
  424. };
  425. reader.readAsDataURL(f);
  426. });
  427. inputPopup.addEventListener('change', (e) => {
  428. const f = e.target.files[0];
  429. if (!f) return;
  430. const reader = new FileReader();
  431. reader.onload = function (ev) {
  432. popupFile = f;
  433. popupPreviewContainer.innerHTML = '';
  434. const imgEl = document.createElement('img');
  435. imgEl.src = ev.target.result;
  436. imgEl.className = 'img-preview';
  437. popupPreviewContainer.appendChild(imgEl);
  438. };
  439. reader.readAsDataURL(f);
  440. });
  441. saveBtn.addEventListener('click', async () => {
  442. // 简单校验
  443. const name = fldName.value.trim();
  444. const introduction = fldIntro.value.trim();
  445. const startTime = fldStart.value;
  446. const endTime = fldEnd.value;
  447. if (!name) { showMessage('请输入活动名称', 'error'); return; }
  448. if (!introduction) { showMessage('请输入活动简介', 'error'); return; }
  449. if (!startTime) { showMessage('请选择开始时间', 'error'); return; }
  450. if (!endTime) { showMessage('请选择结束时间', 'error'); return; }
  451. // 如果既没有已上传的 url,也没有本地文件,则提示
  452. if (!landingUploadedUrl && !landingFile) { showMessage('请上传活动落地页', 'error'); return; }
  453. if (!popupUploadedUrl && !popupFile) { showMessage('请上传落地页弹窗', 'error'); return; }
  454. saveBtn.disabled = true;
  455. saveBtn.textContent = '提交中...';
  456. try {
  457. // 1) 若用户选择了新的图片文件则先上传图片
  458. if (landingFile) {
  459. const uploaded = await uploadFile(landingFile);
  460. if (!uploaded.success) { showMessage('落地页图片上传失败', 'error'); saveBtn.disabled = false; saveBtn.textContent = '确定'; return; }
  461. landingUploadedUrl = uploaded.url;
  462. }
  463. if (popupFile) {
  464. const uploaded = await uploadFile(popupFile);
  465. if (!uploaded.success) { showMessage('弹窗图片上传失败', 'error'); saveBtn.disabled = false; saveBtn.textContent = '确定'; return; }
  466. popupUploadedUrl = uploaded.url;
  467. }
  468. const res = await addLandingApi({
  469. id: editId,
  470. name:name,
  471. introduction:introduction,
  472. start_time: formatDatetimeLocal(startTime),
  473. end_time: formatDatetimeLocal(endTime),
  474. landing_page: landingUploadedUrl,
  475. landing_page_popup: popupUploadedUrl
  476. })
  477. if (res.code == 200) {
  478. showMessage('操作成功');
  479. closeModal();
  480. getLandingList();
  481. } else {
  482. showMessage(json.msg || '操作失败', 'error');
  483. }
  484. } catch (err) {
  485. console.error(err);
  486. showMessage('网络异常', 'error');
  487. } finally {
  488. saveBtn.disabled = false;
  489. saveBtn.textContent = '确定';
  490. }
  491. });
  492. async function uploadFile(file) {
  493. const fd = new FormData();
  494. fd.append('file', file);
  495. fd.append('type', 'image');
  496. fd.append('app_from', 'toujiao');
  497. try {
  498. const resp = await fetch(FILE_UPLOAD_URL, {
  499. method: 'POST',
  500. body: fd
  501. });
  502. const json = await resp.json();
  503. if (json && ((json.code && json.code === 200) || json.data)) {
  504. const url = (json.data && (json.data.url || json.data.file_name)) || json.url || '';
  505. return { success: true, url: url };
  506. } else {
  507. return { success: false };
  508. }
  509. } catch (err) {
  510. console.error('uploadFile error', err);
  511. return { success: false };
  512. }
  513. }
  514. function formatDatetimeLocal(v) {
  515. // v like "2025-10-23T14:00" -> "2025-10-23 14:00"
  516. return v ? v.replace('T', ' ') : '';
  517. }
  518. // 初始加载
  519. getLandingList();
  520. })();
  521. </script>
  522. </body>
  523. </html>