initial commit
21
.edaignore
Normal 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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
*.json linguist-language=JSON-with-Comments
|
||||
8
.gitignore
vendored
Normal 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
@ -0,0 +1 @@
|
||||
_
|
||||
3
.husky/pre-commit
Normal file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
npm install
|
||||
npx lint-staged
|
||||
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
10
.idea/codeStyles/Project.xml
generated
Normal 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
@ -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
@ -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
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AskMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal 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
@ -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
@ -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>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,2 @@
|
||||
# 位于中国大陆网络环境的用户,可以取消下行的注释,以获得更快的 NPM 安装速度
|
||||
# registry=https://registry.npmmirror.com
|
||||
6
.prettierignore
Normal file
@ -0,0 +1,6 @@
|
||||
/build/dist/
|
||||
/coverage/
|
||||
/dist/
|
||||
/node_modules/
|
||||
/.eslintcache
|
||||
debug.log
|
||||
28
.prettierrc.js
Normal 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
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["editorconfig.editorconfig", "streetsidesoftware.code-spell-checker", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
47
.vscode/settings.json
vendored
Normal 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
@ -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
@ -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
@ -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,扩展将尝试拉起,使用期间不要关闭服务端。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 器件入库
|
||||
|
||||
扩展支持从立创商城单 Excel 文档以及 CID 导入器件。
|
||||
|
||||

|
||||
|
||||
### 批量出库
|
||||
|
||||
打开本页面后将自动整理 BOM 与 LEYE 库存相比较,非立创商城器件(无 CID)显示为红底,不在 LEYE 库存内的器件显示为黄底,在 LEYE 库存内的器件显示为绿底。
|
||||
|
||||
建议器件标准化后再使用批量出库功能。
|
||||
|
||||

|
||||
|
||||
## 配置项说明
|
||||
|
||||
| 配置项 | 说明 | 默认值 |
|
||||
|:-------------|:-----------------------------------------------------------|:-------------------------|
|
||||
| 服务器地址 | 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
@ -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
@ -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
@ -0,0 +1,2 @@
|
||||
*.zip
|
||||
*.eext
|
||||
3
build/dist/test.txt
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
C9378
|
||||
C92595
|
||||
C9375
|
||||
BIN
build/dist/立创商城订单详情-SO26012831252.xls
vendored
Normal file
90
build/packaged.ts
Normal 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
@ -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
@ -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
@ -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
@ -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
3
iframe/css/input.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
344
iframe/export.html
Normal 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
@ -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
496
iframe/leye.html
Normal 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
|
||||
>
|
||||
<
|
||||
</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>></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¤t=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
@ -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¤t=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¤t=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
@ -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
|
After Width: | Height: | Size: 179 KiB |
BIN
images/img_02.png
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
images/img_03.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
images/img_04.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
images/img_05.png
Normal file
|
After Width: | Height: | Size: 504 KiB |
BIN
images/img_06.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
images/logo.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
7
images/logo.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
4
locales/en.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"About": "About",
|
||||
"EasyEDA extension SDK v": "EasyEDA extension SDK v${1}"
|
||||
}
|
||||
3
locales/extensionJson/en.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"About...": "About..."
|
||||
}
|
||||
3
locales/extensionJson/zh-Hans.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"About...": "关于..."
|
||||
}
|
||||
4
locales/zh-Hans.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"About": "关于",
|
||||
"EasyEDA extension SDK v": "嘉立创EDA 扩展 SDK v${1}"
|
||||
}
|
||||
49
package.json
Normal 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
@ -0,0 +1,7 @@
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface GlobalThis {
|
||||
__LEYE_INIT_FLAG__?: boolean;
|
||||
}
|
||||
}
|
||||
65
src/index.ts
Normal 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
@ -0,0 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./iframe/**/*.{html,css,js}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
28
tsconfig.json
Normal 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/"]
|
||||
}
|
||||