Browse Source

Fix merge conflicts and issues with spec-quantity pairs

Boss2
Trae AI 2 months ago
parent
commit
0c6c413079
  1. 206
      Reject.js
  2. 8
      node_modules/.package-lock.json
  3. 8
      package-lock.json
  4. 7
      supply-manager.js
  5. 277
      supply.html
  6. 410
      tatus

206
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(

8
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",

8
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",

7
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: [

277
supply.html

@ -1197,7 +1197,7 @@
<div class="upload-area">
<div id="uploadImages" class="upload-images"></div>
<div class="add-image" onclick="triggerImageUpload()">+</div>
<input type="file" id="imageUpload" multiple accept="image/*" style="display: none;" onchange="handleImageUpload(event)">
<input type="file" id="imageUpload" multiple accept="image/*,video/mp4,video/mov,video/avi,video/wmv,video/flv" style="display: none;" onchange="handleImageUpload(event)">
<div style="font-size: 12px; color: #666; margin-top: 10px; text-align: left; padding: 8px; background-color: #f5f5f5; border-radius: 4px;">
<div>图片要求:</div>
<div>1. 箱子堆头图(不得出现地址、电话及货源信息,直营包场除外);</div>
@ -1354,6 +1354,8 @@
<button class="image-viewer-nav prev-btn" onclick="prevPreviewImage()"></button>
<!-- 图片显示 -->
<img id="previewImage" class="preview-image" src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22%3E%3Crect width=%22100%22 height=%22100%22 fill=%22%23f5f5f5%22/%3E%3Ctext x=%2250%22 y=%2250%22 font-family=%22Arial%22 font-size=%2212%22 fill=%22%23999%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22%3E暂无图片%3C/text%3E%3C/svg%3E" style="transform: scale(1); transition: transform 0.1s ease;">
<!-- 视频显示 -->
<video id="previewVideo" class="preview-image" src="" controls style="transform: scale(1); transition: transform 0.1s ease;"></video>
<!-- 下一张按钮 -->
<button class="image-viewer-nav next-btn" onclick="nextPreviewImage()"></button>
<!-- 关闭按钮 -->
@ -1602,9 +1604,14 @@
<!-- 图片将通过JavaScript动态加载 -->
</div>
<div class="add-image" onclick="triggerEditImageUpload()">+</div>
<input type="file" id="editImageUpload" multiple accept="image/*" style="display: none;" onchange="handleEditImageUpload(event)">
<div style="font-size: 12px; color: #999; margin-top: 10px; text-align: left;">最多上传2张图片</div>
</div>
<div style="font-size: 12px; color: #999; margin-top: 10px;">最多上传2张图片</div>
<input type="file" id="editImageUpload" multiple accept="image/*,video/mp4,video/mov,video/avi,video/wmv,video/flv" style="display: none;" onchange="handleEditImageUpload(event)">
<button type="button" onclick="triggerEditImageUpload()" style="margin-top: 10px; padding: 8px 16px; background-color: #f0f0f0; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 14px; cursor: pointer;">
添加图片
</button>
</div>
<!-- 货源类型 -->
@ -2045,6 +2052,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 = `
<video
src="${firstMediaUrl}"
style="width: 100px; height: 100px; object-fit: cover; border-radius: 8px;"
onclick="previewImage('${firstMediaUrl}', ${JSON.stringify(supply.imageUrls || []).replace(/"/g, '&quot;')})"
muted
preload="metadata"
></video>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 24px; pointer-events: none;"></div>
`;
} else {
mediaType = 'image';
mediaPreviewHTML = `
<img
src="${firstMediaUrl}"
alt="${supply.productName}"
style="width: 100px; height: 100px; object-fit: cover; border-radius: 8px;"
onclick="previewImage('${firstMediaUrl}', ${JSON.stringify(supply.imageUrls || []).replace(/"/g, '&quot;')})"
>
`;
}
} else {
// 无媒体文件时的占位符
mediaPreviewHTML = `
<div style="width: 100px; height: 100px; background-color: #f5f5f5; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #999; font-size: 12px;">
暂无媒体
</div>
`;
}
// 处理操作按钮
let actionsHTML = '';
@ -4068,8 +4125,8 @@
return `
<div class="supply-item" data-id="${supply.id}">
<div class="supply-images">
<div class="image-slider">
<img src="${imageUrl || 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22%3E%3Crect width=%22100%22 height=%22100%22 fill=%22%23f5f5f5%22/%3E%3Ctext x=%2250%22 y=%2250%22 font-family=%22Arial%22 font-size=%2212%22 fill=%22%23999%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22%3E暂无图片%3C/text%3E%3C/svg%3E'}" alt="${supply.productName}" onclick="previewImage('${imageUrl || ''}', ${supply.imageUrls ? `[${supply.imageUrls.map(url => `'${url}'`).join(', ')}]` : '[]'})">
<div class="image-slider" style="position: relative;">
${mediaPreviewHTML}
</div>
</div>
<div class="supply-info">
@ -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 = `
<img src="${imageUrl}" alt="商品图片" onclick="previewImage('${imageUrl}')">
<button class="delete-image-btn" onclick="deleteEditImage('${imageUrl}')">×</button>
`;
editUploadImages.appendChild(imageElement);
const mediaElement = document.createElement('div');
mediaElement.className = 'upload-image';
let mediaContent;
if (imageUrl.startsWith('data:video/')) {
// 视频文件
mediaContent = `
<video src="${imageUrl}" alt="商品视频" onclick="previewImage('${imageUrl}')" controls style="width: 100%; height: 100%; object-fit: cover;"></video>
<button class="delete-image-btn" onclick="deleteEditImage('${imageUrl}')">×</button>
`;
} else {
// 图片文件
mediaContent = `
<img src="${imageUrl}" alt="商品图片" onclick="previewImage('${imageUrl}')">
<button class="delete-image-btn" onclick="deleteEditImage('${imageUrl}')">×</button>
`;
}
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 = `
<img src="${imageUrl}" alt="商品图片" onclick="previewImage('${imageUrl}')">
<div class="delete-image" onclick="deleteUploadedImage(${index})"></div>
`;
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 = `
<video src="${mediaUrl}" alt="商品视频" onclick="previewImage('${mediaUrl}')" controls style="width: 100%; height: 100%; object-fit: cover;"></video>
<div class="delete-image" onclick="deleteUploadedImage(${index})"></div>
`;
} else {
// 图片文件
mediaContent = `
<img src="${mediaUrl}" alt="商品图片" onclick="previewImage('${mediaUrl}')">
<div class="delete-image" onclick="deleteUploadedImage(${index})"></div>
`;
}
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 = `
<input type="number" class="form-input quantity-input" placeholder="请输入件数" onwheel="this.blur()">
<button type="button" class="remove-quantity-btn" onclick="removeEditQuantityItem(this)">×</button>
`;
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事件

410
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 <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_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 <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
Loading…
Cancel
Save