diff --git a/Reject.js b/Reject.js index 973feb4..c086f8c 100644 --- a/Reject.js +++ b/Reject.js @@ -3,6 +3,7 @@ const bodyParser = require('body-parser'); const cors = require('cors'); const mysql = require('mysql2/promise'); const path = require('path'); +const OssUploader = require('./oss-uploader'); const app = express(); const PORT = 3000; @@ -694,8 +695,6 @@ app.post('/api/suppliers/:id/terminate', async (req, res) => { } }); -// 导入OSS上传工具 -const OssUploader = require('./oss-uploader'); // 导入图片处理工具 const ImageProcessor = require('./image-processor'); @@ -760,37 +759,48 @@ app.post('/api/supplies/create', async (req, res) => { // 生成唯一的productId const productId = `product_${Date.now()}_${Math.floor(Math.random() * 1000)}`; - // 处理图片上传 + // 处理媒体文件上传(图片和视频) let uploadedImageUrls = []; if (Array.isArray(imageUrls) && imageUrls.length > 0) { - console.log('开始处理图片上传,共', imageUrls.length, '张图片'); + console.log('开始处理媒体文件上传,共', imageUrls.length, '个文件'); - for (const imageUrl of imageUrls) { - if (imageUrl.startsWith('data:image/')) { + for (const mediaUrl of imageUrls) { + if (mediaUrl.startsWith('data:image/') || mediaUrl.startsWith('data:video/')) { // 处理DataURL - const base64Data = imageUrl.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, ''); + let base64Data, ext, fileType; + if (mediaUrl.startsWith('data:image/')) { + // 图片类型 + base64Data = mediaUrl.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, ''); + ext = mediaUrl.match(/^data:image\/(png|jpeg|jpg|gif);base64,/)?.[1] || 'png'; + fileType = 'image'; + } else { + // 视频类型 + base64Data = mediaUrl.replace(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/, ''); + ext = mediaUrl.match(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/)?.[1] || 'mp4'; + fileType = 'video'; + } + let buffer = Buffer.from(base64Data, 'base64'); - const ext = imageUrl.match(/^data:image\/(png|jpeg|jpg|gif);base64,/)?.[1] || 'png'; const filename = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${ext}`; try { // 不再添加水印,前端已处理 console.log('【水印处理】前端已添加水印,跳过后端水印处理'); - // 使用OSS上传带水印的图片 - const ossUrl = await OssUploader.uploadBuffer(buffer, filename, `products/${productName || 'general'}`, 'image'); + // 使用OSS上传媒体文件 + const ossUrl = await OssUploader.uploadBuffer(buffer, filename, `products/${productName || 'general'}`, fileType); uploadedImageUrls.push(ossUrl); - console.log('图片上传成功:', ossUrl); + console.log(`${fileType}上传成功:`, ossUrl); } catch (uploadError) { - console.error('图片上传失败:', uploadError.message); - // 继续上传其他图片,不中断流程 + console.error(`${fileType}上传失败:`, uploadError.message); + // 继续上传其他文件,不中断流程 } } else { // 已经是URL,直接使用 - uploadedImageUrls.push(imageUrl); + uploadedImageUrls.push(mediaUrl); } } - console.log('图片处理完成,成功上传', uploadedImageUrls.length, '张图片'); + console.log('媒体文件处理完成,成功上传', uploadedImageUrls.length, '个文件'); } // 创建商品数据 @@ -805,8 +815,8 @@ app.post('/api/supplies/create', async (req, res) => { grossWeight, yolk, specification, - producting, quality, + producting: producting || '', // 添加产品包装 region, status: 'published', // 直接上架,而不是审核中 supplyStatus: supplyStatus || '', // 预售/现货 @@ -879,16 +889,86 @@ app.post('/api/supplies/create', async (req, res) => { } }); -// 图片上传API - /api/upload-image +// 媒体文件上传API - /api/upload-media +console.log('正在注册媒体文件上传API路由: /api/upload-media'); +app.post('/api/upload-media', async (req, res) => { + console.log('收到媒体文件上传请求:', req.body); + try { + const { fileData, fileName, folder = 'general' } = req.body; + + // 验证参数 + if (!fileData) { + return sendResponse(res, false, null, '文件数据不能为空'); + } + + let base64Data, ext, fileType; + if (fileData.startsWith('data:image/')) { + // 图片类型 + base64Data = fileData.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, ''); + ext = fileData.match(/^data:image\/(png|jpeg|jpg|gif);base64,/)?.[1] || 'png'; + fileType = 'image'; + } else if (fileData.startsWith('data:video/')) { + // 视频类型 + base64Data = fileData.replace(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/, ''); + ext = fileData.match(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/)?.[1] || 'mp4'; + fileType = 'video'; + } else { + return sendResponse(res, false, null, '不支持的文件类型'); + } + + let buffer = Buffer.from(base64Data, 'base64'); + const filename = fileName || `${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${ext}`; + + try { + // 使用OSS上传媒体文件 + const ossUrl = await OssUploader.uploadBuffer(buffer, filename, `uploads/${folder}`, fileType); + console.log(`${fileType}上传成功:`, ossUrl); + + sendResponse(res, true, { + url: ossUrl, + fileType: fileType, + message: `${fileType}上传成功` + }, `${fileType}上传成功`); + } catch (uploadError) { + console.error(`${fileType}上传失败:`, uploadError.message); + sendResponse(res, false, null, `${fileType}上传失败: ${uploadError.message}`); + } + } catch (error) { + console.error('媒体文件上传API错误:', error.message); + sendResponse(res, false, null, `媒体文件上传失败: ${error.message}`); + } +}); + +// 图片上传API - /api/upload-image(兼容旧接口) console.log('正在注册图片上传API路由: /api/upload-image'); app.post('/api/upload-image', async (req, res) => { - console.log('收到图片上传请求'); - // 注意:这里需要实现实际的图片上传逻辑,包括OSS配置 - // 由于当前缺少OSS配置,返回模拟数据 - sendResponse(res, true, { - imageUrl: 'https://example.com/image.jpg', - message: '图片上传成功' - }, '图片上传成功'); + console.log('收到图片上传请求,转发到媒体文件上传API'); + // 将请求转发到媒体文件上传API + const uploadMediaHandler = app._router.stack.find(layer => + layer.route && layer.route.path === '/api/upload-media' && layer.route.methods['post'] + ); + + if (uploadMediaHandler && uploadMediaHandler.route && uploadMediaHandler.route.stack[0]) { + return uploadMediaHandler.route.stack[0].handle(req, res); + } else { + sendResponse(res, false, null, '图片上传API内部错误'); + } +}); + +// 视频上传API - /api/upload-video(专门的视频上传接口) +console.log('正在注册视频上传API路由: /api/upload-video'); +app.post('/api/upload-video', async (req, res) => { + console.log('收到视频上传请求,转发到媒体文件上传API'); + // 将请求转发到媒体文件上传API + const uploadMediaHandler = app._router.stack.find(layer => + layer.route && layer.route.path === '/api/upload-media' && layer.route.methods['post'] + ); + + if (uploadMediaHandler && uploadMediaHandler.route && uploadMediaHandler.route.stack[0]) { + return uploadMediaHandler.route.stack[0].handle(req, res); + } else { + sendResponse(res, false, null, '视频上传API内部错误'); + } }); // 获取审核失败原因API - /api/supplies/:id/reject-reason @@ -1133,48 +1213,66 @@ app.put('/api/supplies/:id/edit', async (req, res) => { } } - // 处理图片上传 + // 处理媒体文件上传(图片和视频) let uploadedImageUrls = []; if (Array.isArray(imageUrls) && imageUrls.length > 0) { - console.log('开始处理编辑图片上传,共', imageUrls.length, '张图片'); + console.log('开始处理编辑媒体文件上传,共', imageUrls.length, '个文件'); - for (const imageUrl of imageUrls) { - if (imageUrl.startsWith('data:image/')) { + for (const mediaUrl of imageUrls) { + if (mediaUrl.startsWith('data:image/') || mediaUrl.startsWith('data:video/')) { // 处理DataURL - const base64Data = imageUrl.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, ''); + let base64Data, ext, fileType; + if (mediaUrl.startsWith('data:image/')) { + // 图片类型 + base64Data = mediaUrl.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, ''); + ext = mediaUrl.match(/^data:image\/(png|jpeg|jpg|gif);base64,/)?.[1] || 'png'; + fileType = 'image'; + } else { + // 视频类型 + base64Data = mediaUrl.replace(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/, ''); + ext = mediaUrl.match(/^data:video\/(mp4|mov|avi|wmv|flv);base64,/)?.[1] || 'mp4'; + fileType = 'video'; + } + let buffer = Buffer.from(base64Data, 'base64'); - const ext = imageUrl.match(/^data:image\/(png|jpeg|jpg|gif);base64,/)?.[1] || 'png'; const filename = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}.${ext}`; try { // 不再添加水印,前端已处理 console.log('【水印处理】前端已添加水印,跳过后端水印处理'); - // 使用OSS上传带水印的图片 - const ossUrl = await OssUploader.uploadBuffer(buffer, filename, `products/${productName || 'general'}`, 'image'); + // 使用OSS上传媒体文件 + const ossUrl = await OssUploader.uploadBuffer(buffer, filename, `products/${productName || 'general'}`, fileType); uploadedImageUrls.push(ossUrl); - console.log('图片上传成功:', ossUrl); + console.log(`${fileType}上传成功:`, ossUrl); } catch (uploadError) { - console.error('图片上传失败:', uploadError.message); - // 继续上传其他图片,不中断流程 + console.error(`${fileType}上传失败:`, uploadError.message); + // 继续上传其他文件,不中断流程 } } else { // 已经是URL,直接使用 - uploadedImageUrls.push(imageUrl); + uploadedImageUrls.push(mediaUrl); } } - console.log('图片处理完成,成功上传', uploadedImageUrls.length, '张图片'); + console.log('媒体文件处理完成,成功上传', uploadedImageUrls.length, '个文件'); } - // 处理数量:由于数据库中quantity是int类型,只保存第一个数量值 - let quantityValue = 0; - if (quantity) { - if (typeof quantity === 'string' && quantity.includes(',')) { - // 逗号分隔的多个数量,只取第一个 - quantityValue = parseInt(quantity.split(',')[0].trim()) || 0; - } else { - quantityValue = parseInt(quantity) || 0; - } + // 如果有新上传的图片,更新imageUrls字段 + if (uploadedImageUrls.length > 0) { + await connection.query( + 'UPDATE products SET imageUrls = ? WHERE id = ?', + [JSON.stringify(uploadedImageUrls), productId] + ); + } + + // 处理数量:保存所有数量值,与规格保持一致 + let quantityValue = quantity; // 直接保存前端提交的数量字符串 + // 如果是数字,转换为字符串 + if (typeof quantity === 'number') { + quantityValue = quantity.toString(); + } else if (typeof quantity === 'string' && !isNaN(parseFloat(quantity)) && isFinite(quantity)) { + // 如果是单个数字字符串,直接使用 + quantityValue = quantity; } // 更新货源信息 @@ -1505,6 +1603,22 @@ async function ensureDatabaseSchema() { console.log('audit_time字段添加成功'); } + // 检查表是否有producting字段 + console.log('检查表products是否有producting字段...'); + const [productingColumns] = await connection.query( + 'SHOW COLUMNS FROM `products` LIKE ?', + ['producting'] + ); + console.log('检查表字段结果:', productingColumns.length > 0 ? '已存在' : '不存在'); + + if (productingColumns.length === 0) { + console.log('添加producting字段到products表...'); + await connection.query( + 'ALTER TABLE `products` ADD COLUMN producting VARCHAR(255) COMMENT "产品包装"' + ); + console.log('producting字段添加成功'); + } + // 检查audit_logs表是否存在,如果不存在则创建 console.log('检查audit_logs表是否存在...'); const [tables] = await connection.query( diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 95a1426..f39158a 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,5 +1,9 @@ { - "name": "Review", +<<<<<<< Updated upstream + "name": "Review2", +======= + "name": "boss", +>>>>>>> Stashed changes "lockfileVersion": 3, "requires": true, "packages": { @@ -34,6 +38,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -830,6 +835,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", diff --git a/package-lock.json b/package-lock.json index 8871825..0ba49cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,9 @@ { - "name": "Review", +<<<<<<< Updated upstream + "name": "Review2", +======= + "name": "boss", +>>>>>>> Stashed changes "lockfileVersion": 3, "requires": true, "packages": { @@ -52,6 +56,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1295,6 +1300,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", diff --git a/supply-manager.js b/supply-manager.js index 0747554..a22c956 100644 --- a/supply-manager.js +++ b/supply-manager.js @@ -34,6 +34,13 @@ class SupplyManager { editSpecSearchKeyword: '', filteredSpecOptions: [], filteredEditSpecOptions: [], + productingOptions: ['1*360枚新包装', '1*360枚旧包新拖', '1*360枚旧包旧拖', '1*420枚新包装', '1*480枚新包装', '30枚蛋托散装', '360枚散托'], + productingSearchKeyword: '', + editProductingSearchKeyword: '', + filteredProductingOptions: [], + filteredEditProductingOptions: [], + showProductingSelectModal: false, + currentProductingMode: 'create', showRegionSelectModal: false, currentRegionMode: 'create', regionOptions: [ diff --git a/supply.html b/supply.html index e24df7d..bf33266 100644 --- a/supply.html +++ b/supply.html @@ -1197,7 +1197,7 @@
+
- +
图片要求:
1. 箱子堆头图(不得出现地址、电话及货源信息,直营包场除外);
@@ -1354,6 +1354,8 @@ + + @@ -1602,9 +1604,14 @@
+
- +
最多上传2张图片
+
最多上传2张图片
+ + @@ -2044,6 +2051,7 @@ // 初始化至少一个规格和件数对 addSpecQuantityPair(); + // 启动倒计时 startCountdowns(); @@ -2596,12 +2604,19 @@ // 删除规格和件数对 function removeSpecQuantityPair(btn) { const pair = btn.parentElement; - const specInput = pair.querySelector('.spec-value'); - const quantityInput = pair.querySelector('.quantity-input'); + const container = pair.parentElement; + const pairs = container.querySelectorAll('.spec-quantity-pair'); - // 清除当前对的输入值 - if (specInput) specInput.value = ''; - if (quantityInput) quantityInput.value = ''; + // 如果超过一个对,移除整个对元素 + if (pairs.length > 1) { + pair.remove(); + } else { + // 否则,只清除输入值 + const specInput = pair.querySelector('.spec-value'); + const quantityInput = pair.querySelector('.quantity-input'); + if (specInput) specInput.value = ''; + if (quantityInput) quantityInput.value = ''; + } } // 为规格和件数对显示规格选择弹窗 @@ -3995,8 +4010,50 @@ const status = statusMap[supply.status] || { text: '未知', class: 'status-draft' }; - // 处理图片 - const imageUrl = supply.imageUrls && supply.imageUrls.length > 0 ? supply.imageUrls[0] : ''; + // 处理媒体文件(图片和视频) + let firstMediaUrl = ''; + let mediaType = 'image'; // 默认为图片 + let mediaPreviewHTML = ''; + + if (supply.imageUrls && supply.imageUrls.length > 0) { + // 获取第一个媒体文件 + firstMediaUrl = supply.imageUrls[0]; + + // 检测媒体类型 + if (firstMediaUrl.startsWith('data:video/') || + firstMediaUrl.match(/\.(mp4|mov|avi|wmv|flv|webm|mkv)$/i) || + firstMediaUrl.includes('video') || + (supply.imageUrls && supply.imageUrls.some(url => url.startsWith('data:video/') || url.match(/\.(mp4|mov|avi|wmv|flv|webm|mkv)$/i)))) { + mediaType = 'video'; + mediaPreviewHTML = ` + +
+ `; + } else { + mediaType = 'image'; + mediaPreviewHTML = ` + ${supply.productName} + `; + } + } else { + // 无媒体文件时的占位符 + mediaPreviewHTML = ` +
+ 暂无媒体 +
+ `; + } // 处理操作按钮 let actionsHTML = ''; @@ -4068,8 +4125,8 @@ return `
-
- ${supply.productName} +
+ ${mediaPreviewHTML}
@@ -4252,6 +4309,7 @@ // 货源状态 document.getElementById('supplyStatus').value = supply.supplyStatus || ''; + // 联系人 - 使用contacts数组查找完整信息 document.getElementById('contactId').value = supply.contactId || ''; selectedContactId = supply.contactId || ''; @@ -4485,6 +4543,9 @@ // 重新加载联系人数据,确保最新 loadContacts(); + // 重新加载联系人数据,确保最新 + loadContacts(); + // 尝试从localStorage加载保存的表单数据 const savedFormData = localStorage.getItem('supplyFormDraft'); if (savedFormData) { @@ -4685,25 +4746,45 @@ updateEditImageDisplay(); } - // 更新编辑页面图片显示 + + // 更新编辑页面媒体文件显示 function updateEditImageDisplay() { const editUploadImages = document.getElementById('editUploadImages'); editUploadImages.innerHTML = ''; editCurrentImages.forEach(imageUrl => { - const imageElement = document.createElement('div'); - imageElement.className = 'upload-image'; - imageElement.innerHTML = ` - 商品图片 - - `; - editUploadImages.appendChild(imageElement); + const mediaElement = document.createElement('div'); + mediaElement.className = 'upload-image'; + + let mediaContent; + if (imageUrl.startsWith('data:video/')) { + // 视频文件 + mediaContent = ` + + + `; + } else { + // 图片文件 + mediaContent = ` + 商品图片 + + `; + } + + mediaElement.innerHTML = mediaContent; + editUploadImages.appendChild(mediaElement); }); } - // 为图片添加水印(前端Canvas实现) + // 为媒体文件添加水印(前端Canvas实现,仅处理图片,视频跳过) function addWatermarkToImage(imageUrl) { return new Promise((resolve, reject) => { + // 如果是视频文件,直接返回,不添加水印 + if (imageUrl.startsWith('data:video/')) { + resolve(imageUrl); + return; + } + const img = new Image(); img.crossOrigin = 'anonymous'; // 允许跨域图片 @@ -4781,19 +4862,32 @@ event.target.value = ''; } - // 渲染已上传图片 + // 渲染已上传媒体文件(图片或视频) function renderUploadedImages() { const uploadArea = document.getElementById('uploadImages'); uploadArea.innerHTML = ''; - supplyData.uploadedImages.forEach((imageUrl, index) => { - const imageElement = document.createElement('div'); - imageElement.className = 'upload-image'; - imageElement.innerHTML = ` - 商品图片 -
- `; - uploadArea.appendChild(imageElement); + supplyData.uploadedImages.forEach((mediaUrl, index) => { + const mediaElement = document.createElement('div'); + mediaElement.className = 'upload-image'; + + let mediaContent; + if (mediaUrl.startsWith('data:video/')) { + // 视频文件 + mediaContent = ` + +
+ `; + } else { + // 图片文件 + mediaContent = ` + 商品图片 +
+ `; + } + + mediaElement.innerHTML = mediaContent; + uploadArea.appendChild(mediaElement); }); } @@ -5103,6 +5197,7 @@ } }); + // 获取自动下架分钟数 const autoOfflineMinutes = document.getElementById('autoOfflineMinutes').value; // 将分钟转换为小时(用于存储到数据库) @@ -5115,6 +5210,7 @@ productName: document.getElementById('productName').value, category: document.getElementById('category').value, price: document.getElementById('price').value, + costprice: document.getElementById('costprice').value, // 添加采购价 quantity: quantities.join(','), // 将件数以英文逗号分隔的字符串形式提交 grossWeight: document.getElementById('grossWeight').value, @@ -5271,12 +5367,28 @@ setupImageViewerEvents(); } - // 更新预览图片 + // 更新预览媒体文件(图片或视频) function updatePreviewImage() { const previewImage = document.getElementById('previewImage'); + const previewVideo = document.getElementById('previewVideo'); const previewCounter = document.getElementById('previewCounter'); - previewImage.src = currentPreviewImages[currentPreviewIndex]; + const mediaUrl = currentPreviewImages[currentPreviewIndex]; + + // 根据媒体类型显示相应的元素 + if (mediaUrl.startsWith('data:video/') || + mediaUrl.match(/\.(mp4|mov|avi|wmv|flv|webm|mkv)$/i) || + mediaUrl.includes('video')) { + // 视频文件 + previewVideo.src = mediaUrl; + previewVideo.style.display = 'block'; + previewImage.style.display = 'none'; + } else { + // 图片文件 + previewImage.src = mediaUrl; + previewImage.style.display = 'block'; + previewVideo.style.display = 'none'; + } // 更新图片计数器 if (previewCounter) { @@ -5305,12 +5417,23 @@ // 关闭图片预览 function closeImagePreview() { const previewModal = document.getElementById('imagePreview'); + const previewVideo = document.getElementById('previewVideo'); + + // 停止视频播放并重置视频状态 + if (previewVideo) { + previewVideo.pause(); + previewVideo.src = ''; + previewVideo.load(); + } + previewModal.classList.remove('active'); } - // 重置图片变换 + // 重置媒体变换 function resetImageTransform() { const previewImage = document.getElementById('previewImage'); + const previewVideo = document.getElementById('previewVideo'); + scale = 1; lastScale = 1; pointX = 0; @@ -5319,10 +5442,17 @@ updateImageTransform(); } - // 更新图片变换 + // 更新媒体变换 function updateImageTransform() { const previewImage = document.getElementById('previewImage'); - previewImage.style.transform = `translate(${pointX}px, ${pointY}px) scale(${scale})`; + const previewVideo = document.getElementById('previewVideo'); + + // 对当前显示的媒体元素应用变换 + if (previewImage.style.display !== 'none') { + previewImage.style.transform = `translate(${pointX}px, ${pointY}px) scale(${scale})`; + } else if (previewVideo.style.display !== 'none') { + previewVideo.style.transform = `translate(${pointX}px, ${pointY}px) scale(${scale})`; + } } // 设置图片查看器事件 @@ -5665,11 +5795,17 @@ document.getElementById('editProductingValue').value = supply.producting || ''; editSelectedProducting = supply.producting || ''; + // 产品包装 + document.getElementById('editProductingDisplayText').textContent = supply.producting || '请选择产品包装'; + document.getElementById('editProductingValue').value = supply.producting || ''; + editSelectedProducting = supply.producting || ''; + // 货源状态(使用按钮组) const supplyStatus = supply.supplyStatus || '预售'; document.getElementById('editSupplyStatus').value = supplyStatus; setEditSupplyStatus(supplyStatus); + // 分配联系人 - 使用contacts数组查找完整信息 document.getElementById('editContactId').value = supply.contactId || ''; editSelectedContactId = supply.contactId || ''; @@ -5689,6 +5825,7 @@ document.getElementById('editRegionDisplayText').textContent = supply.region || '请选择地区'; document.getElementById('editRegionValue').value = supply.region || ''; document.getElementById('editPrice').value = supply.price || ''; + document.getElementById('editCostprice').value = supply.costprice || ''; document.getElementById('editGrossWeight').value = supply.grossWeight || ''; @@ -5696,6 +5833,7 @@ const autoOfflineMinutes = (parseFloat(supply.autoOfflineHours) || 24) * 60; document.getElementById('editAutoOfflineMinutes').value = Math.round(autoOfflineMinutes); + // 规格和件数 - 支持中文逗号分隔和英文逗号分隔字符串 let specifications = []; let quantities = []; @@ -5800,6 +5938,7 @@ currentEditSupplyId = null; // 重置发布模式状态 window.currentEditPublishMode = false; + // 恢复body滚动条 document.body.style.overflow = 'auto'; } @@ -5941,11 +6080,60 @@ } else { currentEditSpecInput.value = ''; } + + + } else { + // 否则,为旧的规格选择逻辑 + if (editSelectedSpec.length > 0) { + document.getElementById('editSpecDisplayText').textContent = editSelectedSpec.join(', '); + document.getElementById('editSpecValue').value = JSON.stringify(editSelectedSpec); + // 根据选中的规格数量动态调整件数输入框数量 + adjustEditQuantityInputs(); + } else { + document.getElementById('editSpecDisplayText').textContent = '请选择规格'; + document.getElementById('editSpecValue').value = '[]'; + } + } hideEditSpecSelectModal(); } + + // 添加编辑件数输入项 + function addEditQuantityItem() { + const container = document.getElementById('editQuantityContainer'); + const quantityItem = document.createElement('div'); + quantityItem.className = 'quantity-item'; + quantityItem.innerHTML = ` + + + `; + container.insertBefore(quantityItem, container.lastElementChild); + } + + // 删除编辑件数输入项 + function removeEditQuantityItem(btn) { + const container = document.getElementById('editQuantityContainer'); + const quantityItems = container.querySelectorAll('.quantity-item'); + + // 至少保留一个件数输入项 + if (quantityItems.length > 1) { + const quantityItem = btn.parentElement; + quantityItem.remove(); + } + } + // 根据选中的规格数量调整编辑件数输入框数量 + function adjustEditQuantityInputs() { + const container = document.getElementById('editQuantityContainer'); + const quantityItems = container.querySelectorAll('.quantity-item'); + const specCount = editSelectedSpec.length; + + // 如果规格数量大于件数输入框数量,添加缺少的输入框 + while (quantityItems.length < specCount) { + addEditQuantityItem(); + } + } // 添加编辑规格和件数对 @@ -5964,12 +6152,19 @@ // 删除编辑规格和件数对 function removeEditSpecQuantityPair(btn) { const pair = btn.parentElement; - const specInput = pair.querySelector('.spec-value'); - const quantityInput = pair.querySelector('.quantity-input'); + const container = pair.parentElement; + const pairs = container.querySelectorAll('.spec-quantity-pair'); - // 清除当前对的输入值 - if (specInput) specInput.value = ''; - if (quantityInput) quantityInput.value = ''; + // 如果超过一个对,移除整个对元素 + if (pairs.length > 1) { + pair.remove(); + } else { + // 否则,只清除输入值 + const specInput = pair.querySelector('.spec-value'); + const quantityInput = pair.querySelector('.quantity-input'); + if (specInput) specInput.value = ''; + if (quantityInput) quantityInput.value = ''; + } } // 为编辑规格和件数对显示规格选择弹窗 @@ -6627,6 +6822,7 @@ } }); + // 获取自动下架分钟数 const autoOfflineMinutes = document.getElementById('editAutoOfflineMinutes')?.value; // 将分钟转换为小时(用于存储到数据库) @@ -6644,6 +6840,7 @@ sourceType: document.getElementById('editSourceType').value, category: document.getElementById('editCategory').value, price: document.getElementById('editPrice').value, + costprice: document.getElementById('editCostprice').value, // 添加采购价 quantity: quantities.join(','), // 将件数以英文逗号分隔的字符串形式提交 grossWeight: document.getElementById('editGrossWeight').value, @@ -6654,6 +6851,7 @@ description: document.getElementById('editDescription').value, region: document.getElementById('editRegionValue').value, contactId: document.getElementById('editContactId').value, + imageUrls: editCurrentImages, // 添加编辑后的图片列表 autoOfflineTime: autoOfflineTime, // 自动下架时间 autoOfflineDays: null, // 不再使用天数,设置为null @@ -6732,6 +6930,7 @@ } // 隐藏编辑货源模态框 + // 处理点击模态框外部关闭 window.onclick = function(event) { // 确保只处理click事件,不处理拖拽选择文本导致的mouseup事件 diff --git a/tatus b/tatus new file mode 100644 index 0000000..2a6a1dc --- /dev/null +++ b/tatus @@ -0,0 +1,410 @@ + + SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS + + Commands marked with * may be preceded by a number, _N. + Notes in parentheses indicate the behavior if _N is given. + A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K. + + h H Display this help. + q :q Q :Q ZZ Exit. + --------------------------------------------------------------------------- + + MMOOVVIINNGG + + e ^E j ^N CR * Forward one line (or _N lines). + y ^Y k ^K ^P * Backward one line (or _N lines). + ESC-j * Forward one file line (or _N file lines). + ESC-k * Backward one file line (or _N file lines). + f ^F ^V SPACE * Forward one window (or _N lines). + b ^B ESC-v * Backward one window (or _N lines). + z * Forward one window (and set window to _N). + w * Backward one window (and set window to _N). + ESC-SPACE * Forward one window, but don't stop at end-of-file. + ESC-b * Backward one window, but don't stop at beginning-of-file. + d ^D * Forward one half-window (and set half-window to _N). + u ^U * Backward one half-window (and set half-window to _N). + ESC-) RightArrow * Right one half screen width (or _N positions). + ESC-( LeftArrow * Left one half screen width (or _N positions). + ESC-} ^RightArrow Right to last column displayed. + ESC-{ ^LeftArrow Left to first column. + F Forward forever; like "tail -f". + ESC-F Like F but stop when search pattern is found. + r ^R ^L Repaint screen. + R Repaint screen, discarding buffered input. + --------------------------------------------------- + Default "window" is the screen height. + Default "half-window" is half of the screen height. + --------------------------------------------------------------------------- + + SSEEAARRCCHHIINNGG + + /_p_a_t_t_e_r_n * Search forward for (_N-th) matching line. + ?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line. + n * Repeat previous search (for _N-th occurrence). + N * Repeat previous search in reverse direction. + ESC-n * Repeat previous search, spanning files. + ESC-N * Repeat previous search, reverse dir. & spanning files. + ^O^N ^On * Search forward for (_N-th) OSC8 hyperlink. + ^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink. + ^O^L ^Ol Jump to the currently selected OSC8 hyperlink. + ESC-u Undo (toggle) search highlighting. + ESC-U Clear search highlighting. + &_p_a_t_t_e_r_n * Display only matching lines. + --------------------------------------------------- + Search is case-sensitive unless changed with -i or -I. + A search pattern may begin with one or more of: + ^N or ! Search for NON-matching lines. + ^E or * Search multiple files (pass thru END OF FILE). + ^F or @ Start search at FIRST file (for /) or last file (for ?). + ^K Highlight matches, but don't move (KEEP position). + ^R Don't use REGULAR EXPRESSIONS. + ^S _n Search for match in _n-th parenthesized subpattern. + ^W WRAP search if no match found. + ^L Enter next character literally into pattern. + --------------------------------------------------------------------------- + + JJUUMMPPIINNGG + + g < ESC-< * Go to first line in file (or line _N). + G > ESC-> * Go to last line in file (or line _N). + p % * Go to beginning of file (or _N percent into file). + t * Go to the (_N-th) next tag. + T * Go to the (_N-th) previous tag. + { ( [ * Find close bracket } ) ]. + } ) ] * Find open bracket { ( [. + ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>. + ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>. + --------------------------------------------------- + Each "find close bracket" command goes forward to the close bracket + matching the (_N-th) open bracket in the top line. + Each "find open bracket" command goes backward to the open bracket + matching the (_N-th) close bracket in the bottom line. + + m_<_l_e_t_t_e_r_> Mark the current top line with . + M_<_l_e_t_t_e_r_> Mark the current bottom line with . + '_<_l_e_t_t_e_r_> Go to a previously marked position. + '' Go to the previous position. + ^X^X Same as '. + ESC-m_<_l_e_t_t_e_r_> Clear a mark. + --------------------------------------------------- + A mark is any upper-case or lower-case letter. + Certain marks are predefined: + ^ means beginning of the file + $ means end of the file + --------------------------------------------------------------------------- + + CCHHAANNGGIINNGG FFIILLEESS + + :e [_f_i_l_e] Examine a new file. + ^X^V Same as :e. + :n * Examine the (_N-th) next file from the command line. + :p * Examine the (_N-th) previous file from the command line. + :x * Examine the first (or _N-th) file from the command line. + ^O^O Open the currently selected OSC8 hyperlink. + :d Delete the current file from the command line list. + = ^G :f Print current file name. + --------------------------------------------------------------------------- + + MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS + + -_<_f_l_a_g_> Toggle a command line option [see OPTIONS below]. + --_<_n_a_m_e_> Toggle a command line option, by name. + __<_f_l_a_g_> Display the setting of a command line option. + ___<_n_a_m_e_> Display the setting of an option, by name. + +_c_m_d Execute the less cmd each time a new file is examined. + + !_c_o_m_m_a_n_d Execute the shell command with $SHELL. + #_c_o_m_m_a_n_d Execute the shell command, expanded like a prompt. + |XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command. + s _f_i_l_e Save input to a file. + v Edit the current file with $VISUAL or $EDITOR. + V Print version number of "less". + --------------------------------------------------------------------------- + + OOPPTTIIOONNSS + + Most options may be changed either on the command line, + or from within less by using the - or -- command. + Options may be given in one of two forms: either a single + character preceded by a -, or a name preceded by --. + + -? ........ --help + Display help (from command line). + -a ........ --search-skip-screen + Search skips current screen. + -A ........ --SEARCH-SKIP-SCREEN + Search starts just after target line. + -b [_N] .... --buffers=[_N] + Number of buffers. + -B ........ --auto-buffers + Don't automatically allocate buffers for pipes. + -c ........ --clear-screen + Repaint by clearing rather than scrolling. + -d ........ --dumb + Dumb terminal. + -D xx_c_o_l_o_r . --color=xx_c_o_l_o_r + Set screen colors. + -e -E .... --quit-at-eof --QUIT-AT-EOF + Quit at end of file. + -f ........ --force + Force open non-regular files. + -F ........ --quit-if-one-screen + Quit if entire file fits on first screen. + -g ........ --hilite-search + Highlight only last match for searches. + -G ........ --HILITE-SEARCH + Don't highlight any matches for searches. + -h [_N] .... --max-back-scroll=[_N] + Backward scroll limit. + -i ........ --ignore-case + Ignore case in searches that do not contain uppercase. + -I ........ --IGNORE-CASE + Ignore case in all searches. + -j [_N] .... --jump-target=[_N] + Screen position of target lines. + -J ........ --status-column + Display a status column at left edge of screen. + -k _f_i_l_e ... --lesskey-file=_f_i_l_e + Use a compiled lesskey file. + -K ........ --quit-on-intr + Exit less in response to ctrl-C. + -L ........ --no-lessopen + Ignore the LESSOPEN environment variable. + -m -M .... --long-prompt --LONG-PROMPT + Set prompt style. + -n ......... --line-numbers + Suppress line numbers in prompts and messages. + -N ......... --LINE-NUMBERS + Display line number at start of each line. + -o [_f_i_l_e] .. --log-file=[_f_i_l_e] + Copy to log file (standard input only). + -O [_f_i_l_e] .. --LOG-FILE=[_f_i_l_e] + Copy to log file (unconditionally overwrite). + -p _p_a_t_t_e_r_n . --pattern=[_p_a_t_t_e_r_n] + Start at pattern (from command line). + -P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t] + Define new prompt. + -q -Q .... --quiet --QUIET --silent --SILENT + Quiet the terminal bell. + -r -R .... --raw-control-chars --RAW-CONTROL-CHARS + Output "raw" control characters. + -s ........ --squeeze-blank-lines + Squeeze multiple blank lines. + -S ........ --chop-long-lines + Chop (truncate) long lines rather than wrapping. + -t _t_a_g .... --tag=[_t_a_g] + Find a tag. + -T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e] + Use an alternate tags file. + -u -U .... --underline-special --UNDERLINE-SPECIAL + Change handling of backspaces, tabs and carriage returns. + -V ........ --version + Display the version number of "less". + -w ........ --hilite-unread + Highlight first new line after forward-screen. + -W ........ --HILITE-UNREAD + Highlight first new line after any forward movement. + -x [_N[,...]] --tabs=[_N[,...]] + Set tab stops. + -X ........ --no-init + Don't use termcap init/deinit strings. + -y [_N] .... --max-forw-scroll=[_N] + Forward scroll limit. + -z [_N] .... --window=[_N] + Set size of window. + -" [_c[_c]] . --quotes=[_c[_c]] + Set shell quote characters. + -~ ........ --tilde + Don't display tildes after end of file. + -# [_N] .... --shift=[_N] + Set horizontal scroll amount (0 = one half screen width). + + --exit-follow-on-close + Exit F command on a pipe when writer closes pipe. + --file-size + Automatically determine the size of the input file. + --follow-name + The F command changes files if the input file is renamed. + --form-feed + Stop scrolling when a form feed character is reached. + --header=[_L[,_C[,_N]]] + Use _L lines (starting at line _N) and _C columns as headers. + --incsearch + Search file as each pattern character is typed in. + --intr=[_C] + Use _C instead of ^X to interrupt a read. + --lesskey-context=_t_e_x_t + Use lesskey source file contents. + --lesskey-src=_f_i_l_e + Use a lesskey source file. + --line-num-width=[_N] + Set the width of the -N line number field to _N characters. + --match-shift=[_N] + Show at least _N characters to the left of a search match. + --modelines=[_N] + Read _N lines from the input file and look for vim modelines. + --mouse + Enable mouse input. + --no-edit-warn + Don't warn when using v command on a file opened via LESSOPEN. + --no-keypad + Don't send termcap keypad init/deinit strings. + --no-histdups + Remove duplicates from command history. + --no-number-headers + Don't give line numbers to header lines. + --no-paste + Ignore pasted input. + --no-search-header-lines + Searches do not include header lines. + --no-search-header-columns + Searches do not include header columns. + --no-search-headers + Searches do not include header lines or columns. + --no-vbell + Disable the terminal's visual bell. + --redraw-on-quit + Redraw final screen when quitting. + --rscroll=[_C] + Set the character used to mark truncated lines. + --save-marks + Retain marks across invocations of less. + --search-options=[EFKNRW-] + Set default options for every search. + --show-preproc-errors + Display a message if preprocessor exits with an error status. + --proc-backspace + Process backspaces for bold/underline. + --PROC-BACKSPACE + Treat backspaces as control characters. + --proc-return + Delete carriage returns before newline. + --PROC-RETURN + Treat carriage returns as control characters. + --proc-tab + Expand tabs to spaces. + --PROC-TAB + Treat tabs as control characters. + --status-col-width=[_N] + Set the width of the -J status column to _N characters. + --status-line + Highlight or color the entire line containing a mark. + --use-backslash + Subsequent options use backslash as escape char. + --use-color + Enables colored text. + --wheel-lines=[_N] + Each click of the mouse wheel moves _N lines. + --wordwrap + Wrap lines at spaces. + + + --------------------------------------------------------------------------- + + LLIINNEE EEDDIITTIINNGG + + These keys can be used to edit text being entered + on the "command line" at the bottom of the screen. + + RightArrow ..................... ESC-l ... Move cursor right one character. + LeftArrow ...................... ESC-h ... Move cursor left one character. + ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word. + ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word. + HOME ........................... ESC-0 ... Move cursor to start of line. + END ............................ ESC-$ ... Move cursor to end of line. + BACKSPACE ................................ Delete char to left of cursor. + DELETE ......................... ESC-x ... Delete char under cursor. + ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor. + ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor. + ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line. + UpArrow ........................ ESC-k ... Retrieve previous command line. + DownArrow ...................... ESC-j ... Retrieve next command line. + TAB ...................................... Complete filename & cycle. + SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle. + ctrl-L ................................... Complete filename, list all. + + SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS + + Commands marked with * may be preceded by a number, _N. + Notes in parentheses indicate the behavior if _N is given. + A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K. + + h H Display this help. + q :q Q :Q ZZ Exit. + --------------------------------------------------------------------------- + + MMOOVVIINNGG + + e ^E j ^N CR * Forward one line (or _N lines). + y ^Y k ^K ^P * Backward one line (or _N lines). + ESC-j * Forward one file line (or _N file lines). + ESC-k * Backward one file line (or _N file lines). + f ^F ^V SPACE * Forward one window (or _N lines). + b ^B ESC-v * Backward one window (or _N lines). + z * Forward one window (and set window to _N). + w * Backward one window (and set window to _N). + ESC-SPACE * Forward one window, but don't stop at end-of-file. + ESC-b * Backward one window, but don't stop at beginning-of-file. + d ^D * Forward one half-window (and set half-window to _N). + u ^U * Backward one half-window (and set half-window to _N). + ESC-) RightArrow * Right one half screen width (or _N positions). + ESC-( LeftArrow * Left one half screen width (or _N positions). + ESC-} ^RightArrow Right to last column displayed. + ESC-{ ^LeftArrow Left to first column. + F Forward forever; like "tail -f". + ESC-F Like F but stop when search pattern is found. + r ^R ^L Repaint screen. + R Repaint screen, discarding buffered input. + --------------------------------------------------- + Default "window" is the screen height. + Default "half-window" is half of the screen height. + --------------------------------------------------------------------------- + + SSEEAARRCCHHIINNGG + + /_p_a_t_t_e_r_n * Search forward for (_N-th) matching line. + ?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line. + n * Repeat previous search (for _N-th occurrence). + N * Repeat previous search in reverse direction. + ESC-n * Repeat previous search, spanning files. + ESC-N * Repeat previous search, reverse dir. & spanning files. + ^O^N ^On * Search forward for (_N-th) OSC8 hyperlink. + ^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink. + ^O^L ^Ol Jump to the currently selected OSC8 hyperlink. + ESC-u Undo (toggle) search highlighting. + ESC-U Clear search highlighting. + &_p_a_t_t_e_r_n * Display only matching lines. + --------------------------------------------------- + Search is case-sensitive unless changed with -i or -I. + A search pattern may begin with one or more of: + ^N or ! Search for NON-matching lines. + ^E or * Search multiple files (pass thru END OF FILE). + ^F or @ Start search at FIRST file (for /) or last file (for ?). + ^K Highlight matches, but don't move (KEEP position). + ^R Don't use REGULAR EXPRESSIONS. + ^S _n Search for match in _n-th parenthesized subpattern. + ^W WRAP search if no match found. + ^L Enter next character literally into pattern. + --------------------------------------------------------------------------- + + JJUUMMPPIINNGG + + g < ESC-< * Go to first line in file (or line _N). + G > ESC-> * Go to last line in file (or line _N). + p % * Go to beginning of file (or _N percent into file). + t * Go to the (_N-th) next tag. + T * Go to the (_N-th) previous tag. + { ( [ * Find close bracket } ) ]. + } ) ] * Find open bracket { ( [. + ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>. + ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>. + --------------------------------------------------- + Each "find close bracket" command goes forward to the close bracket + matching the (_N-th) open bracket in the top line. + Each "find open bracket" command goes backward to the open bracket + matching the (_N-th) close bracket in the bottom line. + + m_<_l_e_t_t_e_r_> Mark the current top line with . + M_<_l_e_t_t_e_r_> Mark the current bottom line with . + '_<_l_e_t_t_e_r_> Go to a previously marked position. + '' Go to the previous position.