seller 端商品字段批量维护工具
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Excel 编辑器</title>
  5. <style>
  6. body {
  7. margin: 0;
  8. padding: 0;
  9. font-family: Arial, sans-serif;
  10. font-size: 12px;
  11. background-color: #f0f0f0;
  12. color: #333;
  13. }
  14. .container {
  15. max-width: 100%;
  16. }
  17. .toolbar {
  18. position: sticky;
  19. top: 0;
  20. background: #fff;
  21. padding: 10px 0;
  22. margin-bottom: 15px;
  23. border-bottom: 1px solid #ddd;
  24. z-index: 100;
  25. display: flex;
  26. gap: 10px;
  27. align-items: center;
  28. }
  29. .file-controls {
  30. display: flex;
  31. gap: 10px;
  32. }
  33. .column-controls {
  34. margin-left: 20px;
  35. display: flex;
  36. gap: 10px;
  37. flex-wrap: wrap;
  38. align-items: center;
  39. }
  40. .column-checkbox {
  41. display: inline-flex;
  42. align-items: center;
  43. gap: 4px;
  44. padding: 4px 8px;
  45. background: #f5f5f5;
  46. border-radius: 4px;
  47. cursor: pointer;
  48. }
  49. .column-checkbox:hover {
  50. background: #e8e8e8;
  51. }
  52. button {
  53. padding: 6px 12px;
  54. background: #4CAF50;
  55. color: white;
  56. border: none;
  57. border-radius: 4px;
  58. cursor: pointer;
  59. }
  60. button:hover {
  61. background: #45a049;
  62. }
  63. input[type="file"] {
  64. padding: 6px;
  65. border: 1px solid #ddd;
  66. border-radius: 4px;
  67. }
  68. table {
  69. border-collapse: collapse;
  70. width: 100%;
  71. background: white;
  72. font-size: 12px;
  73. }
  74. th,
  75. td {
  76. border: 1px solid #ddd;
  77. padding: 0;
  78. vertical-align: top;
  79. }
  80. th {
  81. background: #f5f5f5;
  82. position: sticky;
  83. top: 60px;
  84. z-index: 90;
  85. }
  86. textarea {
  87. width: 100%;
  88. min-height: 100px;
  89. resize: vertical;
  90. padding: 0;
  91. border: 0;
  92. border-radius: 4px;
  93. font-size: 12px;
  94. box-sizing: border-box;
  95. }
  96. .index-column {
  97. width: 60px;
  98. text-align: center;
  99. background-color: #f5f5f5;
  100. position: sticky;
  101. left: 0;
  102. z-index: 80;
  103. }
  104. th.index-column {
  105. z-index: 91;
  106. }
  107. .image-upload-container {
  108. margin-top: 5px;
  109. }
  110. .preview-button {
  111. margin-top: 5px;
  112. background: #2196F3;
  113. }
  114. .preview-button:hover {
  115. background: #1976D2;
  116. }
  117. .preview-modal {
  118. display: none;
  119. position: fixed;
  120. top: 0;
  121. left: 0;
  122. width: 100%;
  123. height: 100%;
  124. background: rgba(0, 0, 0, 0.7);
  125. z-index: 1000;
  126. }
  127. .preview-content {
  128. position: relative;
  129. width: 80%;
  130. height: 80%;
  131. margin: 5% auto;
  132. background: white;
  133. padding: 20px;
  134. overflow: auto;
  135. border-radius: 8px;
  136. }
  137. .close-preview {
  138. position: absolute;
  139. right: 20px;
  140. top: 10px;
  141. font-size: 24px;
  142. cursor: pointer;
  143. }
  144. .h5-preview-content {
  145. max-width: 375px;
  146. margin: 0 auto;
  147. background: #fff;
  148. }
  149. .h5-preview-content img {
  150. max-width: 100%;
  151. height: auto;
  152. }
  153. .hidden-column {
  154. display: none;
  155. }
  156. .quick-actions {
  157. margin-left: 20px;
  158. display: flex;
  159. gap: 10px;
  160. }
  161. .help-button {
  162. background: #607D8B;
  163. color: white;
  164. border: none;
  165. border-radius: 50%;
  166. width: 24px;
  167. height: 24px;
  168. cursor: pointer;
  169. font-weight: bold;
  170. }
  171. .help-modal {
  172. display: none;
  173. position: fixed;
  174. top: 0;
  175. left: 0;
  176. width: 100%;
  177. height: 100%;
  178. background: rgba(0, 0, 0, 0.7);
  179. z-index: 1000;
  180. }
  181. .help-content {
  182. position: relative;
  183. width: 60%;
  184. max-height: 80%;
  185. margin: 5% auto;
  186. background: white;
  187. padding: 20px;
  188. border-radius: 8px;
  189. overflow-y: auto;
  190. }
  191. .shortcut-key {
  192. background: #f0f0f0;
  193. padding: 2px 6px;
  194. border-radius: 3px;
  195. font-family: monospace;
  196. }
  197. .status-bar {
  198. position: fixed;
  199. bottom: 0;
  200. left: 0;
  201. right: 0;
  202. background: #f5f5f5;
  203. padding: 8px 20px;
  204. border-top: 1px solid #ddd;
  205. display: flex;
  206. justify-content: space-between;
  207. z-index: 100;
  208. }
  209. .unsaved-changes {
  210. color: #f44336;
  211. font-weight: bold;
  212. }
  213. .save-reminder {
  214. position: fixed;
  215. bottom: 40px;
  216. right: 20px;
  217. background: #ff9800;
  218. color: white;
  219. padding: 10px 20px;
  220. border-radius: 4px;
  221. display: none;
  222. animation: bounce 1s infinite;
  223. }
  224. @keyframes bounce {
  225. 0%,
  226. 100% {
  227. transform: translateY(0);
  228. }
  229. 50% {
  230. transform: translateY(-5px);
  231. }
  232. }
  233. .search-box {
  234. padding: 6px 12px;
  235. border: 1px solid #ddd;
  236. border-radius: 4px;
  237. margin-right: 10px;
  238. }
  239. .highlight {
  240. background-color: #fff176;
  241. }
  242. .column-preset {
  243. padding: 4px 8px;
  244. background: #e3f2fd;
  245. border: 1px solid #90caf9;
  246. border-radius: 4px;
  247. cursor: pointer;
  248. margin-right: 10px;
  249. }
  250. </style>
  251. </head>
  252. <body>
  253. <div class="container">
  254. <div class="toolbar">
  255. <div class="file-controls">
  256. <input type="file" id="fileInput" accept=".xlsx">
  257. <button onclick="uploadFile()">上传</button>
  258. <button onclick="saveChanges()" id="saveButton">保存</button>
  259. <button class="help-button" onclick="showHelp()">?</button>
  260. </div>
  261. <div class="quick-actions">
  262. <input type="text" class="search-box" placeholder="搜索内容..." onkeyup="searchContent(this.value)">
  263. <button onclick="toggleCommonColumns()">常用列</button>
  264. <button onclick="toggleAllColumns()">显示/隐藏所有列</button>
  265. </div>
  266. </div>
  267. <div class="column-controls" id="columnControls">
  268. <!-- 列控制复选框 -->
  269. </div>
  270. <div id="tableContainer"></div>
  271. </div>
  272. <!-- 帮助模态框 -->
  273. <div id="helpModal" class="help-modal">
  274. <div class="help-content">
  275. <span class="close-preview" onclick="closeHelp()">&times;</span>
  276. <h2>使用帮助</h2>
  277. <h3>快捷键</h3>
  278. <ul>
  279. <li><span class="shortcut-key">Ctrl + S</span> - 保存更改</li>
  280. <li><span class="shortcut-key">Ctrl + F</span> - 搜索内容</li>
  281. <li><span class="shortcut-key">Tab</span> - 切换到下一个单元格</li>
  282. <li><span class="shortcut-key">Shift + Tab</span> - 切换到上一个单元格</li>
  283. </ul>
  284. <h3>常用功能</h3>
  285. <ul>
  286. <li>点击"常用列"可以快速显示常用编辑的列</li>
  287. <li>使用搜索框可以快速定位内容</li>
  288. <li>双击单元格可以快速编辑</li>
  289. <li>图片上传支持多选</li>
  290. </ul>
  291. <h3>注意事项</h3>
  292. <ul>
  293. <li>有未保存的更改时会有提醒</li>
  294. <li>建议定期保存更改</li>
  295. <li>图片上传完成后请等待提示再继续操作</li>
  296. </ul>
  297. </div>
  298. </div>
  299. <div class="status-bar">
  300. <span id="statusText">就绪</span>
  301. <span id="unsavedChanges" class="unsaved-changes" style="display: none;">有未保存的更改</span>
  302. </div>
  303. <div id="saveReminder" class="save-reminder">
  304. 请记得保存更改!
  305. </div>
  306. <div id="previewModal" class="preview-modal">
  307. <div class="preview-content">
  308. <span class="close-preview" onclick="closePreview()">&times;</span>
  309. <div id="previewContent"></div>
  310. </div>
  311. </div>
  312. <script src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.16.0.min.js"></script>
  313. <script>
  314. let headers = [];
  315. let data = [];
  316. let columnVisibility = {};
  317. let hasUnsavedChanges = false;
  318. const commonColumns = ['SKU商品描述(PC)', 'SKU商品描述(h5)', 'SKU商品名称'];
  319. function initColumnControls() {
  320. const container = document.getElementById('columnControls');
  321. container.innerHTML = headers.map(header => `
  322. <label class="column-checkbox">
  323. <input type="checkbox"
  324. checked
  325. onchange="toggleColumn('${header}')"
  326. data-column="${header}">
  327. ${header}
  328. </label>
  329. `).join('');
  330. // 初始化列可见性状态
  331. headers.forEach(header => {
  332. columnVisibility[header] = true;
  333. });
  334. }
  335. function toggleColumn(header) {
  336. columnVisibility[header] = !columnVisibility[header];
  337. const cells = document.querySelectorAll(`[data-column="${header}"]`);
  338. cells.forEach(cell => {
  339. if (cell.tagName === 'INPUT') {
  340. // 复选框状态
  341. cell.checked = columnVisibility[header];
  342. } else {
  343. // 表格单元格
  344. cell.classList.toggle('hidden-column');
  345. }
  346. });
  347. }
  348. function renderTable() {
  349. const container = document.getElementById('tableContainer');
  350. let html = '<table><tr>';
  351. html += '<th class="index-column">序号</th>';
  352. headers.forEach(header => {
  353. html += `<th data-column="${header}" ${!columnVisibility[header] ? 'class="hidden-column"' : ''}>${header}</th>`;
  354. });
  355. html += '</tr>';
  356. data.forEach((row, rowIndex) => {
  357. html += '<tr>';
  358. html += `<td class="index-column">${rowIndex + 1}</td>`;
  359. headers.forEach(header => {
  360. html += `<td data-column="${header}" ${!columnVisibility[header] ? 'class="hidden-column"' : ''}>
  361. <textarea
  362. data-row="${rowIndex}"
  363. data-header="${header}"
  364. onchange="updateData(${rowIndex}, '${header}', this.value)"
  365. >${row[header] || ''}</textarea>
  366. ${header === 'SKU商品描述(PC)' ? `
  367. <div class="image-upload-container">
  368. <input type="file" accept="image/*"
  369. onchange="uploadImage(this, ${rowIndex}, '${header}')" multiple>
  370. </div>
  371. <button class="preview-button" onclick="previewContent(${rowIndex}, '${header}', 'pc')">
  372. 预览
  373. </button>
  374. ` : ''}
  375. ${header === 'SKU商品描述(h5)' ? `
  376. <button class="preview-button" onclick="previewContent(${rowIndex}, '${header}', 'h5')">
  377. 预览
  378. </button>
  379. ` : ''}
  380. </td>`;
  381. });
  382. html += '</tr>';
  383. });
  384. html += '</table>';
  385. container.innerHTML = html;
  386. }
  387. async function uploadFile() {
  388. const fileInput = document.getElementById('fileInput');
  389. const file = fileInput.files[0];
  390. if (!file) return;
  391. const formData = new FormData();
  392. formData.append('excelFile', file);
  393. const response = await fetch('/upload', {
  394. method: 'POST',
  395. body: formData
  396. });
  397. const result = await response.json();
  398. headers = result.headers;
  399. data = result.data;
  400. // 初始化列控制
  401. initColumnControls();
  402. renderTable();
  403. }
  404. function updateData(rowIndex, header, value) {
  405. data[rowIndex][header] = value;
  406. hasUnsavedChanges = true;
  407. document.getElementById('unsavedChanges').style.display = 'block';
  408. document.getElementById('saveReminder').style.display = 'block';
  409. updateStatus('有未保存的更改');
  410. }
  411. async function saveChanges() {
  412. updateStatus('正在保存...');
  413. try {
  414. const response = await fetch('/save', {
  415. method: 'POST',
  416. headers: {
  417. 'Content-Type': 'application/json'
  418. },
  419. body: JSON.stringify({ headers, data })
  420. });
  421. const result = await response.json();
  422. if (!response.ok) {
  423. if (result.message) {
  424. alert(result.message);
  425. } else {
  426. alert('保存失败:' + (result.error || '未知错误'));
  427. }
  428. return;
  429. }
  430. window.location.href = result.downloadUrl;
  431. hasUnsavedChanges = false;
  432. document.getElementById('unsavedChanges').style.display = 'none';
  433. document.getElementById('saveReminder').style.display = 'none';
  434. updateStatus('保存成功');
  435. } catch (error) {
  436. updateStatus('保存失败:' + error.message);
  437. }
  438. }
  439. async function uploadImage(input, rowIndex, header) {
  440. const files = input.files;
  441. if (!files.length) return;
  442. try {
  443. updateStatus('正在上传图片...');
  444. const formData = new FormData();
  445. Array.from(files).forEach(file => {
  446. formData.append('images', file);
  447. });
  448. const response = await fetch('/upload-images', {
  449. method: 'POST',
  450. body: formData
  451. });
  452. if (!response.ok) {
  453. throw new Error('上传失败');
  454. }
  455. const results = await response.json();
  456. // 生成PC描述HTML
  457. const pcImageHtml = results.map(result =>
  458. `<p><img src="${result.url}" title="${result.name}"></p>`
  459. ).join('\n');
  460. // 生成H5描述JSON
  461. const h5ImageJson = results.map(result => ({
  462. type: "image",
  463. content: result.url,
  464. checked: false,
  465. edit_checked: false
  466. }));
  467. // 更新PC描述
  468. const pcTextarea = input.parentElement.previousElementSibling;
  469. const currentPcValue = pcTextarea.value || '';
  470. pcTextarea.value = currentPcValue + (currentPcValue ? '\n' : '') + pcImageHtml;
  471. updateData(rowIndex, header, pcTextarea.value);
  472. // 更新H5描述
  473. const h5Header = 'SKU商品描述(h5)';
  474. const currentH5Value = data[rowIndex][h5Header] || '[]';
  475. let h5Data;
  476. try {
  477. h5Data = JSON.parse(currentH5Value);
  478. if (!Array.isArray(h5Data)) h5Data = [];
  479. } catch {
  480. h5Data = [];
  481. }
  482. // 合并新的图片数据
  483. h5Data.push(...h5ImageJson);
  484. // 更新H5数据
  485. const h5Value = JSON.stringify(h5Data);
  486. updateData(rowIndex, h5Header, h5Value);
  487. // 更新表格显示
  488. const h5Textarea = document.querySelector(`textarea[data-row="${rowIndex}"][data-header="${h5Header}"]`);
  489. if (h5Textarea) {
  490. h5Textarea.value = h5Value;
  491. }
  492. // 清空文件输入框
  493. input.value = '';
  494. updateStatus('图片上传成功');
  495. } catch (error) {
  496. console.error('Upload error:', error);
  497. alert('上传图片失败:' + error.message);
  498. updateStatus('图片上传失败');
  499. }
  500. }
  501. function previewContent(rowIndex, header, type) {
  502. const content = data[rowIndex][header] || '';
  503. const modal = document.getElementById('previewModal');
  504. const previewContent = document.getElementById('previewContent');
  505. if (type === 'pc') {
  506. // PC预览直接显示HTML
  507. previewContent.innerHTML = content;
  508. } else if (type === 'h5') {
  509. // H5预览需要解析JSON并生成预览内容
  510. try {
  511. const h5Data = JSON.parse(content);
  512. const h5Html = `
  513. <div class="h5-preview-content">
  514. ${h5Data.map(item => {
  515. if (item.type === 'image') {
  516. return `<img src="${item.content}" alt="">`;
  517. }
  518. return '';
  519. }).join('')}
  520. </div>
  521. `;
  522. previewContent.innerHTML = h5Html;
  523. } catch (error) {
  524. previewContent.innerHTML = '预览失败:无效的JSON格式';
  525. }
  526. }
  527. modal.style.display = 'block';
  528. }
  529. function closePreview() {
  530. document.getElementById('previewModal').style.display = 'none';
  531. }
  532. // 点击模态框背景关闭预览
  533. document.getElementById('previewModal').addEventListener('click', function (e) {
  534. if (e.target === this) {
  535. closePreview();
  536. }
  537. });
  538. // 添加快捷键支持
  539. document.addEventListener('keydown', function (e) {
  540. if (e.ctrlKey && e.key === 's') {
  541. e.preventDefault();
  542. saveChanges();
  543. }
  544. if (e.ctrlKey && e.key === 'f') {
  545. e.preventDefault();
  546. document.querySelector('.search-box').focus();
  547. }
  548. });
  549. // 搜索功能
  550. function searchContent(searchText) {
  551. if (!searchText) {
  552. clearHighlights();
  553. return;
  554. }
  555. const textareas = document.querySelectorAll('textarea');
  556. textareas.forEach(textarea => {
  557. const content = textarea.value.toLowerCase();
  558. if (content.includes(searchText.toLowerCase())) {
  559. textarea.classList.add('highlight');
  560. textarea.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  561. } else {
  562. textarea.classList.remove('highlight');
  563. }
  564. });
  565. }
  566. function clearHighlights() {
  567. document.querySelectorAll('.highlight').forEach(el => {
  568. el.classList.remove('highlight');
  569. });
  570. }
  571. // 常用列切换
  572. function toggleCommonColumns() {
  573. headers.forEach(header => {
  574. const isCommon = commonColumns.includes(header);
  575. columnVisibility[header] = isCommon;
  576. const checkbox = document.querySelector(`input[data-column="${header}"]`);
  577. if (checkbox) {
  578. checkbox.checked = isCommon;
  579. }
  580. });
  581. renderTable();
  582. }
  583. // 全部列切换
  584. function toggleAllColumns() {
  585. const allVisible = headers.every(header => columnVisibility[header]);
  586. headers.forEach(header => {
  587. columnVisibility[header] = !allVisible;
  588. const checkbox = document.querySelector(`input[data-column="${header}"]`);
  589. if (checkbox) {
  590. checkbox.checked = !allVisible;
  591. }
  592. });
  593. renderTable();
  594. }
  595. function updateStatus(text) {
  596. document.getElementById('statusText').textContent = text;
  597. }
  598. // 帮助功能
  599. function showHelp() {
  600. document.getElementById('helpModal').style.display = 'block';
  601. }
  602. function closeHelp() {
  603. document.getElementById('helpModal').style.display = 'none';
  604. }
  605. // 离开页面提醒
  606. window.addEventListener('beforeunload', function (e) {
  607. if (hasUnsavedChanges) {
  608. e.preventDefault();
  609. e.returnValue = '有未保存的更改,确定要离开吗?';
  610. }
  611. });
  612. </script>
  613. </body>
  614. </html>