497 lines
19 KiB
HTML
497 lines
19 KiB
HTML
<!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>
|