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

381 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() ? '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>