eext-leye/iframe/import.html
2026-02-24 16:15:42 +08:00

621 lines
22 KiB
HTML

<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>器件入库 - LEYE</title>
<link href="/iframe/css/index.css" rel="stylesheet" />
<style>
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
#fixed-window {
width: 1280px;
height: 680px;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.table-container {
height: calc(680px - 160px - 64px);
overflow-y: auto;
}
.sticky-header {
position: sticky;
top: 0;
z-index: 10;
}
@keyframes scan {
0% {
top: 0;
}
100% {
top: 100%;
}
}
.scanning #scan-line {
display: block;
animation: scan 2s linear infinite;
}
</style>
<script src="/iframe/js/xlsx.full.min.js" language="JavaScript"></script>
<script src="/iframe/js/jsQR.min.js" language="JavaScript"></script>
<script src="/iframe/js/qrcode.min.js" language="JavaScript"></script>
<script src="/iframe/js/mqtt.min.js" language="JavaScript"></script>
</head>
<body class="bg-gray-100 font-sans text-sm">
<div id="fixed-window" class="bg-gray-50 flex flex-col overflow-hidden mx-auto">
<header class="flex-shrink-0 bg-white border-b border-gray-200 shadow-sm p-4 space-y-3">
<div class="flex items-center space-x-3">
<span class="font-medium text-gray-700 w-32">立创商城订单导入</span>
<input
id="order-uuid"
type="text"
placeholder="UUID 或者 订单链接"
disabled
class="flex-grow px-3 py-1.5 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
/>
<button id="import-order-btn" class="px-6 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">添加订单</button>
<button id="import-order-file-btn" class="px-6 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">
添加订单详情 Excel 文档
</button>
<button id="open-qr-btn" class="px-6 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">扫描二维码</button>
</div>
<div class="flex items-center space-x-3">
<span class="font-medium text-gray-700 w-32">立创商城编号导入</span>
<input
id="single-cid"
type="text"
placeholder="CID"
class="w-64 px-3 py-1.5 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
/>
<input
id="single-qty"
type="number"
placeholder="数量"
class="w-32 px-3 py-1.5 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
/>
<button id="import-cid-btn" class="px-6 py-1.5 bg-green-600 text-white rounded-md hover:bg-green-700 transition">添加器件</button>
</div>
</header>
<div class="px-4 py-2 bg-gray-50 border-b flex justify-between items-center">
<div class="space-x-2">
<button id="select-all" class="px-3 py-1 bg-white border border-gray-300 rounded hover:bg-gray-100 text-xs">全选</button>
<button id="select-reverse" class="px-3 py-1 bg-white border border-gray-300 rounded hover:bg-gray-100 text-xs">反选</button>
</div>
<div class="text-gray-500 text-xs">待入库项:<span id="list-count" class="font-bold text-blue-600">0</span></div>
</div>
<main class="flex-grow bg-white overflow-hidden m-3 border rounded-lg shadow-inner">
<div class="table-container overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-100 sticky-header text-xs uppercase text-gray-600">
<tr>
<th class="w-12 px-4 py-3 text-center">选择</th>
<th class="px-3 py-3 text-left">型号</th>
<th class="px-3 py-3 text-left">类型</th>
<th class="px-3 py-3 text-left"></th>
<th class="px-3 py-3 text-left">封装</th>
<th class="px-3 py-3 text-left">品牌</th>
<th class="w-24 px-3 py-3 text-center">入库数量</th>
<th class="px-3 py-3 text-left">CID</th>
<th class="w-16 px-3 py-3 text-center">操作</th>
</tr>
</thead>
<tbody id="import-table-body" class="bg-white divide-y divide-gray-200 text-xs">
<tr id="empty-row">
<td colspan="9" class="text-center py-20 text-gray-400">列表为空,请从上方导入数据</td>
</tr>
</tbody>
</table>
</div>
</main>
<div class="flex-shrink-0 p-4 bg-white border-t border-gray-200 flex justify-end space-x-3 shadow-lg">
<button id="clear-list-btn" class="px-6 py-2 border border-red-300 text-red-600 rounded-md hover:bg-red-50">清空列表</button>
<button id="batch-save-btn" class="px-8 py-2 bg-blue-600 text-white font-bold rounded-md hover:bg-blue-700 shadow-md">
批量入库
</button>
</div>
<div id="qr-dialog" class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white rounded-lg shadow-xl w-[450px] overflow-hidden">
<div class="px-4 py-3 border-b flex justify-between items-center bg-gray-50">
<h3 class="font-bold text-gray-700">扫码入库</h3>
<button onclick="closeQrDialog()" class="text-gray-400 hover:text-gray-600">&times;</button>
</div>
<div class="p-4 space-y-4">
<select id="camera-select" class="w-full px-3 py-2 border rounded-md text-sm outline-none focus:ring-2 focus:ring-blue-500">
<option value="">正在检测摄像头...</option>
</select>
<div class="relative w-[250px] h-[250px] mx-auto bg-black rounded-lg overflow-hidden border-2 border-gray-300">
<video id="qr-video" class="absolute inset-0 w-full h-full object-cover shadow-inner" playsinline></video>
<canvas id="qr-canvas" class="hidden"></canvas>
<div
id="scan-line"
class="absolute left-0 right-0 h-0.5 bg-blue-500 opacity-50 shadow-[0_0_8px_rgba(59,130,246,0.8)] hidden"
></div>
</div>
<div id="scan-result" class="p-3 bg-blue-50 rounded-md border border-blue-100">
<div class="text-sm space-y-1">
<p>CID: <span id="res-cid"></span></p>
<p>型号: <span id="res-pm"></span></p>
<p>数量: <span id="res-qty"></span></p>
</div>
</div>
<div class="grid grid-cols-3 gap-2">
<button id="btn-start-camera" class="py-2 bg-gray-600 text-white rounded hover:bg-gray-700 text-sm">打开摄像头</button>
<button id="btn-scan" class="py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm disabled:opacity-50" disabled>
扫描二维码
</button>
<button
id="btn-add-to-list"
class="py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm disabled:opacity-50"
disabled
>
加入列表
</button>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const SERVER = eda.sys_Storage.getExtensionUserConfig('server-host') ?? 'http://localhost:21816/api';
const AUTO_RUN = eda.sys_Storage.getExtensionUserConfig('server-auto-run') ?? true;
const tableBody = document.getElementById('import-table-body');
const listCount = document.getElementById('list-count');
const qrDialog = document.getElementById('qr-dialog');
const qrVideo = document.getElementById('qr-video');
const qrCanvas = document.getElementById('qr-canvas');
const cameraSelect = document.getElementById('camera-select');
let importList = [];
let videoStream = null;
let currentScanData = null;
let mqttClient = null;
const myDeviceId = Math.random().toString(36).substring(2, 10);
function renderList() {
if (importList.length === 0) {
tableBody.innerHTML = `<tr id="empty-row"><td colspan="9" class="text-center py-20 text-gray-400">列表为空,请从上方导入数据</td></tr>`;
listCount.textContent = 0;
return;
}
tableBody.innerHTML = importList
.map(
(item, index) => `
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 text-center"><input type="checkbox" class="row-checkbox" data-index="${index}" ${item.selected ? 'checked' : ''}></td>
<td class="px-3 py-2 font-medium">${item.name || '-'}</td>
<td class="px-3 py-2 text-gray-600">${item.childCat || '-'}</td>
<td class="px-3 py-2">${item.value || '-'}</td>
<td class="px-3 py-2">${item.footprint || '-'}</td>
<td class="px-3 py-2">${item.brand || '-'}</td>
<td class="px-3 py-2 text-center">
<input type="number" class="w-16 border rounded text-center" value="${item.quantity}" onchange="updateQty(${index}, this.value)">
</td>
<td class="px-3 py-2 text-blue-600 font-mono">${item.lcscId}</td>
<td class="px-3 py-2 text-center">
<button onclick="removeItem(${index})" class="text-red-500 hover:text-red-700">删除</button>
</td>
</tr>
`,
)
.join('');
listCount.textContent = importList.length;
}
async function updateCameraList() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter((device) => device.kind === 'videoinput');
let options = videoDevices
.map((d) => `<option value="${d.deviceId}">${d.label || '摄像头 ' + d.deviceId.slice(0, 5)}</option>`)
.join('');
options += `<option value="WEBSOCKET_MODE">WebSocket 远程扫码</option>`;
cameraSelect.innerHTML = options;
} catch (e) {
eda.sys_Message.showToastMessage('无法获取摄像头列表: ' + e.message, ESYS_ToastMessageType.ERROR);
}
}
window.closeQrDialog = () => {
stopCamera();
qrDialog.classList.add('hidden');
if (currentScanData) {
document.getElementById('res-cid').textContent = currentScanData.lcscId;
document.getElementById('res-pm').textContent = currentScanData.name;
document.getElementById('res-qty').textContent = currentScanData.quantity;
currentScanData = null;
}
};
function stopCamera() {
if (videoStream) {
videoStream.getTracks().forEach((track) => track.stop());
videoStream = null;
}
if (mqttClient) {
console.log('正在断开 MQTT 远程连接...');
mqttClient.end(true); // 强制关闭连接
mqttClient = null;
}
qrVideo.style.display = 'block';
const oldQr = document.getElementById('remote-qr-canvas');
if (oldQr) oldQr.remove();
if (qrVideo.parentElement) {
qrVideo.parentElement.classList.remove('scanning');
}
}
async function startRemoteMode() {
if (mqttClient) {
mqttClient.end(true);
}
const broker = 'wss://test.mosquitto.org:8081';
const topic = `leye/scan/${myDeviceId}`;
mqttClient = mqtt.connect(broker);
mqttClient.on('connect', () => {
mqttClient.subscribe(topic);
eda.sys_Message.showToastMessage('MQTT 远程模式启动', ESYS_ToastMessageType.SUCCESS);
const scanUrl = `https://leye.dragon.edu.kg/scan.html?id=${myDeviceId}`;
qrVideo.style.display = 'none';
let qrCanvas = document.getElementById('remote-qr-canvas') || createQrCanvas();
QRCode.toCanvas(qrCanvas, scanUrl, { width: 250 });
});
mqttClient.on('message', (t, message) => {
try {
const msg = JSON.parse(message.toString());
if (msg.type === 'SCAN_RESULT') {
parseQrData(msg.content);
eda.sys_Message.showToastMessage('远程扫码成功', ESYS_ToastMessageType.INFO);
}
} catch (e) {
console.error('MQTT数据解析失败', e);
}
});
}
function createQrCanvas() {
const canvas = document.createElement('canvas');
canvas.id = 'remote-qr-canvas';
canvas.className = 'absolute inset-0 w-full h-full p-4 bg-white';
qrVideo.parentElement.appendChild(canvas);
return canvas;
}
async function startLocalCamera(deviceId) {
const constraints = {
video: deviceId ? { deviceId: { exact: deviceId } } : { facingMode: 'environment' },
};
try {
videoStream = await navigator.mediaDevices.getUserMedia(constraints);
qrVideo.srcObject = videoStream;
qrVideo.play();
document.getElementById('btn-scan').disabled = false;
qrVideo.parentElement.classList.add('scanning');
} catch (e) {
eda.sys_Message.showToastMessage('启动摄像头失败', ESYS_ToastMessageType.ERROR);
}
}
document.getElementById('btn-start-camera').onclick = async () => {
stopCamera();
const mode = cameraSelect.value;
if (mode === 'WEBSOCKET_MODE') {
startRemoteMode();
} else {
startLocalCamera(mode);
}
};
document.getElementById('btn-scan').onclick = () => {
if (!videoStream) return;
const ctx = qrCanvas.getContext('2d', { willReadFrequently: true });
qrCanvas.width = qrVideo.videoWidth;
qrCanvas.height = qrVideo.videoHeight;
ctx.drawImage(qrVideo, 0, 0, qrCanvas.width, qrCanvas.height);
const imageData = ctx.getImageData(0, 0, qrCanvas.width, qrCanvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code) {
eda.sys_Log.add(code.data);
parseQrData(code.data);
} else {
eda.sys_Message.showToastMessage('未扫描到有效二维码,请重试', ESYS_ToastMessageType.WARNING);
}
};
function parseQrData(data) {
try {
// 使用正则提取 pc, pm, qty
const pcMatch = data.match(/pc:([^,}]+)/);
const pmMatch = data.match(/pm:([^,}]+)/);
const qtyMatch = data.match(/qty:(\d+)/);
if (pcMatch) {
currentScanData = {
lcscId: pcMatch[1].trim().toUpperCase(),
name: pmMatch ? pmMatch[1].trim() : '未知型号',
quantity: qtyMatch ? parseInt(qtyMatch[1]) : 1,
selected: true,
};
document.getElementById('res-cid').textContent = currentScanData.lcscId;
document.getElementById('res-pm').textContent = currentScanData.name;
document.getElementById('res-qty').textContent = currentScanData.quantity;
document.getElementById('btn-add-to-list').disabled = false;
} else {
throw new Error('无效的二维码格式');
}
} catch (e) {
eda.sys_Message.showToastMessage('解析失败: ' + e.message, ESYS_ToastMessageType.ERROR);
}
}
document.getElementById('btn-add-to-list').onclick = async () => {
if (!currentScanData) return;
const btn = document.getElementById('btn-add-to-list');
btn.disabled = true;
btn.textContent = '处理中...';
try {
const item = { ...currentScanData };
await enrichData([item]);
importList.push(item);
renderList();
eda.sys_Message.showToastMessage('已加入待入库列表', ESYS_ToastMessageType.SUCCESS);
document.getElementById('res-cid').textContent = '等待扫描...';
document.getElementById('res-pm').textContent = '等待扫描...';
document.getElementById('res-qty').textContent = '等待扫描...';
currentScanData = null;
} catch (e) {
eda.sys_Message.showToastMessage('获取详情失败', ESYS_ToastMessageType.ERROR);
} finally {
btn.disabled = false;
btn.textContent = '加入列表';
}
};
window.updateQty = (index, val) => {
importList[index].quantity = parseInt(val) || 0;
};
window.removeItem = (index) => {
importList.splice(index, 1);
renderList();
};
document.getElementById('import-order-btn').onclick = async () => {
eda.sys_Dialog.showInformationMessage('此功能因可能存在的安全问题已被弃用', '提示', '知道了');
};
document.getElementById('open-qr-btn').onclick = async () => {
qrDialog.classList.remove('hidden');
await updateCameraList();
};
document.getElementById('import-order-file-btn').onclick = async () => {
try {
const file = await eda.sys_FileSystem.openReadFileDialog(['xls', ['xlsx']], false);
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const newItems = [];
for (let i = 18; i < jsonData.length; i++) {
const row = jsonData[i];
if (!row || row.length < 2) continue; // 跳过空行
// 第2列(1): lcscId, 第3列(2): 品牌, 第4列(3): 型号, 第5列(4): 封装, 第7列(6): 数量
const lcscId = (row[1] || '').toString().trim().toUpperCase();
if (!lcscId) continue;
const rawQty = (row[6] || '0').toString();
const qtyMatch = rawQty.match(/\d+/);
const quantity = qtyMatch ? parseInt(qtyMatch[0]) : 0;
newItems.push({
lcscId: lcscId,
brand: row[2] || '-',
name: row[3] || '-',
footprint: row[4] || '-',
quantity: quantity,
value: '-',
childCat: '-',
selected: true,
});
}
if (newItems.length > 0) {
// await enrichData(newItems);
importList.push(...newItems);
renderList();
eda.sys_Message.showToastMessage(`从文件成功导入 ${newItems.length} 个器件`, ESYS_ToastMessageType.SUCCESS);
} else {
eda.sys_Message.showToastMessage('未在找到有效数据', ESYS_ToastMessageType.WARNING);
}
};
reader.readAsArrayBuffer(file);
} catch (err) {
eda.sys_Message.showToastMessage('读取文件失败: ' + err.message, ESYS_ToastMessageType.ERROR);
eda.sys_Log.add('Excel 导入错误: ' + err.stack);
}
};
document.getElementById('import-cid-btn').onclick = async () => {
const cid = document.getElementById('single-cid').value.trim().toUpperCase();
const qty = parseInt(document.getElementById('single-qty').value);
if (!cid || !qty) return eda.sys_Message.showToastMessage('请完整填写编号和数量', ESYS_ToastMessageType.ERROR);
try {
const items = [{ lcscId: cid, quantity: qty, selected: true }];
await enrichData(items);
importList.push(...items);
renderList();
document.getElementById('single-cid').value = '';
document.getElementById('single-qty').value = '';
} catch (e) {
eda.sys_Message.showToastMessage('查询失败', ESYS_ToastMessageType.ERROR);
eda.sys_Log.add(e.message);
}
};
async function enrichData(items) {
const lcscIds = items.map((i) => i.lcscId);
const devs = await eda.lib_Device.getByLcscIds(lcscIds);
for (let i = 0; i < items.length; i++) {
const dev = devs[i];
const item = items[i];
if (dev && dev.uuid) {
try {
const EDA_HOST = eda.sys_Environment.isClient() ? 'https://client' : 'https://pro.lceda.cn';
const infoRes = await eda.sys_ClientUrl.request(EDA_HOST + '/api/v2/devices/' + dev.uuid, 'GET', null, {
headers: { path: '0819f05c4eef4c71ace90d822a990e87' },
});
const infoJson = await infoRes.json();
const info = infoJson.result;
if (info && info.attributes) {
item.name = info.attributes['Manufacturer Part'] || item.name;
item.childCat = info.tags?.child_tag?.name_cn || item.childCat;
item.value = info.attributes['Value'] || '-';
item.footprint = info.attributes['Supplier Footprint'] || item.footprint;
item.brand = info.attributes['Manufacturer'] || item.brand;
item.uuid = dev.uuid;
}
} catch (e) {
console.error(`获取器件 ${item.lcscId} 详情失败:`, e);
eda.sys_Log.add(`获取器件 ${item.lcscId} 详情失败: ${e.message}`);
}
}
}
}
document.getElementById('select-all').onclick = () => {
importList.forEach((i) => (i.selected = true));
renderList();
};
document.getElementById('select-reverse').onclick = () => {
importList.forEach((i) => (i.selected = !i.selected));
renderList();
};
tableBody.addEventListener('change', (e) => {
if (e.target.classList.contains('row-checkbox')) {
const idx = e.target.dataset.index;
importList[idx].selected = e.target.checked;
}
});
document.getElementById('clear-list-btn').onclick = () => {
importList = [];
renderList();
};
document.getElementById('batch-save-btn').onclick = async () => {
const toSave = importList.filter((i) => i.selected && i.lcscId);
if (toSave.length === 0) {
return eda.sys_Message.showToastMessage('请先选择要入库的项', ESYS_ToastMessageType.WARNING);
}
let successCount = 0;
let failCount = 0;
for (const item of toSave) {
try {
const postData = JSON.stringify({
name: item.name || '-',
lcscId: item.lcscId,
quantity: item.quantity,
});
let res = await eda.sys_ClientUrl.request(SERVER + '/addLeyeList', 'POST', postData, {
headers: { 'Content-Type': 'application/json' },
});
let result = await res.json();
if (AUTO_RUN && !result.success) {
window.open('leye://open');
for (let i = 0; i < 3 && !result.success; i++) {
eda.sys_Message.showToastMessage('等待拉起本地服务端...', ESYS_ToastMessageType.INFO);
res = await eda.sys_ClientUrl.request(SERVER + '/addLeyeList', 'POST', postData, {
headers: { 'Content-Type': 'application/json' },
});
result = await res.json();
await new Promise((resolve) => setTimeout(resolve, 1500));
}
}
if (result.success) {
successCount++;
const idx = importList.indexOf(item);
if (idx > -1) importList.splice(idx, 1);
} else {
failCount++;
}
} catch (e) {
console.error(`入库失败: ${item.lcscId}`, e);
eda.sys_Log.add(`入库失败: ${item.lcscId}: ${e.message}`);
failCount++;
}
}
renderList();
if (failCount === 0) {
eda.sys_Message.showToastMessage(`全部入库成功,共 ${successCount}`, ESYS_ToastMessageType.SUCCESS);
} else {
eda.sys_Message.showToastMessage(
`入库完成,成功: ${successCount}, 失败: ${failCount}`,
failCount > 0 ? ESYS_ToastMessageType.WARNING : ESYS_ToastMessageType.SUCCESS,
);
}
};
});
</script>
</body>
</html>