initial commit

This commit is contained in:
Fang_Zhijian 2026-02-09 11:40:12 +08:00
commit 1f98910722
63 changed files with 4972 additions and 0 deletions

21
.edaignore Normal file
View File

@ -0,0 +1,21 @@
/.git/
/.husky/
/.vscode/
/build/
/config/
/coverage/
/node_modules/
/src/
/.editorconfig
/.eslintcache
/.eslintrc.js
/.gitattributes
/.gitignore
/.npmrc
/.prettierignore
/.prettierrc.js
/package-lock.json
/package.json
/tsconfig.json
debug.log
/iframe/js/s_*

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
[*.{html,js,json,ts,css}]
indent_style = tab
indent_size = 4
[*.txt]
insert_final_newline = false
[*.{md,yml,yaml}]
indent_style = space
indent_size = 2

14
.eslintrc.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
plugins: ['eslint-plugin-tsdoc'],
extends: ['alloy', 'alloy/typescript'],
ignorePatterns: ['/build/dist/', '/coverage/', '/dist/', '/node_modules/', '/.eslintcache', 'debug.log'],
env: {
browser: true,
},
rules: {
'no-param-reassign': 'off',
'max-params': 'off',
'tsdoc/syntax': 'warn',
},
root: true,
};

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.json linguist-language=JSON-with-Comments

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/.vscode/*
!/.vscode/extensions.json
!/.vscode/settings.json
/coverage/
/dist/
/node_modules/
/.eslintcache
debug.log

1
.husky/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_

3
.husky/pre-commit Normal file
View File

@ -0,0 +1,3 @@
#!/bin/sh
npm install
npx lint-staged

5
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

10
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,10 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="JavaScript">
<option name="BLOCK_COMMENT_ADD_SPACE" value="true" />
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="BLOCK_COMMENT_ADD_SPACE" value="true" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/copilot.data.migration.agent.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.ask.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

6
.idea/copilot.data.migration.edit.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

13
.idea/eext-hyper-export.iml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="alpinejs" level="application" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{alpinejs}" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/eext-hyper-export.iml" filepath="$PROJECT_DIR$/.idea/eext-hyper-export.iml" />
</modules>
</component>
</project>

6
.idea/prettier.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
# 位于中国大陆网络环境的用户,可以取消下行的注释,以获得更快的 NPM 安装速度
# registry=https://registry.npmmirror.com

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
/build/dist/
/coverage/
/dist/
/node_modules/
/.eslintcache
debug.log

28
.prettierrc.js Normal file
View File

@ -0,0 +1,28 @@
/** @type {import("prettier").Options} */
module.exports = {
printWidth: 150,
tabWidth: 4,
useTabs: true,
semi: true,
singleQuote: true,
quoteProps: 'preserve',
trailingComma: 'all',
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'always',
rangeStart: 0,
rangeEnd: Infinity,
requirePragma: false,
insertPragma: false,
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'css',
endOfLine: 'lf',
embeddedLanguageFormatting: 'auto',
singleAttributePerLine: false,
plugins: ['@trivago/prettier-plugin-sort-imports'],
importOrder: ['<THIRD_PARTY_MODULES>', '^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
importOrderGroupNamespaceSpecifiers: false,
importOrderCaseInsensitive: false,
};

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["editorconfig.editorconfig", "streetsidesoftware.code-spell-checker", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

47
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,47 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.validate": ["javascript", "typescript"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true,
"files.associations": {
"*.json": "jsonc"
},
"cSpell.words": [
"easyeda",
"edaignore",
"EDMT",
"eext",
"ELIB",
"EPCB",
"EPNL",
"ESCH",
"eslintcache",
"ESYS",
"Gitee",
"IDMT",
"iife",
"ILIB",
"IPCB",
"IPNL",
"ISCH",
"ISYS",
"jlceda",
"lceda",
"nodebuffer",
"npmrc",
"OSHW",
"outdir",
"SZJLC",
"TDMT",
"TLIB",
"TPCB",
"TPNL",
"trivago",
"TSCH",
"tsdoc",
"TSYS"
]
}

21
CHANGELOG.md Normal file
View File

@ -0,0 +1,21 @@
# 1.0.3
1. 由于可能存在的安全原因,移除了使用 Cookie 获取立创商城订单的功能,相关功能已被废弃
2. 修改 README 文档
# 1.0.2
1. 新增快捷键 Shift+L 打开查看库存界面
2. 新增公告功能
3. 修正 README 文档中的错误
# 1.0.1
1. 添加清除缓存数据功能
2. 入库、出库界面增加拉起服务端功能
3. 更新 README 文档
4. 修复已知问题
# 1.0.0
初始版本

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2024] JLCEDA <support[#]lceda.cn>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

66
README.md Normal file
View File

@ -0,0 +1,66 @@
# LEYE
LEYE 电子元器件库存管理系统 EDA 联动扩展
> **Notice:** 此版本为测试版,可能存在 BUG若需帮助请与开发者联系
## 简介
本扩展在嘉立创 EDA 专业版内提供了 LEYE 电子元器件库存管理系统,此版本为**免费体验版**,兼容常规版 API。
本扩展允许用户通过立创商城 C 编号、立创商城订单导入元器件,支持从库存内查询、放置器件,支持通过 BOM 批量出库。
使用本扩展需要在本地安装 [LEYE Service 服务端](https://lrurl.top/LeyeService)(目前仅提供 Windows 版),数据均储存在本地,不会上传。
## [介绍视频](https://www.bilibili.com/video/BV1nvcFzpEuP/)
https://www.bilibili.com/video/BV1nvcFzpEuP/
## 如何使用
安装本扩展后**需要给予扩展外部交互**权限以与服务端交互:
- V2.2:设置-扩展-扩展管理器-LEYE-允许外部交互;
- V3高级-扩展管理器-已安装-LEYE-配置-允许外部交互。
V2.2 用户菜单栏将直接出现“LEYE”选项V3 用户若未开启“显示在顶部菜单”,则可在“高级”菜单中找到 LEYE。
若未部署常规版,使用本扩展需要在本地安装 LEYE Service 服务端,[点击此处前往下载](https://lrurl.top/LeyeService)。
### 查看库存
查询界面支持直接在原理图放置库存器件同时可以对库存元件进行管理编辑型号、CID出入库
若未启动 LEYE Service扩展将尝试拉起使用期间不要关闭服务端。
![img01](/images/img_01.png)
![img02](/images/img_02.png)
## 器件入库
扩展支持从立创商城单 Excel 文档以及 CID 导入器件。
![img03](/images/img_03.png)
### 批量出库
打开本页面后将自动整理 BOM 与 LEYE 库存相比较,非立创商城器件(无 CID显示为红底不在 LEYE 库存内的器件显示为黄底,在 LEYE 库存内的器件显示为绿底。
建议器件标准化后再使用批量出库功能。
![img06](/images/img_06.png)
## 配置项说明
| 配置项 | 说明 | 默认值 |
|:-------------|:-----------------------------------------------------------|:-------------------------|
| 服务器地址 | LEYE Service 服务器地址 | `http://localhost:21816` |
| 允许拉起服务端 | 允许通过 [`leye://open`](leye://open) 拉起本地安装的 LEYE Service 服务端 | `true` |
| 立创商城 Cookies | 填写后可利用 Cookies 直接从立创商城获取订单数据 | *空* |
## 已知问题
1. 小尺寸屏幕上 UI 无法自适应,体验不佳
## 开源许可
本扩展使用 [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) 开源许可协议,商业/教育用途请考虑联系开发者获取常规版。

25
backend/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "leye-service",
"version": "1.0.0",
"type": "module",
"bin": "index.js",
"pkg": {
"assets": [
"favicon.ico",
"node_modules/sqlite3/**/*"
],
"targets": [
"node18-win-x64"
],
"outputPath": "dist",
"options": [
"no-warnings"
]
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"sqlite3": "^5.1.6",
"sqlite": "^5.0.1"
}
}

219
backend/server.js Normal file
View File

@ -0,0 +1,219 @@
import { Database } from "bun:sqlite";
import express from "express";
import cors from "cors";
import * as path from "node:path";
const app = express();
const PORT = 21816;
const EXE_DIR = path.dirname(process.execPath);
const DB_NAME = "leye.db";
const DB_PATH = path.join(EXE_DIR, DB_NAME)
const db = new Database(DB_PATH, { create: true });
db.run(`
CREATE TABLE IF NOT EXISTS leye_list_free (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
type INTEGER,
value TEXT,
footprint INTEGER,
brand INTEGER,
info TEXT,
quantity INTEGER,
description TEXT,
doc TEXT,
lcscId TEXT
)
`);
app.use(cors());
app.use(express.json());
app.get("/api/getVersion", (req, res) => {
res.json({ version: "1.0.0" });
});
app.get("/api/getLeyeList", (req, res) => {
const params = req.query;
let sql = "SELECT * FROM leye_list_free WHERE 1=1";
let args = [];
if (params.id) { sql += " AND id = ?"; args.push(params.id); }
if (params.name) { sql += " AND name LIKE ?"; args.push(`%${params.name}%`); }
if (params.type) { sql += " AND type = ?"; args.push(params.type); }
if (params.value) { sql += " AND value LIKE ?"; args.push(`%${params.value}%`); }
if (params.footprint) { sql += " AND footprint = ?"; args.push(params.footprint); }
if (params.brand) { sql += " AND brand = ?"; args.push(params.brand); }
if (params.quantity) { sql += " AND quantity >= ?"; args.push(params.quantity); }
if (params.lcscId) { sql += " AND lcscId = ?"; args.push(params.lcscId); }
try {
const rows = db.query(sql).all(...args);
res.json({ data: rows, total: rows.length, success: true });
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
app.get("/api/getLcscOrder", async (req, res) => {
const uuid = req.headers['x-uuid'] || req.headers['X-Uuid'];
const cookies = req.headers['x-cookies'] || req.headers['X-Cookies'];
let warehouse;
if (!uuid || !cookies) {
return res.status(400).json({
success: false,
error: "Missing required headers"
});
}
try {
const response = await fetch(`https://order-api.szlcsc.com/member/order/details/latest/process?orderUuid=${uuid}`, {
headers: {
"accept": "application/json",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
"pragma": "no-cache",
"priority": "u=1, i",
"sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Microsoft Edge\";v=\"144\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"cookie": cookies,
"Referer": "https://member.szlcsc.com/ "
},
method: "GET"
});
if (!response.ok) {
return res.status(response.status).json({
success: false,
error: `LCSC API returned ${response.status}`
});
}
const data = await response.json();
warehouse = data?.result?.warehouseCode === 'sz' ? 'zh' : 'js';
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
return;
}
try {
const response = await fetch(`https://order-api.szlcsc.com/member/print/deliverynote/${warehouse === 'zh' ? 'z' : 'j'}?uuid=${encodeURIComponent(uuid)}&deliveryType=${warehouse}`, {
headers: {
"accept": "application/json",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cache-control": "no-cache",
"pragma": "no-cache",
"priority": "u=1, i",
"sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Microsoft Edge\";v=\"144\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"cookie": cookies,
"Referer": "https://member.szlcsc.com/ "
},
method: "GET"
});
if (!response.ok) {
return res.status(response.status).json({
success: false,
error: `LCSC API returned ${response.status}`
});
}
const data = await response.json();
res.json({ success: true, data });
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
app.post("/api/addLeyeList", (req, res) => {
const { name, lcscId, quantity } = req.body;
if (!lcscId || !quantity) return res.status(400).send("Missing params");
try {
const existing = db.query("SELECT id, quantity FROM leye_list_free WHERE lcscId = ?").get(lcscId);
if (existing) {
const newQuantity = existing.quantity + quantity;
db.run("UPDATE leye_list_free SET quantity = ? WHERE id = ?", newQuantity, existing.id);
res.json({ success: true, action: "updated", quantity: newQuantity });
} else {
db.run("INSERT INTO leye_list_free (name, lcscId, quantity) VALUES (?, ?, ?)", name, lcscId, quantity);
res.json({ success: true, action: "inserted" });
}
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
app.post("/api/editLeyeList", (req, res) => {
const { id, name, lcscId, quantity } = req.body;
if (!id) return res.status(400).send("Missing params");
try {
const existing = db.query("SELECT id, name, lcscId, quantity FROM leye_list_free WHERE id = ?").get(id);
if (!existing) {
return res.status(404).json({ success: false, error: "Record not found" });
}
const hasChanges =
(name !== undefined && name !== existing.name) ||
(lcscId !== undefined && lcscId !== existing.lcscId) ||
(quantity !== undefined && quantity !== existing.quantity);
if (hasChanges) {
const updates = [];
const params = [];
if (name !== undefined && name !== existing.name) {
updates.push("name = ?");
params.push(name);
}
if (lcscId !== undefined && lcscId !== existing.lcscId) {
updates.push("lcscId = ?");
params.push(lcscId);
}
if (quantity !== undefined && quantity !== existing.quantity) {
updates.push("quantity = ?");
params.push(quantity);
}
if (updates.length > 0) {
params.push(id);
const sql = `UPDATE leye_list_free SET ${updates.join(", ")} WHERE id = ?`;
db.query(sql).run(...params);
}
}
res.json({
success: true,
updated: hasChanges,
message: hasChanges ? "Updated successfully" : "No changes needed"
});
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
app.listen(PORT, "0.0.0.0", () => {
console.log(`LEYE 免费扩展版服务端已启动`);
console.log(`按 Ctrl+C 终止`);
});

2
build/dist/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.zip
*.eext

3
build/dist/test.txt vendored Normal file
View File

@ -0,0 +1,3 @@
C9378
C92595
C9375

Binary file not shown.

90
build/packaged.ts Normal file
View File

@ -0,0 +1,90 @@
import fs from 'fs-extra';
import ignore from 'ignore';
import JSZip from 'jszip';
import * as extensionConfig from '../extension.json';
/**
*
*
* @param str -
* @returns
*/
function multiLineStrToArray(str: string): Array<string> {
return str.split(/[\r\n]+/);
}
/**
* UUID
*
* @param uuid - UUID
* @returns
*/
function testUuid(uuid?: string): uuid is string {
const regExp = /^[a-z0-9]{32}$/g;
if (uuid && uuid !== '00000000000000000000000000000000') {
return regExp.test(uuid.trim());
} else {
return false;
}
}
/**
* UUID
*
* @param uuid - UUID
* @returns UUID
*/
function fixUuid(uuid?: string): string {
uuid = uuid?.trim() || undefined;
if (testUuid(uuid)) {
return uuid.trim();
} else {
return crypto.randomUUID().replaceAll('-', '');
}
}
/**
*
*/
function main() {
if (!testUuid(extensionConfig.uuid)) {
const newExtensionConfig = { ...extensionConfig };
// @ts-ignore
delete newExtensionConfig.default;
newExtensionConfig.uuid = fixUuid(extensionConfig.uuid);
fs.writeJsonSync(__dirname + '/../extension.json', newExtensionConfig, { spaces: '\t', EOL: '\n', encoding: 'utf-8' });
}
const filepathListWithoutFilter = fs.readdirSync(__dirname + '/../', { encoding: 'utf-8', recursive: true });
const edaignoreListWithoutResolve = multiLineStrToArray(fs.readFileSync(__dirname + '/../.edaignore', { encoding: 'utf-8' }));
const edaignoreList: Array<string> = [];
for (const edaignoreLine of edaignoreListWithoutResolve) {
if (edaignoreLine.endsWith('/') || edaignoreLine.endsWith('\\')) {
edaignoreList.push(edaignoreLine.slice(0, edaignoreLine.length - 1));
} else {
edaignoreList.push(edaignoreLine);
}
}
const edaignore = ignore().add(edaignoreList);
const filepathListWithoutResolve = edaignore.filter(filepathListWithoutFilter);
const fileList: Array<string> = [];
const folderList: Array<string> = []; // 无用数据
for (const filepath of filepathListWithoutResolve) {
if (fs.lstatSync(filepath).isFile()) {
fileList.push(filepath.replace(/\\/g, '/'));
} else {
folderList.push(filepath.replace(/\\/g, '/'));
}
}
const zip = new JSZip();
for (const file of fileList) {
zip.file(file, fs.createReadStream(__dirname + '/../' + file));
}
zip.generateNodeStream({ type: 'nodebuffer', streamFiles: true }).pipe(
fs.createWriteStream(__dirname + '/dist/' + extensionConfig.name + '_v' + extensionConfig.version + '.eext'),
);
}
main();

21
config/esbuild.common.ts Normal file
View File

@ -0,0 +1,21 @@
import type esbuild from 'esbuild';
export default {
entryPoints: {
'index': './src/index',
},
entryNames: '[name]',
assetNames: '[name]',
bundle: true, // 用于内部方法调用,请勿修改
minify: false, // 用于内部方法调用,请勿修改
loader: {},
outdir: './dist/',
sourcemap: undefined,
platform: 'browser', // 用于内部方法调用,请勿修改
format: 'iife', // 用于内部方法调用,请勿修改
globalName: 'edaEsbuildExportName', // 用于内部方法调用,请勿修改
treeShaking: true,
ignoreAnnotations: true,
define: {},
external: [],
} satisfies Parameters<(typeof esbuild)['build']>[0];

13
config/esbuild.prod.ts Normal file
View File

@ -0,0 +1,13 @@
import esbuild from 'esbuild';
import common from './esbuild.common';
(async () => {
const ctx = await esbuild.context(common);
if (process.argv.includes('--watch')) {
await ctx.watch();
} else {
await ctx.rebuild();
process.exit();
}
})();

69
extension.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "fangs-leye",
"uuid": "944f7c94a8ca485e848f1118effcbb9a",
"displayName": "LEYE",
"description": "LEYE 电子元器件库存管理系统 EDA 联动扩展",
"version": "1.0.3",
"publisher": "Mr_Fang",
"engines": {
"eda": "^3.2.80"
},
"license": "Apache-2.0",
"repository": {
"type": "gitea",
"url": "https://gitea.miri.site/Mr_Fang/eext-leye"
},
"categories": "Schematic",
"keywords": [
"Tools", "库管", "库存管理"
],
"images": {
"logo": "./images/logo.png"
},
"homepage": "https://leye.dragon.edu.kg/",
"bugs": "https://leye.dragon.edu.kg/",
"activationEvents": {},
"entry": "./dist/index",
"dependentExtensions": {},
"headerMenus": {
"sch": [
{
"id": "sch-leye-menu",
"title": "LEYE",
"menuItems": [
{
"id": "sch-leye-open-main",
"title": "查看库存",
"registerFn": "openLeyeIFrameNew"
},
{
"id": "sch-leye-open-import",
"title": "器件入库",
"registerFn": "openImportIFrame"
},
{
"id": "sch-leye-open-export",
"title": "批量出库",
"registerFn": "openExportIFrame"
},
{
"id": "sch-leye-settings-menu",
"title": "设置",
"menuItems": [
{
"id": "sch-leye-open-settings",
"title": "扩展设置",
"registerFn": "openSettings"
},
{
"id": "sch-leye-about",
"title": "关于",
"registerFn": "about"
}
]
}
]
}
]
}
}

289
iframe/about.html Normal file
View File

@ -0,0 +1,289 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>关于软件</title>
<link rel="stylesheet" href="/iframe/css/index.css" />
<style>
#konami-container {
position: fixed;
left: 50%;
top: 40%;
transform: translateX(-50%);
pointer-events: none;
z-index: 9999;
display: flex;
gap: 6px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.konami-bubble {
display: inline-block;
background: rgba(0, 0, 0, 0.78);
color: #fff;
padding: 8px 12px;
border-radius: 999px;
font-size: 18px;
line-height: 1;
opacity: 1;
animation: konamiFade 900ms ease-out forwards;
/* 移除位移动画相关 */
}
@keyframes konamiFade {
0% {
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#page-root.shake-up {
animation: shake-up 360ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
#page-root.shake-down {
animation: shake-down 360ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
#page-root.shake-left {
animation: shake-left 360ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
#page-root.shake-right {
animation: shake-right 360ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
#page-root.shake-scale {
animation: shake-scale 360ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
@keyframes shake-up {
0% {
transform: translateY(0);
}
20% {
transform: translateY(-12px);
}
40% {
transform: translateY(6px);
}
60% {
transform: translateY(-6px);
}
80% {
transform: translateY(3px);
}
100% {
transform: translateY(0);
}
}
@keyframes shake-down {
0% {
transform: translateY(0);
}
20% {
transform: translateY(12px);
}
40% {
transform: translateY(-6px);
}
60% {
transform: translateY(6px);
}
80% {
transform: translateY(-3px);
}
100% {
transform: translateY(0);
}
}
@keyframes shake-left {
0% {
transform: translateX(0);
}
20% {
transform: translateX(-12px);
}
40% {
transform: translateX(6px);
}
60% {
transform: translateX(-6px);
}
80% {
transform: translateX(3px);
}
100% {
transform: translateX(0);
}
}
@keyframes shake-right {
0% {
transform: translateX(0);
}
20% {
transform: translateX(12px);
}
40% {
transform: translateX(-6px);
}
60% {
transform: translateX(6px);
}
80% {
transform: translateX(-3px);
}
100% {
transform: translateX(0);
}
}
@keyframes shake-scale {
0% {
transform: scale(1);
}
20% {
transform: scale(1.14);
}
40% {
transform: scale(0.92);
}
60% {
transform: scale(1.06);
}
80% {
transform: scale(0.98);
}
100% {
transform: scale(1);
}
}
body {
overflow: hidden;
}
</style>
</head>
<body id="page-root" class="bg-gray-100 flex items-center justify-center min-h-screen">
<div class="min-h-screen bg-gray-100 flex items-center justify-center">
<div class="bg-white p-4 rounded-lg w-[400px] h-[200px] flex flex-col justify-between items-center text-center">
<div>
<h1 class="text-2xl font-bold text-gray-800 mb-2" id="name"></h1>
<p class="text-[12px] text-gray-600" id="description"></p>
<p class="text-gray-600 text-sm" id="version"></p>
<p class="text-gray-600 text-sm" id="tip"></p>
</div>
<div class="flex justify-center space-x-4 mt-auto">
<a id="home" class="text-blue-600 hover:text-blue-800 text-sm transition duration-300 ease-in-out">🏠扩展主页</a>
<a id="bugs" class="text-blue-600 hover:text-blue-800 text-sm transition duration-300 ease-in-out">🐞Bug反馈</a>
<a id="check" class="text-blue-600 hover:text-blue-800 text-sm transition duration-300 ease-in-out">✨检查更新</a>
<a id="afdian" class="text-blue-600 hover:text-blue-800 text-sm transition duration-300 ease-in-out">⚡支持作者</a>
</div>
<div class="flex justify-center text-[12px] text-gray-500 mt-2">本扩展使用 Apache-2.0 许可协议开源</div>
</div>
</div>
<div id="konami-container" aria-hidden="true"></div>
<script>
// 页面加载完成
document.addEventListener('DOMContentLoaded', async function () {
const file = await eda.sys_FileSystem.getExtensionFile('/extension.json');
if (file) {
const extensionData = JSON.parse(await file.text());
document.getElementById('name').textContent = extensionData.displayName;
document.getElementById('description').textContent = extensionData.description;
document.getElementById('version').textContent = `V${extensionData.version}`;
document.getElementById('home').setAttribute('href', extensionData.homepage);
document.getElementById('bugs').setAttribute('href', extensionData.bugs);
}
const konami_unlocked = await eda.sys_Storage.getExtensionUserConfig('konami_unlocked');
if (konami_unlocked) {
document.getElementById('tip').innerHTML =
`<a onclick="eda.sys_IFrame.openIFrame('/iframe/ntr.html', 400, 200)" class="text-blue-600 hover:text-blue-800">🐮 隐藏功能 🐮</a>`;
}
});
document.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'a') {
event.preventDefault();
const url = event.target.getAttribute('href');
if (url) {
eda.sys_Window.open(url);
}
}
});
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')
.then((response) => response.json())
.then((data) => {
if (data && data.code === 0) {
const his_list = data.result;
const latestVersion = his_list[0].version;
const currentVersion = document.getElementById('version').textContent.replace('V', '');
// 比较版本号latestVersion 和 currentVersion
const latestParts = latestVersion.split('.').map(Number);
const currentParts = currentVersion.split('.').map(Number);
let isNewVersionAvailable = false;
for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
const latestPart = latestParts[i] || 0;
const currentPart = currentParts[i] || 0;
if (latestPart > currentPart) {
isNewVersionAvailable = true;
break;
} else if (latestPart < currentPart) {
break;
}
}
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>`;
} else {
eda.sys_Message.showToastMessage('👍 当前已是最新版本', ESYS_ToastMessageType.SUCCESS);
}
} else {
eda.sys_Message.showToastMessage(
'🥺 获取版本信息失败',
ESYS_ToastMessageType.ERROR,
undefined,
undefined,
'去瞅一眼',
"eda.sys_Window.open('https://ext.lceda.cn/item/fangs233/fangs-hyper-export')",
);
}
})
.catch((error) => {
console.error('Error fetching version info:', error);
eda.sys_Message.showToastMessage(
'🥺 获取版本信息失败',
ESYS_ToastMessageType.ERROR,
undefined,
undefined,
'去瞅一眼',
"eda.sys_Window.open('https://ext.lceda.cn/item/fangs233/fangs-hyper-export')",
);
}); */
});
document.getElementById('afdian').addEventListener('click', function (event) {
event.preventDefault();
const url = 'https://afdian.com/@Mr_Fang';
eda.sys_Window.open(url);
});
</script>
</body>
</html>

1565
iframe/css/index.css Normal file

File diff suppressed because it is too large Load Diff

3
iframe/css/input.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

344
iframe/export.html Normal file
View File

@ -0,0 +1,344 @@
<!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="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 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>

319
iframe/import.html Normal file
View File

@ -0,0 +1,319 @@
<!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>
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
#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; }
</style>
<script src="/iframe/js/xlsx.full.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">
<header class="flex-shrink-0 bg-white border-b border-gray-200 shadow-sm p-4 space-y-3">
<div class="flex items-center space-x-3">
<span class="font-medium text-gray-700 w-32">立创商城订单导入</span>
<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>
</div>
<div class="flex items-center space-x-3">
<span class="font-medium text-gray-700 w-32">立创商城编号导入</span>
<input id="single-cid" type="text" placeholder="CID" class="w-64 px-3 py-1.5 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none" />
<input id="single-qty" type="number" placeholder="数量" class="w-32 px-3 py-1.5 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none" />
<button id="import-cid-btn" class="px-6 py-1.5 bg-green-600 text-white rounded-md hover:bg-green-700 transition">添加器件</button>
</div>
</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 overflow-hidden m-3 border rounded-lg 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 uppercase text-gray-600">
<tr>
<th class="w-12 px-4 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-24 px-3 py-3 text-center">入库数量</th>
<th class="px-3 py-3 text-left">CID</th>
<th class="w-16 px-3 py-3 text-center">操作</th>
</tr>
</thead>
<tbody id="import-table-body" class="bg-white divide-y divide-gray-200 text-xs">
<tr id="empty-row"><td colspan="9" class="text-center py-20 text-gray-400">列表为空,请从上方导入数据</td></tr>
</tbody>
</table>
</div>
</main>
<div class="flex-shrink-0 p-4 bg-white border-t border-gray-200 flex justify-end space-x-3 shadow-lg">
<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>
<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');
let importList = [];
function renderList() {
if (importList.length === 0) {
tableBody.innerHTML = `<tr id="empty-row"><td colspan="9" class="text-center py-20 text-gray-400">列表为空,请从上方导入数据</td></tr>`;
listCount.textContent = 0;
return;
}
tableBody.innerHTML = importList.map((item, index) => `
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 text-center"><input type="checkbox" class="row-checkbox" data-index="${index}" ${item.selected ? 'checked' : ''}></td>
<td class="px-3 py-2 font-medium">${item.name || '-'}</td>
<td class="px-3 py-2 text-gray-600">${item.childCat || '-'}</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" value="${item.quantity}" onchange="updateQty(${index}, this.value)">
</td>
<td class="px-3 py-2 text-blue-600 font-mono">${item.lcscId}</td>
<td class="px-3 py-2 text-center">
<button onclick="removeItem(${index})" class="text-red-500 hover:text-red-700">删除</button>
</td>
</tr>
`).join('');
listCount.textContent = importList.length;
}
window.updateQty = (index, val) => { importList[index].quantity = parseInt(val) || 0; };
window.removeItem = (index) => { importList.splice(index, 1); renderList(); };
document.getElementById('import-order-btn').onclick = async () => {
eda.sys_Dialog.showInformationMessage('此功能因可能存在的安全问题已被弃用', '提示', '知道了');
};
document.getElementById('import-order-file-btn').onclick = async () => {
try {
const file = await eda.sys_FileSystem.openReadFileDialog(['xls', ['xlsx']], false);
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const newItems = [];
for (let i = 18; i < jsonData.length; i++) {
const row = jsonData[i];
if (!row || row.length < 2) continue; // 跳过空行
// 第2列(1): lcscId, 第3列(2): 品牌, 第4列(3): 型号, 第5列(4): 封装, 第7列(6): 数量
const lcscId = (row[1] || '').toString().trim().toUpperCase();
if (!lcscId) continue;
const rawQty = (row[6] || '0').toString();
const qtyMatch = rawQty.match(/\d+/);
const quantity = qtyMatch ? parseInt(qtyMatch[0]) : 0;
newItems.push({
lcscId: lcscId,
brand: row[2] || '-',
name: row[3] || '-',
footprint: row[4] || '-',
quantity: quantity,
value: '-',
childCat: '-',
selected: true
});
}
if (newItems.length > 0) {
// await enrichData(newItems);
importList.push(...newItems);
renderList();
eda.sys_Message.showToastMessage(`从文件成功导入 ${newItems.length} 个器件`, ESYS_ToastMessageType.SUCCESS);
} else {
eda.sys_Message.showToastMessage('未在找到有效数据', ESYS_ToastMessageType.WARNING);
}
};
reader.readAsArrayBuffer(file);
} catch (err) {
eda.sys_Message.showToastMessage('读取文件失败: ' + err.message, ESYS_ToastMessageType.ERROR);
eda.sys_Log.add('Excel 导入错误: ' + err.stack);
}
};
document.getElementById('import-cid-btn').onclick = async () => {
const cid = document.getElementById('single-cid').value.trim().toUpperCase();
const qty = parseInt(document.getElementById('single-qty').value);
if (!cid || !qty) return eda.sys_Message.showToastMessage('请完整填写编号和数量', ESYS_ToastMessageType.ERROR);
try {
const items = [{ lcscId: cid, quantity: qty, selected: true }];
await enrichData(items);
importList.push(...items);
renderList();
document.getElementById('single-cid').value = '';
document.getElementById('single-qty').value = '';
} catch (e) {
eda.sys_Message.showToastMessage('查询失败', ESYS_ToastMessageType.ERROR);
eda.sys_Log.add(e.message);
}
};
async function enrichData(items) {
const lcscIds = items.map(i => i.lcscId);
const devs = await eda.lib_Device.getByLcscIds(lcscIds);
for (let i = 0; i < items.length; i++) {
const dev = devs[i];
const item = items[i];
if (dev && dev.uuid) {
try {
const infoRes = await eda.sys_ClientUrl.request(
'https://client/api/v2/devices/' + dev.uuid,
'GET',
null,
{ headers: { path: '0819f05c4eef4c71ace90d822a990e87' } }
);
const infoJson = await infoRes.json();
const info = infoJson.result;
if (info && info.attributes) {
item.name = info.attributes['Manufacturer Part'] || item.name;
item.childCat = info.tags?.child_tag?.name_cn || item.childCat;
item.value = info.attributes['Value'] || '-';
item.footprint = info.attributes['Supplier Footprint'] || item.footprint;
item.brand = info.attributes['Manufacturer'] || item.brand;
item.uuid = dev.uuid;
}
} catch (e) {
console.error(`获取器件 ${item.lcscId} 详情失败:`, e);
eda.sys_Log.add(`获取器件 ${item.lcscId} 详情失败: ${e.message}`);
}
}
}
}
document.getElementById('select-all').onclick = () => {
importList.forEach(i => i.selected = true);
renderList();
};
document.getElementById('select-reverse').onclick = () => {
importList.forEach(i => i.selected = !i.selected);
renderList();
};
tableBody.addEventListener('change', (e) => {
if (e.target.classList.contains('row-checkbox')) {
const idx = e.target.dataset.index;
importList[idx].selected = e.target.checked;
}
});
document.getElementById('clear-list-btn').onclick = () => {
importList = [];
renderList();
};
document.getElementById('batch-save-btn').onclick = async () => {
const toSave = importList.filter(i => i.selected && i.lcscId);
if (toSave.length === 0) {
return eda.sys_Message.showToastMessage('请先选择要入库的项', ESYS_ToastMessageType.WARNING);
}
let successCount = 0;
let failCount = 0;
const SERVER = await eda.sys_Storage.getExtensionUserConfig('server-host') || 'http://localhost:21816';
for (const item of toSave) {
try {
const postData = JSON.stringify({
name: item.name || '-',
lcscId: item.lcscId,
quantity: item.quantity
});
let res = await eda.sys_ClientUrl.request(
SERVER + '/addLeyeList',
'POST',
postData,
{ headers: { 'Content-Type': 'application/json' } }
);
let result = await res.json();
if (AUTO_RUN && !result.success) {
window.open('leye://open');
for (let i = 0; i < 3 && !result.success; i++) {
eda.sys_Message.showToastMessage('等待拉起本地服务端...', ESYS_ToastMessageType.INFO);
res = await eda.sys_ClientUrl.request(
SERVER + '/addLeyeList',
'POST',
postData,
{ headers: { 'Content-Type': 'application/json' } }
);
result = await res.json();
await new Promise(resolve => setTimeout(resolve, 1500));
}
}
if (result.success) {
successCount++;
const idx = importList.indexOf(item);
if (idx > -1) importList.splice(idx, 1);
} else {
failCount++;
}
} catch (e) {
console.error(`入库失败: ${item.lcscId}`, e);
eda.sys_Log.add(`入库失败: ${item.lcscId}: ${e.message}`);
failCount++;
}
}
renderList();
if (failCount === 0) {
eda.sys_Message.showToastMessage(`全部入库成功,共 ${successCount} 项`, ESYS_ToastMessageType.SUCCESS);
} else {
eda.sys_Message.showToastMessage(`入库完成,成功: ${successCount}, 失败: ${failCount}`,
failCount > 0 ? ESYS_ToastMessageType.WARNING : ESYS_ToastMessageType.SUCCESS);
}
};
});
</script>
</body>
</html>

24
iframe/js/xlsx.full.min.js vendored Normal file

File diff suppressed because one or more lines are too long

496
iframe/leye.html Normal file
View File

@ -0,0 +1,496 @@
<!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>
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
#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 - 48px - 52px - 64px);
overflow-y: auto;
}
.sticky-header {
position: sticky;
top: 0;
z-index: 10;
}
/* 排序图标样式 */
.sort-icon {
display: inline-block;
margin-left: 4px;
font-size: 10px;
color: #9ca3af;
}
.sort-active {
color: #2563eb !important;
}
</style>
</head>
<body class="bg-gray-100 font-sans text-sm">
<div id="fixed-window" class="bg-gray-50 flex flex-col overflow-hidden">
<header class="flex-shrink-0 bg-white border-b border-gray-200 shadow-sm p-2 text-sm">
<div class="max-w-full mx-auto flex items-center space-x-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
id="global-search-input"
type="text"
placeholder="搜索元器件"
class="w-[92%] px-2 py-1 text-sm border-0 focus:ring-0 focus:outline-none placeholder-gray-400"
/>
<button id="search-btn" class="w-[5%] px-3 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-xs">搜索</button>
</div>
</header>
<div id="main-content" class="flex-grow flex p-3 space-x-3 overflow-hidden">
<aside class="w-56 flex-shrink-0 bg-white border border-gray-200 rounded-lg shadow-md p-3 flex flex-col overflow-hidden">
<h3 class="text-base font-semibold text-gray-800 border-b pb-1 mb-2">筛选类别</h3>
<div id="category-tree" class="flex-grow overflow-y-auto scrollbar-hide space-y-0.5 text-xs">
<div class="cursor-pointer hover:bg-blue-50 rounded p-1" data-value="0">
<input type="radio" name="category" value="0" id="cat-all" class="mr-1 checked:bg-blue-600" />
<label for="cat-all" class="font-bold text-gray-900">全部</label>
</div>
<div class="text-gray-500 p-1" id="category-loading">加载中...</div>
</div>
</aside>
<main class="flex-grow flex flex-col bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden">
<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 uppercase tracking-wider text-gray-600">
<tr>
<th scope="col" class="w-12 px-2 py-2 text-center">ID</th>
<th scope="col" class="w-48 px-3 py-2 text-left">型号</th>
<th scope="col" id="sort-type" class="w-24 px-3 py-2 text-left cursor-pointer hover:bg-gray-200">
类型 <span class="sort-icon"></span>
</th>
<th scope="col" id="sort-value" class="w-20 px-3 py-2 text-left cursor-pointer hover:bg-gray-200">
<span class="sort-icon"></span>
</th>
<th scope="col" class="w-24 px-3 py-2 text-left">封装</th>
<th scope="col" class="w-32 px-3 py-2 text-left">品牌</th>
<th scope="col" id="sort-quantity" class="w-24 px-3 py-2 text-right cursor-pointer hover:bg-gray-200">
余量 <span class="sort-icon"></span>
</th>
<th scope="col" class="w-32 px-3 py-2 text-left">CID</th>
</tr>
</thead>
<tbody id="data-table-body" class="bg-white divide-y divide-gray-200 text-xs">
<tr>
<td colspan="8" class="text-center py-6 text-gray-500" id="table-status">正在加载元器件列表...</td>
</tr>
</tbody>
</table>
</div>
<div class="flex-shrink-0 border-t border-gray-200 bg-gray-50 p-2 flex justify-between items-center text-xs">
<div class="text-gray-600">
<span id="selected-rows-info">未选择行</span>
</div>
<div class="flex items-center space-x-3">
<div class="text-gray-600">总计 <span id="total-items">0</span> 条 | <span id="total-pages">0</span></div>
<div class="flex items-center space-x-1" hidden>
<button
class="px-2 py-0.5 border border-gray-300 rounded-md text-gray-600 hover:bg-gray-200 disabled:opacity-50"
disabled
>
&lt;
</button>
<span class="px-2 py-0.5 bg-blue-600 text-white rounded-md" id="current-page">1</span>
<button class="px-2 py-0.5 border border-gray-300 rounded-md text-gray-600 hover:bg-gray-200" disabled>&gt;</button>
</div>
</div>
</div>
</main>
</div>
<div class="flex-shrink-0 p-3 bg-white border-t border-gray-200 flex justify-end space-x-3 shadow-lg">
<button id="cancel-btn" class="px-4 py-1.5 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-100 text-sm">取消</button>
<button
id="place-btn"
class="px-4 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 text-sm"
>
放置
</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const categoryTree = document.getElementById('category-tree');
const tableBody = document.getElementById('data-table-body');
const placeButton = document.getElementById('place-btn');
const searchButton = document.getElementById('search-btn');
const searchInput = document.getElementById('global-search-input');
const tableStatus = document.getElementById('table-status');
const selectedRowsInfo = document.getElementById('selected-rows-info');
const totalItemsSpan = document.getElementById('total-items');
const totalPagesSpan = document.getElementById('total-pages');
const SERVER = eda.sys_Storage.getExtensionUserConfig('server-host') || 'http://localhost:21816';
let selectedRowData = null;
let mappings = {
footprint: {},
brand: {},
category: {},
};
const unitMap = new Map([
['M', 1e6],
['k', 1e3],
['m', 1e-3],
['u', 1e-6],
['n', 1e-9],
['p', 1e-12],
]);
let sortRules = [];
let currentRawData = [];
function mapIdToName(type, id) {
const key = String(id);
return mappings[type][key] || `ID:${id}`;
}
function parseValue(val) {
if (val === null || val === undefined || val === '') return -Infinity;
const str = String(val).trim();
if (/^\d/.test(str)) {
const numPart = parseFloat(str);
const match = str.match(/[\d.]+\s*([a-zA-Z])/);
if (match && match[1]) {
const unit = match[1];
if (unitMap.has(unit)) {
return numPart * unitMap.get(unit);
}
}
return numPart;
}
return str;
}
async function fetchMappings() {
try {
const [footprintRes, brandRes] = await Promise.all([
eda.sys_ClientUrl.request(SERVER + '/getLeyeFootprint'),
eda.sys_ClientUrl.request(SERVER + '/getLeyeBrand'),
]);
const footprintData = await footprintRes.json();
const brandData = await brandRes.json();
if (footprintData.data) {
footprintData.data.forEach((item) => {
mappings.footprint[String(item.id)] = item.name;
});
}
if (brandData.data) {
brandData.data.forEach((item) => {
mappings.brand[String(item.id)] = item.name;
});
}
} catch (error) {
console.error('获取映射数据失败:', error);
}
}
function renderCategory(item, isChild = false) {
mappings.category[String(item.value)] = item.title;
const div = document.createElement('div');
div.className = `cursor-pointer hover:bg-blue-50 rounded p-1 ${isChild ? 'ml-4' : ''}`;
div.dataset.value = item.value;
div.innerHTML = `
<input type="radio" name="category" value="${item.value}" id="cat-${item.value}" class="mr-1 checked:bg-blue-600">
<label for="cat-${item.value}" class="${isChild ? 'text-gray-600' : 'text-gray-700 font-medium'}">${item.title}</label>
`;
categoryTree.appendChild(div);
if (item.children && item.children.length > 0) {
item.children.forEach((child) => {
mappings.category[String(child.value)] = child.title;
const childDiv = document.createElement('div');
childDiv.className = 'cursor-pointer hover:bg-blue-50 rounded p-1 ml-8';
childDiv.dataset.value = child.value;
childDiv.innerHTML = `
<input type="radio" name="category" value="${child.value}" id="cat-${child.value}" class="mr-1 checked:bg-blue-600">
<label for="cat-${child.value}" class="text-gray-600">${child.title}</label>
`;
categoryTree.appendChild(childDiv);
});
}
}
async function fetchCategories() {
try {
const loadingElement = document.getElementById('category-loading');
if (loadingElement) loadingElement.textContent = '加载中...';
const response = await eda.sys_ClientUrl.request(SERVER + '/getLeyeType');
const result = await response.json();
if (loadingElement) loadingElement.remove();
if (result.success && result.data) {
result.data.forEach((item) => renderCategory(item));
}
} catch (error) {
const loadingElement = document.getElementById('category-loading');
if (loadingElement) loadingElement.textContent = '加载失败';
console.error('获取分类数据失败:', error);
}
}
async function fetchList(type = '', keyword = '') {
selectedRowData = null;
selectedRowsInfo.textContent = '未选择行';
tableBody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-gray-500">正在搜索元器件...</td></tr>`;
let url = SERVER + '/getLeyeList?pageSize=20&current=1';
if (type && type !== '0') url += `&type=${type}`;
if (keyword) url += `&name=${encodeURIComponent(keyword)}`;
try {
const response = await eda.sys_ClientUrl.request(url);
const result = await response.json();
if (result.success && result.data) {
currentRawData = result.data;
applySortAndRender(result.total, result.pageSize, result.current);
} else {
tableBody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-gray-500">未找到数据</td></tr>`;
totalItemsSpan.textContent = 0;
totalPagesSpan.textContent = 0;
}
} catch (error) {
tableBody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-red-500">数据加载失败</td></tr>`;
console.error('获取元器件列表失败:', error);
}
}
function applySortAndRender(total, pageSize, current) {
let displayData = [...currentRawData];
if (sortRules.length > 0) {
displayData.sort((a, b) => {
for (const rule of sortRules) {
let valA, valB;
if (rule.key === 'type') {
valA = mapIdToName('category', a.type);
valB = mapIdToName('category', b.type);
} else if (rule.key === 'value') {
valA = parseValue(a.value);
valB = parseValue(b.value);
if (typeof valA === typeof valB) {
if (typeof valA === 'number') {
if (valA === valB) continue;
return rule.order === 'asc' ? valA - valB : valB - valA;
} else {
const res = valA.localeCompare(valB);
if (res === 0) continue;
return rule.order === 'asc' ? res : -res;
}
}
const mixedRes = (typeof valA === 'number') ? -1 : 1;
return rule.order === 'asc' ? mixedRes : -mixedRes;
} else if (rule.key === 'quantity') {
valA = Number(a.quantity) || 0;
valB = Number(b.quantity) || 0;
}
if (valA < valB) return rule.order === 'asc' ? -1 : 1;
if (valA > valB) return rule.order === 'asc' ? 1 : -1;
}
return 0;
});
}
renderTable(displayData, total, pageSize, current);
updateSortIcons();
}
function renderTable(data, total, pageSize, current) {
let html = '';
if (data.length === 0) {
tableBody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-gray-500">无数据</td></tr>`;
return;
}
data.forEach((item) => {
const footprintName = mapIdToName('footprint', item.footprint);
const brandName = mapIdToName('brand', item.brand);
const typeName = mapIdToName('category', item.type);
const rowData = JSON.stringify(item);
html += `
<tr class="hover:bg-blue-50 cursor-pointer" data-id="${item.id}" data-row='${rowData}'>
<td class="px-2 py-1.5 text-center text-gray-500">${item.id}</td> <td class="px-3 py-1.5 font-medium text-blue-600">${item.name || '-'}</td>
<td class="px-3 py-1.5 text-left text-gray-700">${typeName || item.type}</td>
<td class="px-3 py-1.5 text-left text-gray-700">${item.value || '-'}</td>
<td class="px-3 py-1.5 text-left text-gray-700">${footprintName}</td>
<td class="px-3 py-1.5 text-left text-gray-700">${brandName}</td>
<td class="px-3 py-1.5 text-right ${item.quantity > 0 ? 'text-green-600' : 'text-red-500'}">${item.quantity}</td>
<td class="px-3 py-1.5 text-left text-gray-500">${item.lcscId || '-'}</td>
</tr>
`;
});
tableBody.innerHTML = html;
totalItemsSpan.textContent = total;
totalPagesSpan.textContent = Math.ceil(total / pageSize);
document.getElementById('current-page').textContent = current;
const firstRow = tableBody.querySelector('tr');
if (firstRow) {
firstRow.classList.add('bg-blue-100');
selectedRowData = JSON.parse(firstRow.dataset.row);
selectedRowsInfo.textContent = '已选择 1 行';
}
}
function updateSortIcons() {
['type', 'value', 'quantity'].forEach((key) => {
const th = document.getElementById(`sort-${key}`);
const icon = th.querySelector('.sort-icon');
const ruleIndex = sortRules.findIndex((r) => r.key === key);
const rule = sortRules[ruleIndex];
if (rule) {
const arrow = rule.order === 'asc' ? '↑' : '↓';
const priority = sortRules.length > 1 ? `(${ruleIndex + 1})` : '';
icon.textContent = arrow + priority;
icon.classList.add('sort-active');
th.classList.add('bg-blue-50');
} else {
icon.textContent = '⇅';
icon.classList.remove('sort-active');
th.classList.remove('bg-blue-50');
}
});
}
const handleSortClick = (key) => {
const existingIndex = sortRules.findIndex((r) => r.key === key);
if (existingIndex > -1) {
if (sortRules[existingIndex].order === 'asc') {
sortRules[existingIndex].order = 'desc';
} else {
sortRules.splice(existingIndex, 1); // 第二次点击 desc 后取消该项排序
}
} else {
sortRules.push({ key, order: 'asc' }); // 添加新排序规则
}
applySortAndRender(totalItemsSpan.textContent, 20, document.getElementById('current-page').textContent);
};
document.getElementById('sort-type').onclick = () => handleSortClick('type');
document.getElementById('sort-value').onclick = () => handleSortClick('value');
document.getElementById('sort-quantity').onclick = () => handleSortClick('quantity');
tableBody.addEventListener('click', function (event) {
let row = event.target.closest('tr');
if (!row || !row.dataset.row) return;
tableBody.querySelectorAll('tr').forEach((r) => r.classList.remove('bg-blue-100'));
row.classList.add('bg-blue-100');
selectedRowData = JSON.parse(row.dataset.row);
selectedRowsInfo.textContent = '已选择 1 行';
});
searchButton.addEventListener('click', function () {
const searchValue = searchInput.value.trim();
const selectedCategoryRadio = document.querySelector('input[name="category"]:checked');
const categoryValue = selectedCategoryRadio ? selectedCategoryRadio.value : '0';
fetchList(categoryValue, searchValue);
});
categoryTree.addEventListener('click', function (event) {
const item = event.target.closest('div[data-value]');
if (item) {
const radio = item.querySelector('input[type="radio"]');
if (radio) radio.checked = true;
fetchList(item.dataset.value, searchInput.value.trim());
}
});
placeButton.addEventListener('click', async function () {
if (selectedRowData) {
if (!selectedRowData.lcscId) {
eda.sys_Message.showToastMessage('无立创商城 CID无法放置', ESYS_ToastMessageType.ERROR);
return;
}
const devices = await eda.lib_Device.getByLcscIds([selectedRowData.lcscId]);
await eda.sys_IFrame.hideIFrame('leye-main');
eda.sys_Message.showToastMessage('请在原理图中点击放置位置', ESYS_ToastMessageType.INFO);
try {
await eda.sch_PrimitiveComponent.placeComponentWithMouse({
uuid: devices[0].uuid,
libraryUuid: '0819f05c4eef4c71ace90d822a990e87',
path: '0819f05c4eef4c71ace90d822a990e87',
});
return;
} catch (e) {
eda.sys_Log.add('call placeComponentWithMouse api fail');
console.log(e);
}
if (eda.sch_Event.isEventListenerAlreadyExist('place_device')) {
eda.sch_Event.removeEventListener('place_device');
eda.sys_Log.add('place_device event listener is already exist');
}
eda.sch_Event.addMouseEventListener('place_device', ESCH_MouseEventType.SELECTED, async () => {
const position = await eda.sch_SelectControl.getCurrentMousePosition();
await eda.sch_PrimitiveComponent.create(
{
uuid: devices[0].uuid,
libraryUuid: '0819f05c4eef4c71ace90d822a990e87',
},
position.x,
position.y,
);
await eda.sys_IFrame.showIFrame('leye-main');
eda.sch_Event.removeEventListener('place_device');
}, true);
}
});
document.getElementById('cancel-btn').addEventListener('click', function () {
eda.sys_IFrame.closeIFrame('leye-main');
});
async function initialize() {
await Promise.all([fetchMappings(), fetchCategories()]);
await fetchList();
}
initialize();
});
</script>
</body>
</html>

557
iframe/newLeye.html Normal file
View File

@ -0,0 +1,557 @@
<!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>
html { user-select: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
#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 - 48px - 52px - 64px); overflow-y: auto; }
.sticky-header { position: sticky; top: 0; z-index: 10; }
.sort-icon { display: inline-block; margin-left: 4px; font-size: 10px; color: #9ca3af; }
.sort-active { color: #2563eb !important; }
.hidden { display: none !important; }
.cat-toggle { transition: transform 0.2s; cursor: pointer; padding: 4px; }
.cat-toggle.collapsed { transform: rotate(-90deg); }
.children-container { overflow: hidden; transition: max-height 0.3s ease-out; }
.children-container.hidden { display: none; }
</style>
</head>
<body class="bg-gray-100 font-sans text-sm">
<div id="fixed-window" class="bg-gray-50 flex flex-col overflow-hidden">
<header class="flex-shrink-0 bg-white border-b border-gray-200 shadow-sm p-2 text-sm">
<div class="max-w-full mx-auto flex items-center space-x-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input id="global-search-input" type="text" placeholder="搜索型号、CID、品牌、值..." class="w-[92%] px-2 py-1 text-sm border-0 focus:ring-0 focus:outline-none placeholder-gray-400" />
<button id="search-btn" class="w-[5%] px-3 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-xs">搜索</button>
</div>
</header>
<div id="main-content" class="flex-grow flex p-3 space-x-3 overflow-hidden">
<aside class="w-60 flex-shrink-0 bg-white border border-gray-200 rounded-lg shadow-md p-3 flex flex-col overflow-hidden">
<h3 class="text-base font-semibold text-gray-800 border-b pb-1 mb-2">筛选类别</h3>
<div id="category-tree" class="flex-grow overflow-y-auto scrollbar-hide space-y-0.5 text-xs">
</div>
</aside>
<main class="flex-grow flex flex-col bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden">
<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 uppercase tracking-wider text-gray-600">
<tr>
<th scope="col" class="w-12 px-2 py-2 text-center">ID</th>
<th scope="col" class="w-48 px-3 py-2 text-left">型号</th>
<th scope="col" id="sort-type" class="w-24 px-3 py-2 text-left cursor-pointer hover:bg-gray-200">类型 <span class="sort-icon"></span></th>
<th scope="col" id="sort-value" class="w-20 px-3 py-2 text-left cursor-pointer hover:bg-gray-200"><span class="sort-icon"></span></th>
<th scope="col" class="w-24 px-3 py-2 text-left">封装</th>
<th scope="col" class="w-32 px-3 py-2 text-left">品牌</th>
<th scope="col" id="sort-quantity" class="w-24 px-3 py-2 text-right cursor-pointer hover:bg-gray-200">余量 <span class="sort-icon"></span></th>
<th scope="col" class="w-32 px-3 py-2 text-left">CID</th>
</tr>
</thead>
<tbody id="data-table-body" class="bg-white divide-y divide-gray-200 text-xs">
<tr><td colspan="8" class="text-center py-6 text-gray-500">正在初始化器件数据...</td></tr>
</tbody>
</table>
</div>
<div class="flex-shrink-0 border-t border-gray-200 bg-gray-50 p-2 flex justify-between items-center text-xs">
<div class="text-gray-600"><span id="selected-rows-info">未选择行</span></div>
<div class="flex items-center space-x-4">
<div class="text-gray-600">展示 <span id="display-range">0-0</span> / 共 <span id="total-items">0</span></div>
<div class="flex items-center space-x-1">
<button id="prev-page" class="px-2 py-0.5 border border-gray-300 rounded-md hover:bg-gray-200 disabled:opacity-30">上页</button>
<span class="px-2 font-medium" id="page-num">1</span>
<button id="next-page" class="px-2 py-0.5 border border-gray-300 rounded-md hover:bg-gray-200 disabled:opacity-30">下页</button>
</div>
</div>
</div>
</main>
</div>
<div class="flex-shrink-0 p-3 bg-white border-t border-gray-200 flex justify-end space-x-3 shadow-lg">
<button id="cancel-btn" class="px-4 py-1.5 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-100 text-sm">关闭</button>
<button id="edit-btn" class="px-4 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">出入库</button>
<button id="place-btn" class="px-4 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">放置</button>
</div>
<div id="edit-dialog" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl w-96 overflow-hidden">
<div class="bg-gray-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-sm font-semibold text-gray-700">编辑器件信息</h3>
<span class="text-[10px] text-gray-400">ID: <span id="dialog-id-display">-</span></span>
</div>
<div class="p-4 space-y-4">
<div class="grid grid-cols-2 gap-3">
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">型号 (Manufacturer Part) <span class="text-red-500">*</span></label>
<input id="edit-name-input" type="text" class="w-full px-2 py-1.5 border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 outline-none text-sm" placeholder="必填" />
</div>
<div class="col-span-2">
<label class="block text-xs text-gray-500 mb-1">LCSC ID (Supplier Part) <span class="text-red-500">*</span></label>
<input id="edit-lcsc-input" type="text" class="w-full px-2 py-1.5 border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 outline-none text-sm" placeholder="必填" />
</div>
</div>
<hr class="border-gray-100">
<div>
<label class="block text-xs text-gray-500 mb-1">变更库存</label>
<div class="flex space-x-2 mb-2">
<button id="op-add" class="flex-1 py-1.5 border border-blue-600 bg-blue-50 text-blue-600 rounded text-xs font-medium">入库 (+)</button>
<button id="op-sub" class="flex-1 py-1.5 border border-gray-300 text-gray-600 rounded text-xs font-medium">出库 (-)</button>
</div>
<div class="flex items-center space-x-3">
<input id="edit-qty-input" type="number" min="0" value="0" class="w-24 px-2 py-1.5 border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 outline-none text-sm" />
<div class="text-[10px] text-gray-400">
<span id="current-qty-val">0</span><span id="target-qty-val">0</span>
</div>
</div>
</div>
</div>
<div class="flex border-t border-gray-100">
<button id="dialog-cancel" class="flex-1 px-4 py-3 text-gray-500 hover:bg-gray-50 text-xs">取消</button>
<button id="dialog-confirm" class="flex-1 px-4 py-3 bg-blue-600 text-white hover:bg-blue-700 text-xs font-bold">提交更改</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const categoryTree = document.getElementById('category-tree');
const tableBody = document.getElementById('data-table-body');
const placeButton = document.getElementById('place-btn');
const searchButton = document.getElementById('search-btn');
const searchInput = document.getElementById('global-search-input');
const selectedRowsInfo = document.getElementById('selected-rows-info');
const totalItemsSpan = document.getElementById('total-items');
const pageNumSpan = document.getElementById('page-num');
const displayRangeSpan = document.getElementById('display-range');
const editBtn = document.getElementById('edit-btn');
const editDialog = document.getElementById('edit-dialog');
const editQtyInput = document.getElementById('edit-qty-input');
const currentQtySpan = document.getElementById('current-qty-val');
const targetQtySpan = document.getElementById('target-qty-val');
const opAddBtn = document.getElementById('op-add');
const opSubBtn = document.getElementById('op-sub');
const dialogCancel = document.getElementById('dialog-cancel');
const dialogConfirm = document.getElementById('dialog-confirm');
const editNameInput = document.getElementById('edit-name-input');
const editLcscInput = document.getElementById('edit-lcsc-input');
const dialogIdDisplay = document.getElementById('dialog-id-display');
const SERVER = eda.sys_Storage.getExtensionUserConfig('server-host') || 'http://localhost:21816/api';
const AUTO_RUN = eda.sys_Storage.getExtensionUserConfig('server-auto-run') || true;
const CACHE_KEY = 'cache-leye-device-details';
let allDevicesData = [];
let filteredData = [];
let selectedRowData = null;
let sortRules = [
{ key: 'type', order: 'desc' },
{ key: 'value', order: 'desc' }
];
let currentOp = 'add';
let currentPage = 1;
const pageSize = 20;
const unitMap = new Map([['M', 1e6], ['k', 1e3], ['m', 1e-3], ['u', 1e-6], ['n', 1e-9], ['p', 1e-12]]);
function parseValue(val) {
if (!val || val === '-') return -Infinity;
const str = String(val).trim();
if (/^\d/.test(str)) {
const numPart = parseFloat(str);
const match = str.match(/[\d.]+\s*([a-zA-Z])/);
if (match && match[1] && unitMap.has(match[1])) return numPart * unitMap.get(match[1]);
return numPart;
}
return str;
}
async function initInventoryData() {
tableBody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-gray-500">正在同步库存状态...</td></tr>`;
try {
const cachedRaw = await eda.sys_Storage.getExtensionUserConfig(CACHE_KEY);
let cachedDetails = [];
try { cachedDetails = cachedRaw ? JSON.parse(cachedRaw) : []; } catch(e) { cachedDetails = []; }
let listRes = await eda.sys_ClientUrl.request(SERVER + '/getLeyeList?pageSize=1000&current=1');
let listResult = await listRes.json();
if (AUTO_RUN && !listResult.success) {
window.open('leye://open');
for (let i = 0; i < 3 && !listResult.success; i++) {
eda.sys_Message.showToastMessage('等待拉起本地服务端...', ESYS_ToastMessageType.INFO);
listRes = await eda.sys_ClientUrl.request(SERVER + '/getLeyeList?pageSize=1000&current=1');
listResult = await listRes.json();
await new Promise(resolve => setTimeout(resolve, 1500));
}
}
if (!listResult.success || !listResult.data) {
throw new Error('同步失败');
}
const validItems = listResult.data.filter(item => item.lcscId);
const currentLcscIds = validItems.map(item => String(item.lcscId).toUpperCase());
const cachedLcscIds = cachedDetails.map(d => String(d.lcscId).toUpperCase());
const newLcscIds = currentLcscIds.filter(id => !cachedLcscIds.includes(id));
let updatedDetails = cachedDetails.filter(d => currentLcscIds.includes(String(d.lcscId).toUpperCase()));
if (newLcscIds.length > 0) {
const devices = await eda.lib_Device.getByLcscIds(newLcscIds);
const chunkSize = 20;
for (let i = 0; i < devices.length; i += chunkSize) {
const chunk = devices.slice(i, i + chunkSize);
tableBody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-gray-500">发现 ${newLcscIds.length} 个新器件,正在更新 (${Math.min(i+chunkSize, devices.length)}/${devices.length})...</td></tr>`;
const chunkData = await Promise.all(chunk.map(async (dev) => {
try {
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/' + dev.uuid, 'GET', null, { headers: { path: '0819f05c4eef4c71ace90d822a990e87' } });
const infoJson = await infoRes.json();
const info = infoJson.result;
return {
uuid: dev.uuid,
parentCat: info.tags?.parent_tag?.name_cn || '其他',
childCat: info.tags?.child_tag?.name_cn || '其他',
name: info.attributes['Manufacturer Part'] || '-',
footprint: info.attributes['Supplier Footprint'] || '-',
value: info.attributes['Value'] || '-',
brand: info.attributes['Manufacturer'] || '-',
lcscId: String(info.attributes['Supplier Part']).toUpperCase(),
};
} catch (e) { return null; }
}));
updatedDetails.push(...chunkData.filter(d => d !== null));
}
await eda.sys_Storage.setExtensionUserConfig(CACHE_KEY, JSON.stringify(updatedDetails));
}
allDevicesData = updatedDetails.map(detail => {
const invItem = validItems.find(vi => String(vi.lcscId).toUpperCase() === detail.lcscId);
return { ...detail, id: invItem ? invItem.id : '?', quantity: invItem ? invItem.quantity : 0 };
});
generateCategoryTree();
handleLocalSearch();
} catch (error) {
tableBody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-red-500">加载失败: ${error.message}</td></tr>`;
console.log('加载失败: ', error);
eda.sys_Log.add('加载失败: ', error.message);
}
}
function generateCategoryTree() {
const tree = {};
allDevicesData.forEach(d => {
if (!tree[d.parentCat]) tree[d.parentCat] = new Set();
tree[d.parentCat].add(d.childCat);
});
let html = `
<div class="cursor-pointer hover:bg-blue-50 rounded p-1 flex items-center" data-cat="all">
<div class="w-4"></div>
<input type="radio" name="category" value="all" id="cat-all" checked class="mr-2">
<label for="cat-all" class="font-bold text-gray-900 cursor-pointer">全部</label>
</div>`;
Object.keys(tree).sort().forEach(parent => {
const parentId = btoa(encodeURIComponent(parent)).replace(/=/g, '');
html += `
<div class="parent-group">
<div class="cursor-pointer hover:bg-blue-50 rounded p-1 flex items-center" data-cat="${parent}">
<span class="cat-toggle text-gray-400 hover:text-blue-600 collapsed" data-toggle="${parentId}"></span>
<input type="radio" name="category" value="${parent}" id="cat-${parentId}" class="mr-2">
<label for="cat-${parentId}" class="text-gray-700 font-semibold cursor-pointer truncate">${parent}</label>
</div>
<div id="children-${parentId}" class="children-container ml-4 border-l border-gray-100 pl-2 hidden">`;
Array.from(tree[parent]).sort().forEach(child => {
const childId = btoa(encodeURIComponent(child)).replace(/=/g, '');
html += `
<div class="cursor-pointer hover:bg-blue-50 rounded p-1 flex items-center" data-cat="${child}">
<div class="w-4"></div>
<input type="radio" name="category" value="${child}" id="cat-${childId}" class="mr-2">
<label for="cat-${childId}" class="text-gray-600 cursor-pointer truncate">${child}</label>
</div>`;
});
html += `</div></div>`;
});
categoryTree.innerHTML = html;
categoryTree.querySelectorAll('.cat-toggle').forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
const targetId = btn.dataset.toggle;
const container = document.getElementById(`children-${targetId}`);
btn.classList.toggle('collapsed');
container.classList.toggle('hidden');
};
});
}
function handleLocalSearch() {
currentPage = 1;
const keyword = searchInput.value.toLowerCase().trim();
const selectedRadio = document.querySelector('input[name="category"]:checked');
const catValue = selectedRadio ? selectedRadio.value : 'all';
filteredData = allDevicesData.filter(d => {
const matchesSearch = !keyword ||
(d.name && d.name.toLowerCase().includes(keyword)) ||
(d.lcscId && d.lcscId.toLowerCase().includes(keyword)) ||
(d.brand && d.brand.toLowerCase().includes(keyword)) ||
(d.value && d.value.toLowerCase().includes(keyword));
const matchesCat = catValue === 'all' || d.parentCat === catValue || d.childCat === catValue;
return matchesSearch && matchesCat;
});
applySortAndRender();
}
function applySortAndRender() {
let data = [...filteredData];
if (sortRules.length > 0) {
data.sort((a, b) => {
for (const rule of sortRules) {
let valA, valB;
if (rule.key === 'type') { valA = a.childCat; valB = b.childCat; }
else if (rule.key === 'value') { valA = parseValue(a.value); valB = parseValue(b.value); }
else if (rule.key === 'quantity') { valA = a.quantity; valB = b.quantity; }
if (valA === valB) continue;
if (typeof valA === 'number' && typeof valB === 'number') {
return rule.order === 'asc' ? valA - valB : valB - valA;
}
const res = String(valA).localeCompare(String(valB), 'zh-CN');
return rule.order === 'asc' ? res : -res;
}
return 0;
});
}
renderTable(data);
updateSortIcons();
}
function renderTable(data) {
const total = data.length;
totalItemsSpan.textContent = total;
if (total === 0) {
tableBody.innerHTML = `<tr><td colspan="8" class="text-center py-6 text-gray-500">未找到匹配库存项</td></tr>`;
displayRangeSpan.textContent = '0-0';
return;
}
const start = (currentPage - 1) * pageSize;
const end = Math.min(start + pageSize, total);
const pageData = data.slice(start, end);
displayRangeSpan.textContent = `${start + 1}-${end}`;
pageNumSpan.textContent = currentPage;
document.getElementById('prev-page').disabled = currentPage === 1;
document.getElementById('next-page').disabled = end >= total;
tableBody.innerHTML = pageData.map(item => `
<tr class="hover:bg-blue-50 cursor-pointer ${selectedRowData && selectedRowData.lcscId === item.lcscId ? 'bg-blue-100' : ''}" data-row='${JSON.stringify(item)}'>
<td class="px-2 py-1.5 text-center text-gray-500">${item.id}</td>
<td class="px-3 py-1.5 font-medium text-blue-600">${item.name}</td>
<td class="px-3 py-1.5 text-left text-gray-700">${item.childCat}</td>
<td class="px-3 py-1.5 text-left text-gray-700">${item.value}</td>
<td class="px-3 py-1.5 text-left text-gray-700">${item.footprint}</td>
<td class="px-3 py-1.5 text-left text-gray-700">${item.brand}</td>
<td class="px-3 py-1.5 text-right ${item.quantity > 0 ? 'text-green-600' : 'text-red-500'}">${item.quantity}</td>
<td class="px-3 py-1.5 text-left text-gray-500">${item.lcscId}</td>
</tr>
`).join('');
}
function selectRow(row) {
tableBody.querySelectorAll('tr').forEach(r => r.classList.remove('bg-blue-100'));
row.classList.add('bg-blue-100');
selectedRowData = JSON.parse(row.dataset.row);
selectedRowsInfo.textContent = '已选择 1 行';
}
function updateSortIcons() {
['type', 'value', 'quantity'].forEach(key => {
const th = document.getElementById(`sort-${key}`);
const icon = th.querySelector('.sort-icon');
const ruleIndex = sortRules.findIndex(r => r.key === key);
const rule = sortRules[ruleIndex];
if (rule) {
icon.textContent = (rule.order === 'asc' ? '↑' : '↓') + (sortRules.length > 1 ? `(${ruleIndex + 1})` : '');
icon.classList.add('sort-active');
th.classList.add('bg-blue-50');
} else {
icon.textContent = '⇅';
icon.classList.remove('sort-active');
th.classList.remove('bg-blue-50');
}
});
}
searchButton.onclick = handleLocalSearch;
searchInput.onkeyup = (e) => e.key === 'Enter' && handleLocalSearch();
document.getElementById('prev-page').onclick = () => { if (currentPage > 1) { currentPage--; applySortAndRender(); } };
document.getElementById('next-page').onclick = () => { if (currentPage * pageSize < filteredData.length) { currentPage++; applySortAndRender(); } };
categoryTree.onclick = (e) => {
const div = e.target.closest('div[data-cat]');
if (div && !e.target.classList.contains('cat-toggle')) {
div.querySelector('input').checked = true;
handleLocalSearch();
}
};
const onSortClick = (key) => {
const idx = sortRules.findIndex(r => r.key === key);
if (idx > -1) {
if (sortRules[idx].order === 'asc') sortRules[idx].order = 'desc';
else sortRules.splice(idx, 1);
} else {
sortRules.push({ key, order: 'asc' });
}
applySortAndRender();
};
document.getElementById('sort-type').onclick = () => onSortClick('type');
document.getElementById('sort-value').onclick = () => onSortClick('value');
document.getElementById('sort-quantity').onclick = () => onSortClick('quantity');
tableBody.onclick = (e) => {
const row = e.target.closest('tr');
if (row && row.dataset.row) selectRow(row);
};
tableBody.ondblclick = (e) => {
const row = e.target.closest('tr');
if (row && row.dataset.row) {
selectRow(row);
placeButton.click();
}
};
placeButton.onclick = async () => {
if (!selectedRowData) return;
await eda.sys_IFrame.closeIFrame('leye-main');
try {
await eda.sch_PrimitiveComponent.placeComponentWithMouse({
uuid: selectedRowData.uuid,
libraryUuid: '0819f05c4eef4c71ace90d822a990e87',
});
} catch (e) { console.error(e); }
};
editBtn.onclick = () => {
if (!selectedRowData) {
eda.sys_Message.showToastMessage('请先在列表中选择一个器件', ESYS_ToastMessageType.WARNING);
return;
}
dialogIdDisplay.textContent = selectedRowData.id;
editNameInput.value = selectedRowData.name || '';
editLcscInput.value = selectedRowData.lcscId || '';
editQtyInput.value = 0;
currentQtySpan.textContent = selectedRowData.quantity;
const change = parseInt(editQtyInput.value) || 0;
const current = parseInt(selectedRowData.quantity) || 0;
const target = currentOp === 'add' ? current + change : current - change;
targetQtySpan.textContent = target;
targetQtySpan.className = target < 0 ? 'text-red-500 font-bold' : 'text-blue-600 font-bold';
editDialog.classList.remove('hidden');
};
opAddBtn.onclick = () => {
currentOp = 'add';
opAddBtn.className = 'flex-1 py-1.5 border border-blue-600 bg-blue-50 text-blue-600 rounded text-xs font-medium';
opSubBtn.className = 'flex-1 py-1.5 border border-gray-300 text-gray-600 rounded text-xs font-medium';
const change = parseInt(editQtyInput.value) || 0;
const current = parseInt(selectedRowData.quantity) || 0;
const target = currentOp === 'add' ? current + change : current - change;
targetQtySpan.textContent = target;
targetQtySpan.className = target < 0 ? 'text-red-500 font-bold' : 'text-blue-600 font-bold';
};
opSubBtn.onclick = () => {
currentOp = 'sub';
opSubBtn.className = 'flex-1 py-1.5 border border-red-600 bg-red-50 text-red-600 rounded text-xs font-medium';
opAddBtn.className = 'flex-1 py-1.5 border border-gray-300 text-gray-600 rounded text-xs font-medium';
const change = parseInt(editQtyInput.value) || 0;
const current = parseInt(selectedRowData.quantity) || 0;
const target = currentOp === 'add' ? current + change : current - change;
targetQtySpan.textContent = target;
targetQtySpan.className = target < 0 ? 'text-red-500 font-bold' : 'text-blue-600 font-bold';
};
editQtyInput.oninput = () => {
const change = parseInt(editQtyInput.value) || 0;
const current = parseInt(selectedRowData.quantity) || 0;
const target = currentOp === 'add' ? current + change : current - change;
targetQtySpan.textContent = target;
targetQtySpan.className = target < 0 ? 'text-red-500 font-bold' : 'text-blue-600 font-bold';
};
dialogCancel.onclick = () => editDialog.classList.add('hidden');
dialogConfirm.onclick = async () => {
const newName = editNameInput.value.trim();
const newLcscId = editLcscInput.value.trim();
const changeQty = parseInt(editQtyInput.value) || 0;
if (!newName || !newLcscId) {
eda.sys_Message.showToastMessage('型号和 LCSC ID 不能为空', ESYS_ToastMessageType.ERROR);
return;
}
const newQuantity = currentOp === 'add' ?
selectedRowData.quantity + changeQty :
selectedRowData.quantity - changeQty;
dialogConfirm.disabled = true;
dialogConfirm.textContent = '正在保存...';
try {
const res = await eda.sys_ClientUrl.request(SERVER + '/editLeyeList', 'POST', JSON.stringify({
id: selectedRowData.id,
name: newName,
lcscId: newLcscId,
quantity: newQuantity
}), { headers: { 'Content-Type': 'application/json' } });
const result = await res.json();
if (result.success) {
eda.sys_Message.showToastMessage('更新成功', ESYS_ToastMessageType.SUCCESS);
const deviceIndex = allDevicesData.findIndex(d => d.id === selectedRowData.id);
if (deviceIndex !== -1) {
allDevicesData[deviceIndex].name = newName;
allDevicesData[deviceIndex].lcscId = newLcscId;
allDevicesData[deviceIndex].quantity = newQuantity;
selectedRowData = { ...allDevicesData[deviceIndex] };
}
await eda.sys_Storage.setExtensionUserConfig(CACHE_KEY, JSON.stringify(allDevicesData));
handleLocalSearch();
editDialog.classList.add('hidden');
} else {
throw new Error(result.message || '后端处理失败');
}
} catch (e) {
eda.sys_Message.showToastMessage('提交失败: ' + e.message, ESYS_ToastMessageType.ERROR);
} finally {
dialogConfirm.disabled = false;
dialogConfirm.textContent = '提交更改';
}
};
document.getElementById('cancel-btn').onclick = () => eda.sys_IFrame.closeIFrame('leye-main');
initInventoryData();
});
</script>
</body>
</html>

226
iframe/settings.html Normal file
View File

@ -0,0 +1,226 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>扩展设置</title>
<link rel="stylesheet" href="/iframe/css/index.css" />
<style>
::-webkit-scrollbar {
height: 0.25rem;
width: 0.25rem;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
}
*:focus {
outline: none;
}
input[type='checkbox'] {
appearance: none;
-webkit-appearance: none;
width: 2rem;
height: 1rem;
background-color: #d1d5db; /* Tailwind's gray-300 */
border-radius: 9999px; /* Full rounded */
position: relative;
cursor: pointer;
transition: background-color 0.2s;
}
input[type='checkbox']::before {
content: '';
position: absolute;
top: 0.125rem;
left: 0.125rem;
width: 0.75rem;
height: 0.75rem;
background-color: white;
border-radius: 9999px;
transition: transform 0.2s;
}
input[type='checkbox']:checked {
background-color: #3b82f6;
}
input[type='checkbox']:checked::before {
transform: translateX(1rem);
}
input[type='checkbox']:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
input[type='checkbox']:disabled::before {
background-color: #f3f4f6;
}
/* Tooltip 基础样式 */
.tooltip-box {
position: fixed;
background: #1f2937;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
z-index: 1000;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.tooltip-box.show {
opacity: 1;
}
.tooltip-box::after {
content: '';
position: absolute;
left: var(--arrow-left, 12px);
border: 6px solid transparent;
}
.tooltip-box:not(.below)::after {
top: 100%;
border-top-color: #1f2937;
}
.tooltip-box.below::after {
bottom: 100%;
border-bottom-color: #1f2937;
}
</style>
</head>
<body class="bg-gray-100">
<div class="min-h-screen bg-gray-100">
<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-server-host" class="text-gray-700">服务器地址</label>
<input type="text" id="settings-server-host" class="border border-gray-300 rounded pl-2 py-0.5 w-48" />
</div>
<div class="flex items-center justify-between mb-4">
<label for="settings-server-auto-run" class="text-gray-700">
允许拉起服务端
<span class="tooltip text-sm text-gray-500 font-light" data-tooltip="允许拉起本地 LEYE 服务端">?</span>
</label>
<input type="checkbox" id="settings-server-auto-run" />
</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-clean-cache" class="text-gray-700">
清除缓存
<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>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const serverHostInput = document.getElementById('settings-server-host');
const serverAutoRun = document.getElementById('settings-server-auto-run');
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;
serverHostInput.addEventListener('change', async () => {
saveConfig('server-host', serverHostInput.value);
});
serverAutoRun.addEventListener('change', async () => {
saveConfig('server-auto-run', serverAutoRun.checked);
});
cleanCacheBtn.addEventListener('click', async () => {
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) {
eda.sys_Storage.setExtensionUserConfig(key, value).then(() => {
eda.sys_Message.showToastMessage('设置已保存', ESYS_ToastMessageType.SUCCESS);
});
}
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tooltipContainer = document.createElement('div');
tooltipContainer.className = 'global-tooltip-container';
document.body.appendChild(tooltipContainer);
const tooltipElements = document.querySelectorAll('.tooltip');
tooltipElements.forEach((element) => {
const tooltipText = element.dataset.tooltip;
if (!tooltipText) return;
const tooltip = document.createElement('div');
tooltip.className = 'tooltip-box';
tooltip.textContent = tooltipText;
tooltipContainer.appendChild(tooltip);
const showTooltip = () => {
tooltip.classList.add('show');
positionTooltip(element, tooltip);
};
const hideTooltip = () => {
tooltip.classList.remove('show');
};
element.addEventListener('mouseenter', showTooltip);
element.addEventListener('mouseleave', hideTooltip);
element.addEventListener('focus', showTooltip);
element.addEventListener('blur', hideTooltip);
});
function positionTooltip(target, tooltip) {
const rect = target.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
let top = rect.top - tooltipRect.height - 8;
let left = rect.left - 12;
if (top < 8) {
top = rect.bottom + 8;
tooltip.classList.add('below');
} else {
tooltip.classList.remove('below');
}
if (left < 8) {
left = 8;
} else if (left + tooltipRect.width > window.innerWidth - 8) {
left = window.innerWidth - tooltipRect.width - 8;
}
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
tooltip.style.setProperty('--arrow-left', '12px');
}
window.addEventListener('scroll', () => {
document.querySelectorAll('.tooltip-box.show').forEach((t) => {
t.classList.remove('show');
});
});
});
</script>
</body>
</html>

BIN
images/img_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
images/img_02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
images/img_03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
images/img_04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
images/img_05.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

BIN
images/img_06.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

7
images/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

4
locales/en.json Normal file
View File

@ -0,0 +1,4 @@
{
"About": "About",
"EasyEDA extension SDK v": "EasyEDA extension SDK v${1}"
}

View File

@ -0,0 +1,3 @@
{
"About...": "About..."
}

View File

@ -0,0 +1,3 @@
{
"About...": "关于..."
}

4
locales/zh-Hans.json Normal file
View File

@ -0,0 +1,4 @@
{
"About": "关于",
"EasyEDA extension SDK v": "嘉立创EDA 扩展 SDK v${1}"
}

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "pro-api-sdk",
"version": "1.1.1",
"description": "嘉立创EDA & EasyEDA 专业版扩展 API 开发工具",
"author": "JLCEDA <support@lceda.cn>",
"license": "Apache-2.0",
"homepage": "https://pro.lceda.cn/",
"scripts": {
"compile": "rimraf ./dist/ && ts-node ./config/esbuild.prod.ts",
"prepare": "husky",
"prettier:all": "prettier --write .",
"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",
"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",
"@microsoft/tsdoc": "^0.15.1",
"@tailwindcss/cli": "^4.1.8",
"@trivago/prettier-plugin-sort-imports": "^5.2.1",
"@types/fs-extra": "^11.0.4",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"esbuild": "^0.24.2",
"eslint": "^8.57.0",
"eslint-config-alloy": "^5.1.2",
"eslint-plugin-tsdoc": "^0.4.0",
"fs-extra": "^11.3.0",
"husky": "^9.1.7",
"ignore": "^7.0.3",
"jszip": "^3.10.1",
"lint-staged": "^15.3.0",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.17",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"javascript-obfuscator": "^4.1.1"
},
"lint-staged": {
"*.ts": "eslint --cache --fix",
"*.{js,ts,html,css,json,md}": "prettier --write"
},
"engines": {
"node": ">=20.5.0"
}
}

7
src/global.d.ts vendored Normal file
View File

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

65
src/index.ts Normal file
View File

@ -0,0 +1,65 @@
/**
*
*
*
* `extension.json` `entry`
*
* 使 `export` `headerMenus`
* `headerMenus`
*
*
* https://prodocs.lceda.cn/cn/api/guide/
*/
// 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 openLeyeIFrame(): Promise<void> {
await eda.sys_IFrame.openIFrame('/iframe/leye.html', 1280, 680, 'leye-main', { minimizeButton: true, grayscaleMask: true });
}
export async function openLeyeIFrameNew(): Promise<void> {
await eda.sys_IFrame.openIFrame('/iframe/newLeye.html', 1280, 680, 'leye-main', { minimizeButton: true, grayscaleMask: true });
}
export async function openImportIFrame(): Promise<void> {
await eda.sys_IFrame.openIFrame('/iframe/import.html', 1280, 680, 'leye-import', { minimizeButton: true, grayscaleMask: true });
}
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);
}
// @ts-ignore
if (!globalThis['__LEYE_INIT_FLAG__']) {
console.log('[LEYE] 扩展初始化');
// @ts-ignore
globalThis['__LEYE_INIT_FLAG__'] = true;
// @ts-ignore
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));
// 获取公告
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);
});
}

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./iframe/**/*.{html,css,js}"],
theme: {
extend: {},
},
plugins: [],
}

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"incremental": true,
"target": "ESNext",
"lib": ["ESNext", "DOM"],
"experimentalDecorators": true,
"module": "CommonJS",
"moduleResolution": "Node10",
"resolveJsonModule": true,
"allowJs": true,
"sourceMap": false,
"outDir": "./dist/",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"isolatedModules": true
},
"include": ["./src/", "./node_modules/@jlceda/pro-api-types/"]
}