2026-02-09 11:40:12 +08:00
|
|
|
<!doctype html>
|
|
|
|
|
<html lang="zh">
|
2026-02-24 16:15:42 +08:00
|
|
|
<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>
|
2026-02-09 11:40:12 +08:00
|
|
|
</div>
|
2026-02-24 16:15:42 +08:00
|
|
|
</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>
|
2026-02-09 11:40:12 +08:00
|
|
|
</div>
|
2026-02-24 16:15:42 +08:00
|
|
|
<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>
|
2026-02-09 11:40:12 +08:00
|
|
|
</div>
|
2026-02-24 16:15:42 +08:00
|
|
|
</main>
|
|
|
|
|
</div>
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
<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>
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
<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>
|
2026-02-09 11:40:12 +08:00
|
|
|
</div>
|
2026-02-24 16:15:42 +08:00
|
|
|
<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>
|
2026-02-09 11:40:12 +08:00
|
|
|
</div>
|
2026-02-24 16:15:42 +08:00
|
|
|
|
|
|
|
|
<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>
|
2026-02-09 11:40:12 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-24 16:15:42 +08:00
|
|
|
|
|
|
|
|
<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;
|
2026-02-09 11:40:12 +08:00
|
|
|
}
|
2026-02-24 16:15:42 +08:00
|
|
|
return str;
|
2026-02-09 11:40:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
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 {
|
2026-02-25 21:16:55 +08:00
|
|
|
const EDA_HOST =
|
|
|
|
|
eda.sys_Environment.isClient() && !eda.sys_Environment.isOnlineMode()
|
|
|
|
|
? 'https://client'
|
|
|
|
|
: 'https://pro.lceda.cn';
|
2026-02-24 16:15:42 +08:00
|
|
|
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);
|
2026-02-09 11:40:12 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
function generateCategoryTree() {
|
|
|
|
|
const tree = {};
|
|
|
|
|
allDevicesData.forEach((d) => {
|
|
|
|
|
if (!tree[d.parentCat]) tree[d.parentCat] = new Set();
|
|
|
|
|
tree[d.parentCat].add(d.childCat);
|
|
|
|
|
});
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
let html = `
|
2026-02-09 11:40:12 +08:00
|
|
|
<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>`;
|
|
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
Object.keys(tree)
|
|
|
|
|
.sort()
|
|
|
|
|
.forEach((parent) => {
|
|
|
|
|
const parentId = btoa(encodeURIComponent(parent)).replace(/=/g, '');
|
|
|
|
|
html += `
|
2026-02-09 11:40:12 +08:00
|
|
|
<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">`;
|
|
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
Array.from(tree[parent])
|
|
|
|
|
.sort()
|
|
|
|
|
.forEach((child) => {
|
|
|
|
|
const childId = btoa(encodeURIComponent(child)).replace(/=/g, '');
|
|
|
|
|
html += `
|
2026-02-09 11:40:12 +08:00
|
|
|
<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>`;
|
2026-02-24 16:15:42 +08:00
|
|
|
});
|
|
|
|
|
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;
|
|
|
|
|
});
|
2026-02-09 11:40:12 +08:00
|
|
|
}
|
2026-02-24 16:15:42 +08:00
|
|
|
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) => `
|
2026-02-09 11:40:12 +08:00
|
|
|
<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>
|
2026-02-24 16:15:42 +08:00
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.join('');
|
2026-02-09 11:40:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
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();
|
|
|
|
|
};
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
document.getElementById('sort-type').onclick = () => onSortClick('type');
|
|
|
|
|
document.getElementById('sort-value').onclick = () => onSortClick('value');
|
|
|
|
|
document.getElementById('sort-quantity').onclick = () => onSortClick('quantity');
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
tableBody.onclick = (e) => {
|
|
|
|
|
const row = e.target.closest('tr');
|
|
|
|
|
if (row && row.dataset.row) selectRow(row);
|
|
|
|
|
};
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
tableBody.ondblclick = (e) => {
|
|
|
|
|
const row = e.target.closest('tr');
|
|
|
|
|
if (row && row.dataset.row) {
|
|
|
|
|
selectRow(row);
|
|
|
|
|
placeButton.click();
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
editBtn.onclick = () => {
|
|
|
|
|
if (!selectedRowData) {
|
|
|
|
|
eda.sys_Message.showToastMessage('请先在列表中选择一个器件', ESYS_ToastMessageType.WARNING);
|
|
|
|
|
return;
|
2026-02-09 11:40:12 +08:00
|
|
|
}
|
|
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
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');
|
|
|
|
|
};
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
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';
|
|
|
|
|
};
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-02-09 11:40:12 +08:00
|
|
|
|
2026-02-24 16:15:42 +08:00
|
|
|
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>
|
2026-02-09 11:40:12 +08:00
|
|
|
</html>
|