update-case-cover-images.html 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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.0" />
  6. <title>批量更新案例封面为家装图片</title>
  7. <style>
  8. body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif; background: #f5f7fb; margin: 0; }
  9. .container { max-width: 980px; margin: 32px auto; background: #fff; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.06); overflow: hidden; }
  10. .header { padding: 24px 28px; background: linear-gradient(135deg,#667eea,#764ba2); color: #fff; }
  11. .header h1 { margin: 0 0 6px; font-size: 20px; }
  12. .header p { margin: 0; opacity: .9; font-size: 13px; }
  13. .content { padding: 22px 28px; }
  14. .btn { display: inline-flex; align-items: center; gap: 8px; padding: 12px 18px; border: 0; border-radius: 10px; cursor: pointer; font-weight: 600; }
  15. .btn-primary { background: linear-gradient(135deg,#667eea,#764ba2); color: #fff; }
  16. .btn-secondary { background: #eef2ff; color: #3730a3; }
  17. .btn:disabled { opacity: .6; cursor: not-allowed; }
  18. .stat { display:flex; gap:12px; margin: 16px 0 6px; }
  19. .stat .card { flex:1; background:#f9fafb; border-radius:10px; padding:12px 14px; }
  20. .card h4 { margin:0 0 2px; font-size:13px; color:#6b7280; }
  21. .card div { font-size:22px; font-weight:700; }
  22. .log { background:#0b1220; color:#bcd1ff; border-radius:10px; padding:14px; height: 240px; overflow:auto; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-size: 12px; }
  23. .grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(180px,1fr)); gap:12px; margin-top:16px; }
  24. .item { background:#f8fafc; border-radius:10px; overflow:hidden; border:1px solid #e5e7eb; }
  25. .item img { width:100%; height:120px; object-fit:cover; display:block; }
  26. .item .meta { padding:8px 10px; font-size:12px; color:#374151; }
  27. </style>
  28. </head>
  29. <body>
  30. <div class="container">
  31. <div class="header">
  32. <h1>批量更新案例封面为家装图片</h1>
  33. <p>优先使用已上传图片;若没有,则使用默认家装设计图片(Unsplash)。</p>
  34. </div>
  35. <div class="content">
  36. <div style="display:flex; gap:10px; align-items:center; margin-bottom:14px;">
  37. <button id="run" class="btn btn-primary">🚀 开始更新封面图片</button>
  38. <button id="preview" class="btn btn-secondary">👀 仅预览将被更新的案例</button>
  39. </div>
  40. <div class="stat">
  41. <div class="card"><h4>总案例</h4><div id="total">-</div></div>
  42. <div class="card"><h4>需更新</h4><div id="need">-</div></div>
  43. <div class="card"><h4>已成功</h4><div id="ok">0</div></div>
  44. <div class="card"><h4>失败</h4><div id="fail">0</div></div>
  45. </div>
  46. <div class="log" id="log"></div>
  47. <div class="grid" id="grid"></div>
  48. </div>
  49. </div>
  50. <script type="module">
  51. const logEl = document.getElementById('log');
  52. const gridEl = document.getElementById('grid');
  53. const totalEl = document.getElementById('total');
  54. const needEl = document.getElementById('need');
  55. const okEl = document.getElementById('ok');
  56. const failEl = document.getElementById('fail');
  57. const defaultInteriorImages = [
  58. 'https://images.unsplash.com/photo-1600210492486-724fe5c67fb0?w=1200&h=800&fit=crop',
  59. 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=1200&h=800&fit=crop',
  60. 'https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?w=1200&h=800&fit=crop',
  61. 'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=1200&h=800&fit=crop',
  62. 'https://images.unsplash.com/photo-1600566753086-00f18fb6b3ea?w=1200&h=800&fit=crop',
  63. 'https://images.unsplash.com/photo-1600573472550-8090b5e0745e?w=1200&h=800&fit=crop',
  64. 'https://images.unsplash.com/photo-1600585154526-990dced4db0d?w=1200&h=800&fit=crop',
  65. 'https://images.unsplash.com/photo-1600566752355-35792bedcfea?w=1200&h=800&fit=crop',
  66. 'https://images.unsplash.com/photo-1600585154154-1eab6d02deae?w=1200&h=800&fit=crop',
  67. 'https://images.unsplash.com/photo-1600585153820-98d9849a432d?w=1200&h=800&fit=crop',
  68. 'https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?w=1200&h=800&fit=crop',
  69. 'https://images.unsplash.com/photo-1505691723518-36a5ac3b2dfe?w=1200&h=800&fit=crop',
  70. 'https://images.unsplash.com/photo-1505691938895-1758d7feb511?w=1200&h=800&fit=crop',
  71. 'https://images.unsplash.com/photo-1579632652768-1c0cbe20a3c1?w=1200&h=800&fit=crop',
  72. 'https://images.unsplash.com/photo-1565182999561-18d7f0b2b4e1?w=1200&h=800&fit=crop'
  73. ];
  74. const sleep = ms => new Promise(r => setTimeout(r, ms));
  75. const log = (...args) => { logEl.textContent += `[${new Date().toLocaleTimeString()}] ` + args.join(' ') + '\n'; logEl.scrollTop = logEl.scrollHeight; };
  76. const isPlaceholder = (url='') => !url || url.includes('placeholder') || url.endsWith('.svg') || url.includes('assets/images');
  77. const pickImages = (count=4) => {
  78. const start = Math.floor(Math.random() * defaultInteriorImages.length);
  79. return Array.from({length: count}).map((_,i) => defaultInteriorImages[(start+i)%defaultInteriorImages.length]);
  80. };
  81. async function fetchCases(Parse) {
  82. const cid = localStorage.getItem('company');
  83. if (!cid) throw new Error('未发现公司ID (localStorage.company)');
  84. const q = new Parse.Query('Case');
  85. q.equalTo('company', { __type: 'Pointer', className: 'Company', objectId: cid });
  86. q.notEqualTo('isDeleted', true);
  87. q.limit(1000);
  88. const list = await q.find();
  89. return list;
  90. }
  91. function needsUpdate(caseObj) {
  92. const cover = caseObj.get('coverImage') || '';
  93. const imgs = caseObj.get('images') || [];
  94. const hasRealUploaded = Array.isArray(imgs) && imgs.some(u => u && !isPlaceholder(u) && u.startsWith('http'));
  95. // 需要更新的条件:封面是占位符/空;或者没有任何真实图片
  96. return isPlaceholder(cover) || !hasRealUploaded;
  97. }
  98. async function preview() {
  99. try {
  100. const Parse = await import('https://cdn.skypack.dev/fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
  101. const cases = await fetchCases(Parse);
  102. totalEl.textContent = String(cases.length);
  103. const targets = cases.filter(needsUpdate);
  104. needEl.textContent = String(targets.length);
  105. log(`📊 找到 ${cases.length} 个案例,其中 ${targets.length} 个需要更新`);
  106. gridEl.innerHTML = '';
  107. targets.slice(0, 30).forEach((c, idx) => {
  108. const img = (c.get('images') || []).find(u => u && !isPlaceholder(u)) || defaultInteriorImages[idx % defaultInteriorImages.length];
  109. const div = document.createElement('div');
  110. div.className = 'item';
  111. div.innerHTML = `<img src="${img}" alt="case"/><div class="meta">${c.get('name') || c.id}</div>`;
  112. gridEl.appendChild(div);
  113. });
  114. } catch (e) {
  115. log('❌ 预览失败:', e.message || e);
  116. }
  117. }
  118. async function run() {
  119. const btn = document.getElementById('run');
  120. btn.disabled = true;
  121. try {
  122. const Parse = await import('https://cdn.skypack.dev/fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
  123. const cases = await fetchCases(Parse);
  124. totalEl.textContent = String(cases.length);
  125. const targets = cases.filter(needsUpdate);
  126. needEl.textContent = String(targets.length);
  127. log(`🚀 开始更新 ${targets.length} 个案例封面...`);
  128. let ok = 0, fail = 0, idx = 0;
  129. for (const c of targets) {
  130. try {
  131. const imgs = c.get('images') || [];
  132. const realImgs = imgs.filter(u => u && !isPlaceholder(u) && u.startsWith('http'));
  133. const useImgs = realImgs.length > 0 ? realImgs : pickImages(4);
  134. const cover = useImgs[0];
  135. c.set('coverImage', cover);
  136. if (realImgs.length === 0) {
  137. c.set('images', useImgs);
  138. }
  139. await c.save();
  140. ok++;
  141. okEl.textContent = String(ok);
  142. idx++;
  143. if (idx % 3 === 0) await sleep(200); // 温和些
  144. log(`✅ 已更新: ${c.get('name') || c.id}`);
  145. } catch (err) {
  146. fail++;
  147. failEl.textContent = String(fail);
  148. log('❌ 更新失败: ', (c.get && c.get('name')) || c.id, '-', err.message || err);
  149. }
  150. }
  151. log(`🎉 完成!成功 ${ok},失败 ${fail}`);
  152. await preview();
  153. } catch (e) {
  154. log('❌ 执行失败:', e.message || e);
  155. } finally {
  156. btn.disabled = false;
  157. }
  158. }
  159. document.getElementById('preview').addEventListener('click', preview);
  160. document.getElementById('run').addEventListener('click', run);
  161. // 自动预览一次
  162. preview();
  163. </script>
  164. </body>
  165. </html>