Compare commits

..

No commits in common. "master" and "v1.0.6" have entirely different histories.

11 changed files with 1991 additions and 2526 deletions

View File

@ -17,5 +17,5 @@
/package-lock.json
/package.json
/tsconfig.json
/backend/
debug.log
/iframe/js/s_*

View File

@ -1,20 +1,3 @@
# 1.0.9
1. 修复全在线客户端无法获取器件数据的问题
2. 扩展包不再打包服务端源码
3. 更新 README 文档
# 1.0.8
1. 修复“允许拉起服务端”配置项无效的问题
2. 修复网页端无法获取器件数据的问题
# 1.0.7
1. 新增“出库自动填充 BOM”选项开启后打开出库界面将自动填充 BOM
2. 新增“自动检查更新”选项:开启后,插件将在 EDA 启动时自动检查更新并提示用户
3. 移除不必要的依赖包
# 1.0.6
1. 增加远程扫码入库功能

View File

@ -10,16 +10,14 @@ LEYE 电子元器件库存管理系统 EDA 联动扩展
本扩展允许用户通过立创商城 C 编号、立创商城订单导入元器件,支持从库存内查询、放置器件,支持通过 BOM 批量出库。
使用本扩展需要在本地安装 [LEYE Service 服务端](https://lrurl.top/LeyeService)(目前仅提供 Windows 版[服务端开源](/backend/server.js)),数据均储存在本地,不会上传。
使用本扩展需要在本地安装 [LEYE Service 服务端](https://lrurl.top/LeyeService)(目前仅提供 Windows 版),数据均储存在本地,不会上传。
## [介绍视频](https://www.bilibili.com/video/BV1nvcFzpEuP/)
https://www.bilibili.com/video/BV1nvcFzpEuP/
## 如何使用
安装本扩展后**需要给予扩展外部交互**权限以与服务端交互:
- V2.2:设置-扩展-扩展管理器-LEYE-允许外部交互;
- V3高级-扩展管理器-已安装-LEYE-配置-允许外部交互。
@ -43,13 +41,13 @@ V2.2 用户菜单栏将直接出现“LEYE”选项V3 用户若未开启“
![img03](/images/img_03.png)
> **Tip: 扫描立创商城二维码导入器件** _V1.0.5+_
>
> **Tip: 扫描立创商城二维码导入器件** *V1.0.5+*
>
> 在 V1.0.5 版本以上,可以通过扫描立创商城物料包装上的二维码导入器件,扫描后会自动识别 CID 并导入对应器件。
> ![img07](/images/img_07.png)
> **Tip: 使用手机扫码导入器件** _V1.0.6+_
>
> **Tip: 使用手机扫码导入器件** *V1.0.6+*
>
> 在 V1.0.6 版本以上可以在扫码界面选择“WebSocket 远程扫码”,点击“打开摄像头”按钮后用手机扫描二维码即可打开远程端。
> 远程端同样支持扫描立创商城物料包装上的二维码导入器件,扫描后会自动识别 CID 并导入对应器件。
> ![img07](/images/img_08.png)
@ -64,10 +62,10 @@ V2.2 用户菜单栏将直接出现“LEYE”选项V3 用户若未开启“
## 配置项说明
| 配置项 | 说明 | 默认值 |
| :------------- | :----------------------------------------------------------------------- | :----------------------- |
| 服务器地址 | LEYE Service 服务器地址 | `http://localhost:21816` |
| 允许拉起服务端 | 允许通过 [`leye://open`](leye://open) 拉起本地安装的 LEYE Service 服务端 | `true` |
| 配置项 | 说明 | 默认值 |
|:-------------|:-----------------------------------------------------------|:-------------------------|
| 服务器地址 | LEYE Service 服务器地址 | `http://localhost:21816` |
| 允许拉起服务端 | 允许通过 [`leye://open`](leye://open) 拉起本地安装的 LEYE Service 服务端 | `true` |
## 已知问题
@ -76,7 +74,6 @@ V2.2 用户菜单栏将直接出现“LEYE”选项V3 用户若未开启“
## 开源许可
本扩展使用以下开源软件:
- [SheetJS](https://www.npmjs.com/package/xlsx)Excel 解析库,使用 [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) 许可协议授权
- [jsQR](https://www.npmjs.com/package/jsqr):二维码解析库,使用 [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) 许可协议授权
- [node-qrcode](https://www.npmjs.com/package/qrcode):二维码生成库,使用 [MIT License](https://choosealicense.com/licenses/mit/) 许可协议授权

View File

@ -3,7 +3,7 @@
"uuid": "944f7c94a8ca485e848f1118effcbb9a",
"displayName": "LEYE",
"description": "LEYE 电子元器件库存管理系统 EDA 联动扩展",
"version": "1.0.9",
"version": "1.0.6",
"publisher": "Mr_Fang",
"engines": {
"eda": "^3.2.80"
@ -14,7 +14,9 @@
"url": "https://gitea.miri.site/Mr_Fang/eext-leye"
},
"categories": "Schematic",
"keywords": ["Tools", "库管", "库存管理"],
"keywords": [
"Tools", "库管", "库存管理"
],
"images": {
"logo": "./images/logo.png"
},

File diff suppressed because it is too large Load Diff

View File

@ -1,188 +1,151 @@
<!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>
<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 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>
<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();
<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>
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();
}
<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="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';
}
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 `
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="非库内器件无法选择"' : ''}>
@ -204,178 +167,178 @@
<button onclick="removeItem(${index})" class="text-red-500 hover:underline">删除</button>
</td>
</tr>`;
})
.join('');
}).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;
}
});
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();
document.getElementById('select-all').onclick = () => {
outboundList.forEach(item => {
if (item.lcscId && cachedCIDs.has(item.lcscId)) {
item.selected = true;
}
});
</script>
</body>
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 infoRes = await eda.sys_ClientUrl.request('https://client/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('cancel-btn').onclick = () => eda.sys_IFrame.closeIFrame();
await initCache();
await scanSchematic();
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -109,27 +109,16 @@
</label>
<input type="checkbox" id="settings-server-auto-run" />
</div>
<div class="flex items-center justify-between mb-4">
<label for="settings-auto-fill-bom" class="text-gray-700">出库自动填充 BOM</label>
<input type="checkbox" id="settings-auto-fill-bom" />
</div>
</div>
<div class="bg-white p-4 rounded-lg w-[400px]">
<h2 class="text-lg font-semibold text-gray-800 mb-4">
高级设置
<span class="text-[12px] text-gray-500 font-light ml-2"> 实验或调试选项 </span>
</h2>
<div class="flex items-center justify-between mb-4">
<label for="settings-auto-check-update" class="text-gray-700">
自动检查更新
<span class="tooltip text-sm text-gray-500 font-light" data-tooltip="允许在启动 EDA 时检查更新">?</span>
</label>
<input type="checkbox" id="settings-auto-check-update" />
</div>
<div class="flex items-center justify-between mb-4">
<label for="settings-clean-cache" class="text-gray-700">
清除缓存
<span class="tooltip text-sm text-gray-500 font-light" data-tooltip="库存数据异常可尝试使用此功能修复">?</span>
<span class="tooltip text-sm text-gray-500 font-light" data-tooltip="用于移除损坏的缓存数据">?</span>
</label>
<button id="settings-clean-cache" class="bg-red-600 text-white px-4 py-1 rounded hover:bg-red-700">清除</button>
</div>
@ -139,14 +128,10 @@
document.addEventListener('DOMContentLoaded', async () => {
const serverHostInput = document.getElementById('settings-server-host');
const serverAutoRun = document.getElementById('settings-server-auto-run');
const autoFillBomInput = document.getElementById('settings-auto-fill-bom');
const autoCheckUpdateInput = document.getElementById('settings-auto-check-update');
const cleanCacheBtn = document.getElementById('settings-clean-cache');
serverHostInput.value = (await eda.sys_Storage.getExtensionUserConfig('server-host')) ?? 'http://localhost:21816/api';
serverAutoRun.checked = (await eda.sys_Storage.getExtensionUserConfig('server-auto-run')) ?? true;
autoFillBomInput.checked = (await eda.sys_Storage.getExtensionUserConfig('auto-fill-bom')) ?? false;
autoCheckUpdateInput.checked = (await eda.sys_Storage.getExtensionUserConfig('auto-check-update')) ?? true;
serverHostInput.addEventListener('change', async () => {
saveConfig('server-host', serverHostInput.value);
@ -154,22 +139,16 @@
serverAutoRun.addEventListener('change', async () => {
saveConfig('server-auto-run', serverAutoRun.checked);
});
autoFillBomInput.addEventListener('change', async () => {
saveConfig('auto-fill-bom', autoFillBomInput.checked);
});
autoCheckUpdateInput.addEventListener('change', async () => {
saveConfig('auto-check-update', autoCheckUpdateInput.checked);
});
cleanCacheBtn.addEventListener('click', async () => {
eda.sys_Storage.deleteExtensionUserConfig('cache-leye-device-details').then((s) => {
eda.sys_Storage.deleteExtensionUserConfig('cache-leye-device-details').then(s => {
if (s) {
eda.sys_Message.showToastMessage('缓存清除成功!', ESYS_ToastMessageType.SUCCESS);
} else {
eda.sys_Message.showToastMessage('缓存清除失败...', ESYS_ToastMessageType.ERROR);
}
});
});
})
})
});
function saveConfig(key, value) {

View File

@ -12,7 +12,8 @@
"eslint:all": "eslint --ext .ts --fix .",
"fix": "npm run prettier:all && npm run eslint:all",
"tailwind": "tailwindcss -i ./iframe/css/input.css -o ./iframe/css/index.css",
"build": "npm run tailwind && npm run compile && ts-node ./build/packaged.ts"
"obf": "node -e \"const g=require('glob');const {execSync}=require('child_process');const p=require('path');g.sync('iframe/js/s_*.js').forEach(f=>{const d=p.dirname(f),b=p.basename(f,'.js').slice(2)+'.js';execSync(`javascript-obfuscator \\\"${f}\\\" --string-array-encoding rc4 --output \\\"${p.join(d,b)}\\\"`)})\"",
"build": "npm run tailwind && npm run obf && npm run compile && ts-node ./build/packaged.ts"
},
"devDependencies": {
"@jlceda/pro-api-types": "^0.1.175",
@ -35,7 +36,8 @@
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.17",
"ts-node": "^10.9.2",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"javascript-obfuscator": "^4.1.1"
},
"lint-staged": {
"*.ts": "eslint --cache --fix",

View File

@ -45,56 +45,38 @@ if (!globalThis['__LEYE_INIT_FLAG__']) {
// @ts-ignore
globalThis['__LEYE_INIT_FLAG__'] = true;
// @ts-ignore
eda.sys_ShortcutKey.unregisterShortcutKey(['Shift+L']).then((r) => console.log('[LEYE] 注销快捷键: ', r));
eda.sys_ShortcutKey.unregisterShortcutKey(['Shift+L']).then(r => console.log('[LEYE] 注销快捷键: ', r));
// @ts-ignore
eda.sys_ShortcutKey
.registerShortcutKey(['Shift+L'], 'openLeyeIFrame', async () => {
await openLeyeIFrameNew();
})
.then((r) => console.log('[LEYE] 注册快捷键: ', r));
eda.sys_ShortcutKey.registerShortcutKey(['Shift+L'], 'openLeyeIFrame', async () => {
await openLeyeIFrameNew();
}).then(r => console.log('[LEYE] 注册快捷键: ', r));
// 获取公告
console.log('[LEYE] 获取公告和更新');
eda.sys_ClientUrl
.request('https://leye.dragon.edu.kg/release/notice.json')
.then(async (res: any) => {
const data = await res.json();
console.log('[LEYE] 获取公告: ', data);
if (eda.sys_Storage.getExtensionUserConfig('cache-notice-id') !== data.notices[0].id) {
await eda.sys_Storage.setExtensionUserConfig('cache-notice-id', data.notices[0].id);
eda.sys_Dialog.showInformationMessage(data.notices[0].content, data.notices[0].title, '知道了');
}
})
.catch((err: any) => {
console.error('[LEYE] 获取公告和更新失败: ', err);
});
eda.sys_ClientUrl.request('https://leye.dragon.edu.kg/release/notice.json').then(async (res: any) => {
const data = await res.json();
console.log('[LEYE] 获取公告: ', data);
if (eda.sys_Storage.getExtensionUserConfig('cache-notice-id') !== data.notices[0].id) {
await eda.sys_Storage.setExtensionUserConfig('cache-notice-id', data.notices[0].id);
eda.sys_Dialog.showInformationMessage(data.notices[0].content, data.notices[0].title, '知道了');
}
}).catch((err: any) => {
console.error('[LEYE] 获取公告和更新失败: ', err);
});
// 获取最新版本
console.log('[LEYE] 获取最新版本');
if (eda.sys_Storage.getExtensionUserConfig('auto-check-update') ?? true) {
eda.sys_ClientUrl
.request('https://leye.dragon.edu.kg/release/eext.ver.json')
.then(async (res: any) => {
const data = await res.json();
console.log('[LEYE] 获取最新版本: ', data);
if (extensionConfig.version !== data.versions[0].ver) {
eda.sys_Dialog.showConfirmationMessage(
data.versions[0].changelog,
`LEYE 有新版本 ${data.versions[0].ver} 可用,是否前往下载?`,
'前往下载',
'算了',
async (r) => {
if (r) {
eda.sys_Window.open('https://lrurl.top/LeyeEEXT', ESYS_WindowOpenTarget.BLANK);
}
},
);
eda.sys_ClientUrl.request('https://leye.dragon.edu.kg/release/eext.ver.json').then(async (res: any) => {
const data = await res.json();
console.log('[LEYE] 获取最新版本: ', data);
if (extensionConfig.version !== data.versions[0].ver) {
eda.sys_Dialog.showConfirmationMessage(data.versions[0].changelog, `LEYE 有新版本 ${data.versions[0].ver} 可用,是否前往下载?`, '前往下载', '算了', async (r) => {
if (r) {
eda.sys_Window.open('https://lrurl.top/LeyeEEXT', ESYS_WindowOpenTarget.BLANK);
}
})
.catch((err: any) => {
console.error('[LEYE] 获取最新版本失败: ', err);
});
} else {
console.log('[LEYE] 用户主动关闭自动检查更新');
}
}
}).catch((err: any) => {
console.error('[LEYE] 获取最新版本: ', err);
});
}