视频右边是手机端实际效果演示,含条形码识别演示
手机端产品库位查询工具
用聚水潭申请的接口写的手机端网页。 用途是给仓库的人员方便在手机查询产品的仓位、数量、核对实物。
1、支持扫描货品的商品编码进行查询,免去人工输入的繁琐。
2、支持图片放大预览,双指缩放图片大小,单指拖动图片
前端用AI写的,条形码识别要用到quagga包。后端要去聚水潭申请拿到access_token,具体看聚水潭开放平台文档,我用的是商家自研的授权。
项目用inscode开发的,用Express框架写好代码,直接部署就能在公网访问了。

服务端响应格式
html
{
"code": 0,
"msg": "",
"count": 1,
"data": [
{
"pic": "图片url",
"i_id": "BGD25070201",
"labels": "客户定制",
"sku_id": "BGD25070201-客订巴塔那护发精油60ml-LL",
"name": "客订巴塔那护发精油60ml",
"properties_value": "60ml",
"sale_price": 1.63,
"supplier_name": "A-东莞厂",
"l": 3.8,
"w": 3.8,
"h": 11.9,
"volume": 171.84,
"unit": "瓶",
"modified": "2025-08-05 09:28:23.963",
"creator_name": "王进",
"created": "2025-07-02 17:26:48.830",
"is_series_number": false,
"c_id": 389468118,
"supplier_id": 16892712,
"co_id": 12020245,
"owner_co_id": 0,
"sku_type": "normal",
"_primary_key": "BGD25070201-客订巴塔那护发精油60ml-LL",
"qty": 0,
"order_lock": 0,
"purchase_qty": 0,
"bin": "D-1-25",
"supplier_count": 1,
"sale_price_formula_id": 0
}
]
}
html代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>查询页面</title>
<script src="quagga/dist/quagga.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body {
background-color: #f5f5f5;
color: #333;
padding: 8px;
overflow-x: hidden;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.search-form {
background-color: #fff;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
position: relative;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-size: 14px;
color: #666;
}
.form-control {
width: 100%;
padding: 12px 40px 12px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.scan-icon {
position: absolute;
right: 10px;
top: 67%;
transform: translateY(-50%);
background-color: transparent;
color: white;
border: none;
/* border-radius: 50%; */
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 30px;
}
.btn {
width: 100%;
padding: 12px;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.btn:hover {
background-color: #3367d6;
}
.query-type-group {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.query-type-option {
display: flex;
align-items: center;
}
.query-type-option input {
margin-right: 5px;
}
.result-container {
margin-top: 20px;
background-color: #fff;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: none;
}
.loading {
text-align: center;
padding: 20px;
display: none;
}
.error {
color: #d93025;
padding: 10px;
background-color: #fce8e6;
border-radius: 4px;
margin-top: 10px;
display: none;
}
.result-list {
list-style: none;
}
.result-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid #eee;
gap: 15px;
}
.result-item:last-child {
border-bottom: none;
}
.result-image {
width: 80px;
height: 80px;
flex-shrink: 0;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
}
.result-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.result-content {
flex: 1;
}
.result-content h3 {
font-size: 16px;
margin-bottom: 5px;
color: #4285f4;
}
.result-content p {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
flex-wrap: wrap;
}
.pagination button {
margin: 2px;
padding: 5px 10px;
background-color: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.pagination button.active {
background-color: #4285f4;
color: white;
border-color: #4285f4;
}
.pagination button:hover:not(.active) {
background-color: #e0e0e0;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-info {
text-align: center;
margin-top: 10px;
font-size: 14px;
color: #666;
}
.scanner-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.scanner-container {
width: 100%;
max-width: 100%;
height: 70vh;
position: relative;
background-color: #000;
overflow: hidden;
}
.scanner-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.scanner-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
border: 2px solid rgba(0, 255, 0, 0.5);
box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.7);
pointer-events: none;
}
.scanner-guide {
position: absolute;
top: 10%;
left: 10%;
width: 80%;
height: 2px;
background: rgba(0, 255, 0, 0.5);
animation: scan 2s infinite linear;
}
@keyframes scan {
0% { top: 10%; }
100% { top: 90%; }
}
.scanner-cancel {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 18px;
z-index: 1001;
}
.scanner-result {
position: absolute;
bottom: 20px;
left: 0;
width: 100%;
text-align: center;
color: white;
background-color: rgba(0, 0, 0, 0.5);
padding: 15px;
font-size: 16px;
display: none;
z-index: 1001;
}
.image-preview-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: none;
align-items: center;
justify-content: center;
z-index: 2000;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-preview-modal.active {
opacity: 1;
}
.image-preview-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
touch-action: none;
}
.image-preview-img {
max-width: 100%;
max-height: 100%;
transform: scale(1);
transition: transform 0.3s ease;
will-change: transform;
}
.image-preview-close {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 18px;
z-index: 2001;
}
</style>
</head>
<body>
<div class="container">
<!-- <div class="header">
<h1>信息查询</h1>
</div> -->
<div class="search-form">
<div class="query-type-group">
<div class="query-type-option">
<input type="radio" id="style-code" name="query-type" value="iid" checked>
<label for="style-code">款式编码</label>
</div>
<div class="query-type-option">
<input type="radio" id="product-code" name="query-type" value="skuId">
<label for="product-code">商品编码</label>
</div>
<div class="query-type-option">
<input type="radio" id="product-name" name="query-type" value="name">
<label for="product-name">商品名称</label>
</div>
</div>
<div class="form-group">
<label for="query">请输入查询内容</label>
<input type="text" id="query" class="form-control" placeholder="例如:商品编码、或商品名称等">
<button class="scan-icon" id="scan-btn"><svg class="icon" style="width: 1.0498046875em;height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1075 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14811"><path d="M668.45257166 923.42857133v-55.247238h221.037714v-221.062095H944.76190466V923.42857133zM115.80952366 923.42857133V647.11923833h55.271619v221.062095h221.062095V923.42857133z m731.428572-97.523809H213.33333366V582.09523833h633.904762v243.809524zM115.80952366 536.60038133v-55.271619h828.952381v55.271619zM847.23809566 435.80952333H213.33333366V192.00000033h633.904762v243.809523z m42.300952-65.024V149.72342833h-221.062095V94.47619033H944.76190466v276.309333z m-773.705143 0V94.47619033h276.333714v55.247238H171.10552366v221.062095z" fill="#333333" p-id="14812"></path></svg></button>
</div>
<button id="search-btn" class="btn">查询</button>
</div>
<div class="loading" id="loading">
<p>正在查询中,请稍候...</p>
</div>
<div class="error" id="error">
查询失败,请检查输入或稍后重试
</div>
<div class="result-container" id="result">
<!-- <h2>查询结果</h2> -->
<ul class="result-list" id="result-list"></ul>
<div class="pagination" id="pagination"></div>
<div class="page-info" id="page-info"></div>
</div>
</div>
<div class="scanner-modal" id="scanner-modal">
<div class="scanner-container">
<video class="scanner-video" id="scanner-video" playsinline></video>
<div class="scanner-overlay"></div>
<div class="scanner-guide"></div>
<button class="scanner-cancel" id="scanner-cancel">✕</button>
<div class="scanner-result" id="scanner-result"></div>
</div>
</div>
<div class="image-preview-modal" id="image-preview-modal">
<button class="image-preview-close" id="image-preview-close">✕</button>
<div class="image-preview-container">
<img class="image-preview-img" id="image-preview-img" src="" alt="预览图片">
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchBtn = document.getElementById('search-btn');
const queryInput = document.getElementById('query');
const resultContainer = document.getElementById('result');
const resultList = document.getElementById('result-list');
const loadingElement = document.getElementById('loading');
const errorElement = document.getElementById('error');
const scanBtn = document.getElementById('scan-btn');
const scannerModal = document.getElementById('scanner-modal');
const scannerVideo = document.getElementById('scanner-video');
const scannerCancel = document.getElementById('scanner-cancel');
const scannerResult = document.getElementById('scanner-result');
const paginationContainer = document.getElementById('pagination');
const pageInfoElement = document.getElementById('page-info');
const imagePreviewModal = document.getElementById('image-preview-modal');
const imagePreviewImg = document.getElementById('image-preview-img');
const imagePreviewClose = document.getElementById('image-preview-close');
const queryTypeRadios = document.querySelectorAll('input[name="query-type"]');
// 分页相关变量
let currentPage = 1;
let totalPages = 1;
const itemsPerPage = 90; // 每页90条记录
// 图片预览相关变量
let currentScale = 1;
let currentTranslateX = 0;
let currentTranslateY = 0;
let isDragging = false;
let startX, startY;
let initialTranslateX, initialTranslateY;
let initialDistance = 0;
// 条形码扫描相关变量
let scannerRunning = false;
let mediaStream = null;
// 点击扫描图标打开扫描模态框
scanBtn.addEventListener('click', function() {
startScanner();
});
// 关闭扫描模态框
scannerCancel.addEventListener('click', function() {
stopScanner();
scannerModal.style.display = 'none';
});
// 点击查询按钮执行查询
searchBtn.addEventListener('click', function() {
performSearch(true); // 传递true表示是新查询
});
// 执行查询的函数
function performSearch(isNewQuery = true) {
const query = queryInput.value.trim();
let selectedQueryType = 'iid';
queryTypeRadios.forEach(radio => {
if (radio.checked) {
selectedQueryType = radio.value;
}
});
// 关键修复:如果是新查询,重置当前页码为1
if (isNewQuery) {
currentPage = 1;
}
loadingElement.style.display = 'block';
resultContainer.style.display = 'none';
errorElement.style.display = 'none';
resultList.innerHTML = '';
paginationContainer.innerHTML = '';
pageInfoElement.textContent = '';
// 构建API请求URL
let apiUrl;
if (query) {
apiUrl = `${window.origin}/api/GetPageListV2?page=${currentPage}&limit=${itemsPerPage}&${selectedQueryType}=${encodeURIComponent(query)}`;
} else {
apiUrl = `${window.origin}/api/GetPageListV2?page=${currentPage}&limit=${itemsPerPage}`;
}
// 模拟API请求
fetch(apiUrl)
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常');
}
return response.json();
})
.then(data => {
loadingElement.style.display = 'none';
if (data.code === 0 && data.data && data.data.length > 0) {
// 计算总页数
totalPages = Math.ceil(data.count / itemsPerPage);
// 显示分页信息
updatePageInfo(data.count);
// 渲染分页控件
renderPagination();
// 显示当前页的数据
displayPageResults(data.data);
resultContainer.style.display = 'block';
} else {
errorElement.textContent = data.msg || '没有找到匹配的记录';
errorElement.style.display = 'block';
}
})
.catch(error => {
loadingElement.style.display = 'none';
errorElement.textContent = '查询失败: ' + error.message;
errorElement.style.display = 'block';
console.error('查询错误:', error);
});
}
// 显示当前页的数据
function displayPageResults(results) {
resultList.innerHTML = '';
if (results.length > 0) {
results.forEach(function(item) {
const li = document.createElement('li');
li.className = 'result-item';
li.innerHTML = `
<div class="result-image">
<img src="${item.pic || item.pic_big}" alt="${item.name || ''}">
</div>
<div class="result-content">
<h3>${item.sku_id}</h3>
<p><strong>库位:</strong> ${item.bin || ''}</p>
<p><strong>库存:</strong> ${item.qty || 0} --- <strong>订单占有:</strong> ${item.order_lock || 0}</p>
<p><strong>销售属性:</strong> ${item.properties_value || '未填写'}</p>
<p><strong>备注:</strong> ${item.remark || ''}</p>
</div>
`;
resultList.appendChild(li);
});
}
}
// 更新分页信息
function updatePageInfo(totalItems) {
const startItem = (currentPage - 1) * itemsPerPage + 1;
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
pageInfoElement.textContent = `显示 ${startItem}-${endItem} 条,共 ${totalItems} 条`;
}
// 渲染分页控件
function renderPagination() {
paginationContainer.innerHTML = '';
// 上一页按钮
const prevButton = document.createElement('button');
prevButton.textContent = '上一页';
prevButton.disabled = currentPage === 1;
prevButton.addEventListener('click', function() {
if (currentPage > 1) {
currentPage--;
performSearch(false); // 传递false表示是翻页操作
}
});
paginationContainer.appendChild(prevButton);
// 页码按钮
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
if (startPage > 1) {
const firstPageButton = document.createElement('button');
firstPageButton.textContent = '1';
firstPageButton.addEventListener('click', function() {
currentPage = 1;
performSearch(false);
});
paginationContainer.appendChild(firstPageButton);
if (startPage > 2) {
const ellipsis = document.createElement('span');
ellipsis.textContent = '...';
paginationContainer.appendChild(ellipsis);
}
}
for (let i = startPage; i <= endPage; i++) {
const pageButton = document.createElement('button');
pageButton.textContent = i;
if (i === currentPage) {
pageButton.classList.add('active');
}
pageButton.addEventListener('click', (function(page) {
return function() {
currentPage = page;
performSearch(false);
};
})(i));
paginationContainer.appendChild(pageButton);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const ellipsis = document.createElement('span');
ellipsis.textContent = '...';
paginationContainer.appendChild(ellipsis);
}
const lastPageButton = document.createElement('button');
lastPageButton.textContent = totalPages;
lastPageButton.addEventListener('click', function() {
currentPage = totalPages;
performSearch(false);
});
paginationContainer.appendChild(lastPageButton);
}
// 下一页按钮
const nextButton = document.createElement('button');
nextButton.textContent = '下一页';
nextButton.disabled = currentPage === totalPages;
nextButton.addEventListener('click', function() {
if (currentPage < totalPages) {
currentPage++;
performSearch(false);
}
});
paginationContainer.appendChild(nextButton);
}
// 打开图片预览
function openImagePreview(src) {
imagePreviewImg.src = src;
imagePreviewModal.style.display = 'flex';
setTimeout(() => {
imagePreviewModal.classList.add('active');
}, 10);
resetImageTransform();
}
// 关闭图片预览
function closeImagePreview() {
imagePreviewModal.classList.remove('active');
setTimeout(() => {
imagePreviewModal.style.display = 'none';
}, 300);
}
// 重置图片变换
function resetImageTransform() {
currentScale = 1;
currentTranslateX = 0;
currentTranslateY = 0;
updateImageTransform();
}
// 更新图片变换
function updateImageTransform() {
imagePreviewImg.style.transform = `scale(${currentScale}) translate(${currentTranslateX}px, ${currentTranslateY}px)`;
}
// 触摸事件处理
imagePreviewImg.addEventListener('touchstart', function(e) {
if (e.touches.length === 1) {
// 单指触摸 - 准备拖动
isDragging = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
initialTranslateX = currentTranslateX;
initialTranslateY = currentTranslateY;
} else if (e.touches.length === 2) {
// 双指触摸 - 准备缩放
isDragging = false;
// 计算两指之间的距离
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
// 存储初始距离用于计算缩放比例
e.target.dataset.initialDistance = distance;
}
}, { passive: false });
imagePreviewImg.addEventListener('touchmove', function(e) {
if (isDragging && e.touches.length === 1) {
// 单指拖动
const deltaX = e.touches[0].clientX - startX;
const deltaY = e.touches[0].clientY - startY;
currentTranslateX = initialTranslateX + deltaX;
currentTranslateY = initialTranslateY + deltaY;
updateImageTransform();
} else if (e.touches.length === 2) {
// 双指缩放
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
// 获取初始距离
if (e.target.dataset.initialDistance) {
const initialDistance = parseFloat(e.target.dataset.initialDistance);
// 计算缩放比例
if (initialDistance > 0) {
const scale = currentScale * (currentDistance / initialDistance);
// 限制缩放范围
currentScale = Math.max(0.5, Math.min(3, scale));
updateImageTransform();
}
}
}
}, { passive: false });
imagePreviewImg.addEventListener('touchend', function() {
isDragging = false;
}, { passive: false });
// 关闭按钮点击事件
imagePreviewClose.addEventListener('click', closeImagePreview);
// 点击模态框背景关闭预览
imagePreviewModal.addEventListener('click', function(e) {
if (e.target === imagePreviewModal) {
closeImagePreview();
}
});
// 使用事件委托处理图片点击事件
document.addEventListener('click', function(e) {
// 检查点击的是否是结果图片
if (e.target.closest('.result-image img')) {
const img = e.target.closest('.result-image img');
openImagePreview(img.src);
}
});
// 开始扫描
function startScanner() {
if (scannerRunning) return;
scannerRunning = true;
scannerResult.style.display = 'none';
scannerModal.style.display = 'flex';
// 检查浏览器是否支持getUserMedia
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
showScannerError("您的浏览器不支持摄像头访问");
return;
}
// 请求摄像头权限
navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment',
width: { ideal: 1280 },
height: { ideal: 720 }
},
audio: false
}).then(function(stream) {
mediaStream = stream;
scannerVideo.srcObject = stream;
// 确保视频播放
scannerVideo.onloadedmetadata = function() {
scannerVideo.play().catch(e => {
showScannerError("无法播放视频流: " + e.message);
stopScanner();
});
};
// 初始化Quagga
initQuagga();
}).catch(function(err) {
showScannerError("无法访问摄像头: " + (err.message || "用户拒绝授权"));
stopScanner();
});
}
function initQuagga() {
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: scannerVideo,
constraints: {
width: { min: 640 },
height: { min: 480 },
facingMode: "environment",
aspectRatio: { min: 1, max: 2 }
},
},
decoder: {
readers: [
"ean_reader",
"ean_8_reader",
"code_128_reader",
"code_39_reader",
"code_39_vin_reader",
"codabar_reader",
"upc_reader",
"upc_e_reader"
],
debug: {
drawBoundingBox: false,
showFrequency: false,
drawScanline: false,
showPattern: false
}
},
locator: {
patchSize: "medium",
halfSample: true
},
numOfWorkers: 4,
frequency: 10,
debug: false
}, function(err) {
if (err) {
showScannerError("初始化扫描器失败: " + (err.message || "未知错误"));
stopScanner();
return;
}
Quagga.start();
// 监听扫描结果
Quagga.onDetected(function(result) {
if (result.codeResult) {
const code = result.codeResult.code;
scannerResult.textContent = "扫描结果: " + code;
scannerResult.dataset.barcode = code;
scannerResult.style.display = 'block';
// 停止扫描
stopScanner();
scannerModal.style.display = 'none';
// 自动填充输入框并执行查询
queryInput.value = code;
performSearch();
}
});
});
}
function showScannerError(message) {
scannerResult.textContent = message;
scannerResult.style.display = 'block';
scannerRunning = false;
}
// 停止扫描
function stopScanner() {
if (!scannerRunning) return;
try {
if (Quagga) {
Quagga.stop();
}
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
scannerVideo.srcObject = null;
} catch (e) {
console.error("停止扫描时出错:", e);
}
scannerRunning = false;
}
});
</script>
</body>
</html>