382 lines
14 KiB
HTML
382 lines
14 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>
|
|
#fixed-window {
|
|
width: 1000px;
|
|
height: 600px;
|
|
border: 1px solid #e5e7eb;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
}
|
|
.table-container {
|
|
height: calc(600px - 80px - 44px - 64px);
|
|
overflow-y: auto;
|
|
}
|
|
.bg-match {
|
|
background-color: #f0fdf4;
|
|
} /* 淡绿色:匹配成功 */
|
|
.bg-no-match {
|
|
background-color: #fefcf2;
|
|
} /* 淡黄色:不匹配 */
|
|
.bg-no-cid {
|
|
background-color: #fef2f2;
|
|
} /* 淡红色:无 CID */
|
|
.sticky-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
.designator-cell {
|
|
max-width: 120px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|
|
</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 p-4 flex items-center space-x-3">
|
|
<span class="font-medium text-gray-700">立创商城编号导入</span>
|
|
<input
|
|
id="input-cid"
|
|
type="text"
|
|
placeholder="CID"
|
|
class="w-48 px-3 py-1.5 border rounded-md outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
<input
|
|
id="input-qty"
|
|
type="number"
|
|
placeholder="数量"
|
|
class="w-24 px-3 py-1.5 border rounded-md outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
<button id="add-manual-btn" class="px-4 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700">添加</button>
|
|
</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 m-3 border rounded-lg overflow-hidden 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 text-gray-600">
|
|
<tr>
|
|
<th class="w-12 px-2 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-20 px-3 py-3 text-center">数量</th>
|
|
<th class="w-32 px-3 py-3 text-left">CID</th>
|
|
<th class="w-16 px-3 py-3 text-center">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="outbound-table-body" class="divide-y divide-gray-200 text-xs">
|
|
<tr>
|
|
<td colspan="9" class="text-center py-20 text-gray-400">获取 BOM...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="flex-shrink-0 p-4 bg-white border-t flex justify-end space-x-3">
|
|
<p class="pr-24 text-red-500 py-2">建议“器件标准化”后使用此功能</p>
|
|
<button id="cancel-btn" class="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-100">取消</button>
|
|
<button id="fill-bom-btn" class="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-100">应用 BOM</button>
|
|
<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="outbound-btn" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-bold">一键出库</button>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', async function () {
|
|
const tableBody = document.getElementById('outbound-table-body');
|
|
const listCount = document.getElementById('list-count');
|
|
let outboundList = [];
|
|
let cachedCIDs = new Set();
|
|
|
|
async function initCache() {
|
|
const cachedRaw = await eda.sys_Storage.getExtensionUserConfig('cache-leye-device-details');
|
|
try {
|
|
const details = cachedRaw ? JSON.parse(cachedRaw) : [];
|
|
cachedCIDs = new Set(details.map((d) => String(d.lcscId).toUpperCase()));
|
|
} catch (e) {
|
|
cachedCIDs = new Set();
|
|
}
|
|
}
|
|
|
|
function parseProperty(comp, key) {
|
|
if (key === 'Manufacturer Part') return comp.getState_ManufacturerId();
|
|
const props = comp.getState_OtherProperty() || {};
|
|
return props[key] || '-';
|
|
}
|
|
|
|
function resolveModelName(comp) {
|
|
let name = comp.getState_Name();
|
|
if (name && name.startsWith('=')) {
|
|
const key = name.replace(/[={}]/g, '');
|
|
return parseProperty(comp, key);
|
|
}
|
|
return name || '-';
|
|
}
|
|
|
|
async function scanSchematic() {
|
|
const all = await eda.sch_PrimitiveComponent.getAll('part', true);
|
|
const groups = {};
|
|
|
|
all.forEach((comp) => {
|
|
const uuid = comp.getState_Component().uuid;
|
|
if (!groups[uuid]) {
|
|
groups[uuid] = {
|
|
name: resolveModelName(comp),
|
|
designators: [],
|
|
value: parseProperty(comp, 'Value'),
|
|
footprint: parseProperty(comp, 'Supplier Footprint'),
|
|
brand: comp.getState_Manufacturer() || '-',
|
|
lcscId: (comp.getState_SupplierId() || '').toUpperCase(),
|
|
quantity: 0,
|
|
selected: false,
|
|
};
|
|
}
|
|
groups[uuid].quantity++;
|
|
groups[uuid].designators.push(comp.getState_Designator());
|
|
});
|
|
|
|
outboundList = Object.keys(groups).map((uuid) => {
|
|
const item = groups[uuid];
|
|
const match = item.lcscId && cachedCIDs.has(item.lcscId);
|
|
return {
|
|
...item,
|
|
designatorStr: item.designators.sort().join(', '),
|
|
uuid: uuid,
|
|
selected: match,
|
|
};
|
|
});
|
|
renderTable();
|
|
}
|
|
|
|
function renderTable() {
|
|
if (outboundList.length === 0) {
|
|
tableBody.innerHTML = `<tr><td colspan="9" class="text-center py-20 text-gray-400">BOM 为空</td></tr>`;
|
|
listCount.textContent = '0';
|
|
return;
|
|
}
|
|
|
|
listCount.textContent = outboundList.length;
|
|
tableBody.innerHTML = outboundList
|
|
.map((item, index) => {
|
|
const isMatch = item.lcscId && cachedCIDs.has(item.lcscId);
|
|
let rowClass = 'bg-no-cid';
|
|
if (item.lcscId) {
|
|
rowClass = isMatch ? 'bg-match' : 'bg-no-match';
|
|
}
|
|
|
|
return `
|
|
<tr class="${rowClass}">
|
|
<td class="px-2 py-2 text-center">
|
|
<input type="checkbox" class="row-checkbox" data-index="${index}" ${item.selected ? 'checked' : ''} ${!isMatch ? 'title="非库内器件无法选择"' : ''}>
|
|
</td>
|
|
<td class="px-3 py-2 font-medium">${item.name}</td>
|
|
<td class="px-3 py-2 text-gray-600">
|
|
<div class="designator-cell" title="${item.designatorStr}">${item.designatorStr}</div>
|
|
</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 bg-transparent" value="${item.quantity}" onchange="updateRow(${index}, 'quantity', this.value)">
|
|
</td>
|
|
<td class="px-3 py-2">
|
|
<input type="text" class="w-32 border rounded px-1 bg-transparent font-mono" value="${item.lcscId}" onchange="updateRow(${index}, 'lcscId', this.value)">
|
|
</td>
|
|
<td class="px-3 py-2 text-center">
|
|
<button onclick="removeItem(${index})" class="text-red-500 hover:underline">删除</button>
|
|
</td>
|
|
</tr>`;
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
tableBody.addEventListener('change', (e) => {
|
|
if (e.target.classList.contains('row-checkbox')) {
|
|
const idx = e.target.dataset.index;
|
|
const item = outboundList[idx];
|
|
const isMatch = item.lcscId && cachedCIDs.has(item.lcscId);
|
|
|
|
if (e.target.checked && !isMatch) {
|
|
e.target.checked = false;
|
|
item.selected = false;
|
|
eda.sys_Message.showToastMessage('只有已入库的器件才能被勾选', ESYS_ToastMessageType.WARNING);
|
|
return;
|
|
}
|
|
item.selected = e.target.checked;
|
|
}
|
|
});
|
|
|
|
document.getElementById('select-all').onclick = () => {
|
|
outboundList.forEach((item) => {
|
|
if (item.lcscId && cachedCIDs.has(item.lcscId)) {
|
|
item.selected = true;
|
|
}
|
|
});
|
|
renderTable();
|
|
};
|
|
|
|
document.getElementById('select-reverse').onclick = () => {
|
|
outboundList.forEach((item) => {
|
|
if (item.lcscId && cachedCIDs.has(item.lcscId)) {
|
|
item.selected = !item.selected;
|
|
}
|
|
});
|
|
renderTable();
|
|
};
|
|
|
|
window.updateRow = (index, key, val) => {
|
|
if (key === 'quantity') outboundList[index].quantity = parseInt(val) || 0;
|
|
if (key === 'lcscId') {
|
|
outboundList[index].lcscId = val.toUpperCase();
|
|
if (!cachedCIDs.has(outboundList[index].lcscId)) {
|
|
outboundList[index].selected = false;
|
|
}
|
|
renderTable();
|
|
}
|
|
};
|
|
|
|
window.removeItem = (index) => {
|
|
outboundList.splice(index, 1);
|
|
renderTable();
|
|
};
|
|
|
|
document.getElementById('add-manual-btn').onclick = async () => {
|
|
const cid = document.getElementById('input-cid').value.trim().toUpperCase();
|
|
const qty = parseInt(document.getElementById('input-qty').value);
|
|
if (!cid || !qty) return;
|
|
|
|
const devs = await eda.lib_Device.getByLcscIds([cid]);
|
|
let detail = { name: '-', value: '-', footprint: '-', brand: '-', designatorStr: '手动添加', selected: false };
|
|
|
|
if (devs[0]) {
|
|
const EDA_HOST =
|
|
eda.sys_Environment.isClient() && !eda.sys_Environment.isOnlineMode() ? 'https://client' : 'https://pro.lceda.cn';
|
|
const infoRes = await eda.sys_ClientUrl.request(EDA_HOST + '/api/v2/devices/' + devs[0].uuid, 'GET', null, {
|
|
headers: { path: '0819f05c4eef4c71ace90d822a990e87' },
|
|
});
|
|
const info = (await infoRes.json()).result;
|
|
detail.name = info.attributes['Manufacturer Part'] || '-';
|
|
detail.value = info.attributes['Value'] || '-';
|
|
detail.footprint = info.attributes['Supplier Footprint'] || '-';
|
|
detail.brand = info.attributes['Manufacturer'] || '-';
|
|
if (cachedCIDs.has(cid)) detail.selected = true;
|
|
}
|
|
|
|
outboundList.push({ ...detail, lcscId: cid, quantity: qty });
|
|
renderTable();
|
|
document.getElementById('input-cid').value = '';
|
|
};
|
|
|
|
document.getElementById('clear-list-btn').onclick = () => {
|
|
outboundList = [];
|
|
renderTable();
|
|
};
|
|
|
|
document.getElementById('outbound-btn').onclick = async () => {
|
|
const selectedItems = outboundList.filter((item) => item.selected && item.lcscId);
|
|
|
|
if (selectedItems.length === 0) {
|
|
return eda.sys_Message.showToastMessage('请先勾选要出库的器件', ESYS_ToastMessageType.WARNING);
|
|
}
|
|
|
|
const SERVER = (await eda.sys_Storage.getExtensionUserConfig('server-host')) ?? 'http://localhost:21816';
|
|
const AUTO_RUN = (await eda.sys_Storage.getExtensionUserConfig('server-auto-run')) ?? true;
|
|
let successCount = 0;
|
|
let failItems = [];
|
|
|
|
// eda.sys_Message.showToastMessage(`正在处理 ${selectedItems.length} 项出库...`, ESYS_ToastMessageType.INFO);
|
|
|
|
for (const item of selectedItems) {
|
|
try {
|
|
let getRes = await eda.sys_ClientUrl.request(`${SERVER}/getLeyeList?lcscId=${item.lcscId}`, 'GET');
|
|
let getResult = await getRes.json();
|
|
|
|
if (AUTO_RUN && !getResult.success) {
|
|
window.open('leye://open');
|
|
for (let i = 0; i < 3 && !getResult.success; i++) {
|
|
eda.sys_Message.showToastMessage('等待拉起本地服务端...', ESYS_ToastMessageType.INFO);
|
|
getRes = await eda.sys_ClientUrl.request(`${SERVER}/getLeyeList?lcscId=${item.lcscId}`, 'GET');
|
|
getResult = await getRes.json();
|
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
}
|
|
}
|
|
|
|
if (getResult.success && getResult.data && getResult.data.length > 0) {
|
|
const remoteData = getResult.data[0];
|
|
const remoteId = remoteData.id;
|
|
const remoteQty = remoteData.quantity;
|
|
|
|
const newQuantity = remoteQty - item.quantity;
|
|
|
|
if (newQuantity < 0) {
|
|
failItems.push(`${item.lcscId} (库存不足: 剩${remoteQty})`);
|
|
continue;
|
|
}
|
|
|
|
const postRes = await eda.sys_ClientUrl.request(
|
|
SERVER + '/editLeyeList',
|
|
'POST',
|
|
JSON.stringify({
|
|
id: remoteId,
|
|
quantity: newQuantity,
|
|
}),
|
|
{ headers: { 'Content-Type': 'application/json' } },
|
|
);
|
|
|
|
const postResult = await postRes.json();
|
|
if (postResult.success) {
|
|
successCount++;
|
|
item.selected = false;
|
|
} else {
|
|
failItems.push(`${item.lcscId} (更新失败)`);
|
|
}
|
|
} else {
|
|
failItems.push(`${item.lcscId} (库内未找到)`);
|
|
}
|
|
} catch (e) {
|
|
console.error(`出库请求异常: ${item.lcscId}`, e);
|
|
eda.sys_Log.add(`出库请求异常: ${item.lcscId}: ${e.message}`);
|
|
failItems.push(`${item.lcscId} (网络异常)`);
|
|
}
|
|
}
|
|
|
|
renderTable();
|
|
|
|
if (failItems.length === 0) {
|
|
eda.sys_Message.showToastMessage(`成功出库 ${successCount} 项!`, ESYS_ToastMessageType.SUCCESS);
|
|
} else {
|
|
const errorMsg = failItems.join('; ');
|
|
eda.sys_Message.showToastMessage(`出库完成:成功:${successCount},失败:${failItems.length}`, ESYS_ToastMessageType.WARNING);
|
|
eda.sys_Log.add(errorMsg);
|
|
}
|
|
};
|
|
|
|
document.getElementById('fill-bom-btn').onclick = () => scanSchematic();
|
|
|
|
document.getElementById('cancel-btn').onclick = () => eda.sys_IFrame.closeIFrame();
|
|
|
|
await initCache();
|
|
if (eda.sys_Storage.getExtensionUserConfig('auto-fill-bom')) await scanSchematic();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|