Compare commits

..

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

14 changed files with 1909 additions and 2567 deletions

View File

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

View File

@ -1,26 +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. 增加远程扫码入库功能
2. 修改 README 文档
3. 修复已知问题
# 1.0.5
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,17 +41,11 @@ V2.2 用户菜单栏将直接出现“LEYE”选项V3 用户若未开启“
![img03](/images/img_03.png)
> **Tip: 扫描立创商城二维码导入器件** _V1.0.5+_
>
> **Tip: 扫描立创商城二维码导入器件**
>
> 在 V1.0.5 版本以上,可以通过扫描立创商城物料包装上的二维码导入器件,扫描后会自动识别 CID 并导入对应器件。
> ![img07](/images/img_07.png)
> **Tip: 使用手机扫码导入器件** _V1.0.6+_
>
> 在 V1.0.6 版本以上可以在扫码界面选择“WebSocket 远程扫码”,点击“打开摄像头”按钮后用手机扫描二维码即可打开远程端。
> 远程端同样支持扫描立创商城物料包装上的二维码导入器件,扫描后会自动识别 CID 并导入对应器件。
> ![img07](/images/img_08.png)
### 批量出库
打开本页面后将自动整理 BOM 与 LEYE 库存相比较,非立创商城器件(无 CID显示为红底不在 LEYE 库存内的器件显示为黄底,在 LEYE 库存内的器件显示为绿底。
@ -64,10 +56,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,11 +68,8 @@ 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/) 许可协议授权
- [MQTT.js](https://www.npmjs.com/package/mqtt)MQTT 客户端库,使用 [MIT License](https://choosealicense.com/licenses/mit/) 许可协议授权
- [Tailwind CSS](https://www.npmjs.com/package/tailwindcss)CSS 框架,使用 [MIT License](https://choosealicense.com/licenses/mit/) 许可协议授权
- [jsQR](https://www.jsdelivr.com/package/npm/jsqr):二维码解析库,使用 [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) 许可协议授权
- [xlsx](https://www.jsdelivr.com/package/npm/xlsx)Excel 解析库,使用 [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) 许可协议授权
- [Tailwind CSS](https://www.jsdelivr.com/package/npm/tailwindcss)CSS 框架,使用 [MIT License](https://choosealicense.com/licenses/mit/) 许可协议授权
本扩展使用 [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) 开源许可协议,商业/教育用途请考虑联系开发者获取常规版。

View File

@ -3,7 +3,7 @@
"uuid": "944f7c94a8ca485e848f1118effcbb9a",
"displayName": "LEYE",
"description": "LEYE 电子元器件库存管理系统 EDA 联动扩展",
"version": "1.0.9",
"version": "1.0.5",
"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

19
iframe/js/mqtt.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 KiB

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);
});
}