This commit is contained in:
Fang_Zhijian 2026-02-09 17:57:40 +08:00
parent 45fe4e70b6
commit 887aa55930
10 changed files with 334 additions and 27 deletions

View File

@ -1,3 +1,10 @@
# 1.0.5
1. 新增扫码入库功能
2. 修改 README 文档
3. 新增检查更新功能
4. 修复已知问题
# 1.0.3
1. 由于可能存在的安全原因,移除了使用 Cookie 获取立创商城订单的功能,相关功能已被废弃

View File

@ -37,10 +37,15 @@ V2.2 用户菜单栏将直接出现“LEYE”选项V3 用户若未开启“
## 器件入库
扩展支持从立创商城单 Excel 文档以及 CID 导入器件。
扩展支持从立创商城单 Excel 文档、立创商城物料二维码以及 CID 导入器件。
![img03](/images/img_03.png)
> **Tip: 扫描立创商城二维码导入器件**
>
> 在 V1.0.5 版本以上,可以通过扫描立创商城物料包装上的二维码导入器件,扫描后会自动识别 CID 并导入对应器件。
> ![img07](/images/img_07.png)
### 批量出库
打开本页面后将自动整理 BOM 与 LEYE 库存相比较,非立创商城器件(无 CID显示为红底不在 LEYE 库存内的器件显示为黄底,在 LEYE 库存内的器件显示为绿底。
@ -62,4 +67,9 @@ V2.2 用户菜单栏将直接出现“LEYE”选项V3 用户若未开启“
## 开源许可
本扩展使用以下开源软件:
- [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.3",
"version": "1.0.5",
"publisher": "Mr_Fang",
"engines": {
"eda": "^3.2.80"

View File

@ -221,18 +221,13 @@
document.getElementById('check').addEventListener('click', function (event) {
event.preventDefault();
eda.sys_Message.showToastMessage(
'🥺 暂不支持',
ESYS_ToastMessageType.ERROR
);
/* eda.sys_ClientUrl
.request('https://ext.lceda.cn/api/v1/extensions/his_version_list?bizKey=', 'GET')
eda.sys_ClientUrl
.request('https://leye.dragon.edu.kg/release/eext.ver.json', 'GET')
.then((response) => response.json())
.then((data) => {
if (data && data.code === 0) {
const his_list = data.result;
const latestVersion = his_list[0].version;
if (data) {
const his_list = data.versions;
const latestVersion = his_list[0].ver;
const currentVersion = document.getElementById('version').textContent.replace('V', '');
// 比较版本号latestVersion 和 currentVersion
const latestParts = latestVersion.split('.').map(Number);
@ -251,7 +246,7 @@
if (isNewVersionAvailable) {
eda.sys_Message.showToastMessage(`😋 有新版本可用: V${latestVersion}`, ESYS_ToastMessageType.INFO);
document.getElementById('tip').innerHTML =
`<a href="https://ext.lceda.cn/item/fangs233/fangs-hyper-export" target="_blank" class="text-blue-600 hover:text-blue-800">前往更新新版本</a>`;
`<a href="https://lrurl.top/LeyeEEXT" target="_blank" class="text-blue-600 hover:text-blue-800">前往更新新版本</a>`;
} else {
eda.sys_Message.showToastMessage('👍 当前已是最新版本', ESYS_ToastMessageType.SUCCESS);
}
@ -262,7 +257,7 @@
undefined,
undefined,
'去瞅一眼',
"eda.sys_Window.open('https://ext.lceda.cn/item/fangs233/fangs-hyper-export')",
"eda.sys_Window.open('https://lrurl.top/LeyeEEXT')",
);
}
})
@ -274,9 +269,9 @@
undefined,
undefined,
'去瞅一眼',
"eda.sys_Window.open('https://ext.lceda.cn/item/fangs233/fangs-hyper-export')",
"eda.sys_Window.open('https://lrurl.top/LeyeEEXT')",
);
}); */
});
});
document.getElementById('afdian').addEventListener('click', function (event) {

View File

@ -709,6 +709,10 @@ video {
display: flex;
}
.inline-flex {
display: inline-flex;
}
.table {
display: table;
}
@ -745,6 +749,19 @@ video {
display: none;
}
.size-1 {
width: 0.25rem;
height: 0.25rem;
}
.h-0 {
height: 0px;
}
.h-0\.5 {
height: 0.125rem;
}
.h-1 {
height: 0.25rem;
}
@ -757,10 +774,22 @@ video {
height: 200px;
}
.h-\[250px\] {
height: 250px;
}
.h-full {
height: 100%;
}
.min-h-screen {
min-height: 100vh;
}
.w-1 {
width: 0.25rem;
}
.w-12 {
width: 3rem;
}
@ -813,10 +842,18 @@ video {
width: 24rem;
}
.w-\[250px\] {
width: 250px;
}
.w-\[400px\] {
width: 400px;
}
.w-\[450px\] {
width: 450px;
}
.w-\[5\%\] {
width: 5%;
}
@ -879,6 +916,10 @@ video {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
@ -903,6 +944,10 @@ video {
justify-content: space-between;
}
.gap-2 {
gap: 0.5rem;
}
.gap-3 {
gap: 0.75rem;
}
@ -943,6 +988,12 @@ video {
margin-bottom: calc(0.125rem * var(--tw-space-y-reverse));
}
.space-y-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
}
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
@ -1010,6 +1061,10 @@ video {
border-width: 0px;
}
.border-2 {
border-width: 2px;
}
.border-b {
border-bottom-width: 1px;
}
@ -1022,6 +1077,15 @@ video {
border-top-width: 1px;
}
.border-dashed {
border-style: dashed;
}
.border-blue-100 {
--tw-border-opacity: 1;
border-color: rgb(219 234 254 / var(--tw-border-opacity, 1));
}
.border-blue-600 {
--tw-border-opacity: 1;
border-color: rgb(37 99 235 / var(--tw-border-opacity, 1));
@ -1087,6 +1151,11 @@ video {
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
}
.bg-gray-600 {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1));
}
.bg-green-600 {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
@ -1115,6 +1184,11 @@ video {
--tw-bg-opacity: 0.5;
}
.object-cover {
-o-object-fit: cover;
object-fit: cover;
}
.p-1 {
padding: 0.25rem;
}
@ -1346,6 +1420,11 @@ video {
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
}
.text-orange-600 {
--tw-text-opacity: 1;
color: rgb(234 88 12 / var(--tw-text-opacity, 1));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
@ -1375,12 +1454,22 @@ video {
color: rgb(156 163 175 / var(--tw-placeholder-opacity, 1));
}
.opacity-50 {
opacity: 0.5;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-\[0_0_8px_rgba\(59\2c 130\2c 246\2c 0\.8\)\] {
--tw-shadow: 0 0 8px rgba(59,130,246,0.8);
--tw-shadow-colored: 0 0 8px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-inner {
--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);
@ -1429,6 +1518,11 @@ video {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.invert {
--tw-invert: invert(100%);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
@ -1483,6 +1577,11 @@ video {
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
}
.hover\:bg-gray-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
}
.hover\:bg-green-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity, 1));
@ -1508,6 +1607,11 @@ video {
color: rgb(30 64 175 / var(--tw-text-opacity, 1));
}
.hover\:text-gray-600:hover {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.hover\:text-red-700:hover {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity, 1));

View File

@ -11,8 +11,11 @@
#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>
</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">
@ -22,6 +25,7 @@
<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>
@ -68,15 +72,58 @@
<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;
function renderList() {
if (importList.length === 0) {
@ -105,6 +152,127 @@
listCount.textContent = importList.length;
}
async function updateCameraList() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
cameraSelect.innerHTML = videoDevices.map(d =>
`<option value="${d.deviceId}">${d.label || '摄像头 ' + d.deviceId.slice(0, 5)}</option>`
).join('');
} catch (e) {
eda.sys_Message.showToastMessage('无法获取摄像头列表: ' + e.message, ESYS_ToastMessageType.ERROR);
}
}
window.closeQrDialog = () => {
stopCamera();
qrDialog.classList.add('hidden');
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;
}
qrVideo.parentElement.classList.remove('scanning');
}
document.getElementById('btn-start-camera').onclick = async () => {
stopCamera();
const deviceId = cameraSelect.value;
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-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(); };
@ -114,6 +282,11 @@
};
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);

8
iframe/js/jsQR.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
images/img_07.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

7
src/global.d.ts vendored
View File

@ -1,7 +0,0 @@
export {};
declare global {
interface GlobalThis {
__LEYE_INIT_FLAG__?: boolean;
}
}

View File

@ -10,12 +10,13 @@
*
* https://prodocs.lceda.cn/cn/api/guide/
*/
import * as extensionConfig from '../extension.json';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function activate(status?: 'onStartupFinished', arg?: string): void {}
export function about(): void {
eda.sys_IFrame.openIFrame('/iframe/about.html', 400, 200);
export async function about(): Promise<void> {
await eda.sys_IFrame.openIFrame('/iframe/about.html', 400, 200);
}
export async function openLeyeIFrame(): Promise<void> {
@ -34,8 +35,8 @@ export async function openExportIFrame(): Promise<void> {
await eda.sys_IFrame.openIFrame('/iframe/export.html', 1000, 600, 'leye-export', { minimizeButton: true, grayscaleMask: true });
}
export function openSettings(): void {
eda.sys_IFrame.openIFrame('/iframe/settings.html', 400, 600);
export async function openSettings(): Promise<void> {
await eda.sys_IFrame.openIFrame('/iframe/settings.html', 400, 600);
}
// @ts-ignore
@ -62,4 +63,20 @@ if (!globalThis['__LEYE_INIT_FLAG__']) {
}).catch((err: any) => {
console.error('[LEYE] 获取公告和更新失败: ', err);
});
// 获取最新版本
console.log('[LEYE] 获取最新版本');
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);
});
}