基本完成接口配置页面

This commit is contained in:
jinchao 2025-05-07 13:46:48 +08:00
parent 28ac4d236f
commit 1e07a82611
13 changed files with 2202 additions and 491 deletions

View File

@ -1,57 +0,0 @@
module XNSim
{
module ATA04
{
struct Aerodynamics_input
{
@optional double l_04_i_aerocomac_alpdot_f8;
@optional double l_04_i_aerocomac_beta_f8;
@optional double l_04_i_aerocomac_press_alt_f8;
@optional double l_04_i_aerocomac_tas_f8;
@optional double l_04_i_aerocomac_mach_f8;
@optional double l_04_i_aerocomac_nx_f8;
@optional double l_04_i_aerocomac_ny_f8;
@optional double l_04_i_aerocomac_nz_f8;
@optional double l_04_i_aerocomac_p_f8;
@optional double l_04_i_aerocomac_q_f8;
@optional double l_04_i_aerocomac_r_f8;
@optional double l_04_i_aerocomac_qbar_f8;
@optional double l_04_i_aerocomac_blcg_f8;
@optional double l_04_i_aerocomac_bscg_f8;
@optional double l_04_i_aerocomac_wlcg_f8;
@optional double l_04_i_aerocomac_ail_f8[10];
@optional double l_04_i_aerocomac_elv_f8[4];
@optional double l_04_i_aerocomac_rud_f8[2];
@optional double l_04_i_aerocomac_stab_f8;
@optional double l_04_i_aerocomac_gear_f8[7];
@optional double l_04_i_aerocomac_flap_f8[10];
@optional double l_04_i_aerocomac_slat_f8[20];
@optional double l_04_i_aerocomac_spl_f8[20];
@optional double l_04_i_aerocomac_tnet_f8[4];
@optional double l_04_i_aerocomac_kice_f8[20];
@optional double l_04_i_aerocomac_alt_agl_f8;
};
struct Aerodynamics_output
{
@optional double l_04_o_aerocomac_fxb_f8;
@optional double l_04_o_aerocomac_fyb_f8;
@optional double l_04_o_aerocomac_fzb_f8;
@optional double l_04_o_aerocomac_mxb_f8;
@optional double l_04_o_aerocomac_myb_f8;
@optional double l_04_o_aerocomac_mzb_f8;
@optional double l_04_o_aerocomac_cls_f8;
@optional double l_04_o_aerocomac_cl_f8;
@optional double l_04_o_aerocomac_cd_f8;
@optional double l_04_o_aerocomac_cm_f8;
@optional double l_04_o_aerocomac_cr_f8;
@optional double l_04_o_aerocomac_cy_f8;
@optional double l_04_o_aerocomac_cn_f8;
};
struct Aerodynamics_heartbeat
{
long aero_model_heartbeat;
};
};
};

Binary file not shown.

View File

@ -10,11 +10,17 @@
- Linux: `liblogin.so`(符合 Linux 动态库命名规范) - Linux: `liblogin.so`(符合 Linux 动态库命名规范)
- 支持基于权限级别的用户角色系统 - 支持基于权限级别的用户角色系统
- 使用 Node.js 作为后端服务 - 使用 Node.js 作为后端服务
- 接口配置管理功能:
- 支持添加、编辑、删除接口
- 支持从 Excel 文件导入接口配置
- 支持按构型、ATA 章节、结构体名等条件筛选
- 支持分页显示和批量操作
## 环境要求 ## 环境要求
- Node.js 14.x 或更高版本 - Node.js 14.x 或更高版本
- 环境变量 `XNCore` 指向动态库所在的根目录(系统会在该目录的 lib 子目录中查找动态库) - 环境变量 `XNCore` 指向动态库所在的根目录(系统会在该目录的 lib 子目录中查找动态库)
- MySQL 数据库(用于存储接口配置数据)
## 安装步骤 ## 安装步骤
@ -42,13 +48,24 @@ $env:XNCore = "C:\path\to\xncore\directory"
npm install npm install
``` ```
4. 启动服务器 4. 配置数据库连接
在项目根目录创建 `.env` 文件,添加以下配置:
```env
DB_HOST=localhost
DB_USER=your_username
DB_PASSWORD=your_password
DB_NAME=xnsim_db
```
5. 启动服务器
```bash ```bash
npm start npm start
``` ```
5. 访问 http://localhost:3000 打开登录页面 6. 访问 http://localhost:3000 打开登录页面
## 登录 API ## 登录 API
@ -65,6 +82,44 @@ API 响应包含以下信息:
- `permissionLevel`: 用户权限级别(整数,仅在登录成功时返回) - `permissionLevel`: 用户权限级别(整数,仅在登录成功时返回)
- `role`: 用户角色描述(字符串,仅在登录成功时返回) - `role`: 用户角色描述(字符串,仅在登录成功时返回)
## 接口配置 API
### 接口列表
- 路径: `/api/interface/list`
- 方法: `GET`
- 响应: 返回所有接口配置数据
### 添加接口
- 路径: `/api/interface/add`
- 方法: `POST`
- 请求体: 包含接口配置信息的 JSON 对象
### 更新接口
- 路径: `/api/interface/update`
- 方法: `PUT`
- 请求体: 包含当前数据和原始数据的 JSON 对象
### 删除接口
- 路径: `/api/interface/delete`
- 方法: `DELETE`
- 参数: 通过查询字符串传递接口标识信息
### 导入接口
- 路径: `/api/interface/import`
- 方法: `POST`
- 请求体: 包含导入数据的 JSON 对象
### 获取导入模板
- 路径: `/api/interface/template`
- 方法: `GET`
- 响应: 返回 Excel 模板文件
## 权限级别 ## 权限级别
系统支持以下权限级别: 系统支持以下权限级别:
@ -77,12 +132,23 @@ API 响应包含以下信息:
## 技术栈 ## 技术栈
- 前端: HTML, CSS, JavaScript - 前端: HTML, CSS, JavaScript, Web Components
- 后端: Node.js, Express - 后端: Node.js, Express
- 外部库: ffi-napi (用于调用 C++动态库) - 数据库: MySQL
- 外部库:
- ffi-napi (用于调用 C++动态库)
- xlsx (用于处理 Excel 文件)
## 注意事项 ## 注意事项
- 确保动态库是由 C++编写的,并且包含`Login_validateUser`导出函数(对应 Login 类的 validateUser 静态方法) - 确保动态库是由 C++编写的,并且包含`Login_validateUser`导出函数(对应 Login 类的 validateUser 静态方法)
- `Login_validateUser`函数接受两个字符串参数(用户名和密码),并返回整数表示权限级别(负值表示登录失败) - `Login_validateUser`函数接受两个字符串参数(用户名和密码),并返回整数表示权限级别(负值表示登录失败)
- 系统会根据操作系统类型在`$XNCore/lib`目录下查找相应类型的动态库,并遵循各操作系统的命名规范 - 系统会根据操作系统类型在`$XNCore/lib`目录下查找相应类型的动态库,并遵循各操作系统的命名规范
- 接口配置数据需要遵循以下规则:
- 接口名称必须符合 C++ 命名规范
- 数组大小必须大于 1第二维可以为 0
- 必填字段包括构型、ATA 章节、接口结构体名、接口名称、数据类型
## 更新计划
1. 添加接口配置从 IDL 文件导入的功能

View File

@ -1,24 +1,28 @@
import './interface-config/toolbar.js'; import './interface-config/toolbar.js';
import './interface-config/data-table.js'; import './interface-config/data-table.js';
import './interface-config/variable-form.js'; import './interface-config/variable-form.js';
import './interface-config/import-dialog.js';
class InterfaceConfig extends HTMLElement { class InterfaceConfig extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.data = []; this.data = [];
this.products = []; // 存储构型列表
this.atas = []; // 存储ATA章节列表
} }
connectedCallback() { connectedCallback() {
this.render(); this.render();
this.loadData();
this.addEventListeners(); this.addEventListeners();
this.loadData();
} }
addEventListeners() { addEventListeners() {
const toolbar = this.shadowRoot.querySelector('interface-toolbar'); const toolbar = this.shadowRoot.querySelector('interface-toolbar');
const dataTable = this.shadowRoot.querySelector('interface-data-table'); const dataTable = this.shadowRoot.querySelector('interface-data-table');
const variableForm = this.shadowRoot.querySelector('variable-form'); const variableForm = this.shadowRoot.querySelector('variable-form');
const importDialog = this.shadowRoot.querySelector('import-dialog');
// 添加变量事件 // 添加变量事件
toolbar.addEventListener('add-variable', () => { toolbar.addEventListener('add-variable', () => {
@ -35,16 +39,16 @@ class InterfaceConfig extends HTMLElement {
} }
if (!confirm(`确定要删除选中的 ${selectedRows.length} 个接口吗?`)) { if (!confirm(`确定要删除选中的 ${selectedRows.length} 个接口吗?`)) {
return; return;
} }
try { try {
const deletePromises = selectedRows.map(row => { const deletePromises = selectedRows.map(row => {
const { SystemName, ProductName, ATAName, ModelStructName, InterfaceName } = row; const { SystemName, ProductName, ATAName, ModelStructName, InterfaceName } = row;
return fetch(`/api/interface/delete?systemName=${SystemName}&productName=${ProductName}&ataName=${ATAName}&modelStructName=${ModelStructName}&interfaceName=${InterfaceName}`, { return fetch(`/api/interface/delete?systemName=${SystemName}&productName=${ProductName}&ataName=${ATAName}&modelStructName=${ModelStructName}&interfaceName=${InterfaceName}`, {
method: 'DELETE' method: 'DELETE'
}); });
}); });
const results = await Promise.all(deletePromises); const results = await Promise.all(deletePromises);
const failed = results.some(result => !result.ok); const failed = results.some(result => !result.ok);
@ -85,64 +89,83 @@ class InterfaceConfig extends HTMLElement {
}); });
// 导入数据事件 // 导入数据事件
toolbar.addEventListener('import-data', () => { toolbar.addEventListener('import-data', async (e) => {
const input = document.createElement('input'); const { file, type } = e.detail;
input.type = 'file'; if (!file) return;
input.accept = '.xlsx,.xls';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
formData.append('type', type);
try { try {
const response = await fetch('/api/interface/import', { const response = await fetch('/api/icd/import', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('导入失败'); throw new Error('导入失败');
}
const result = await response.json();
if (result.success) {
await this.loadData();
alert(`导入完成,成功: ${result.results.filter(r => r.success).length}, 失败: ${result.results.filter(r => !r.success).length}`);
} else {
throw new Error(result.error || '导入失败');
}
} catch (error) {
console.error('导入数据时出错:', error);
alert('导入数据失败');
} }
};
input.click(); const result = await response.json();
if (result.success) {
importDialog.show(result.data);
} else {
throw new Error(result.error || '导入失败');
}
} catch (error) {
console.error('导入数据时出错:', error);
alert('导入数据失败: ' + error.message);
}
});
// 确认导入事件
importDialog.addEventListener('confirm-import', async (e) => {
const importData = e.detail;
try {
console.log('准备导入的数据:', importData);
const response = await fetch('/api/interface/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(importData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '保存导入数据失败');
}
const result = await response.json();
if (result.success) {
await this.loadData();
alert('导入成功');
} else {
throw new Error(result.error || '保存导入数据失败');
}
} catch (error) {
console.error('保存导入数据时出错:', error);
alert('保存导入数据失败: ' + error.message);
}
}); });
// 表格编辑按钮事件 // 表格编辑按钮事件
dataTable.addEventListener('edit', (e) => { dataTable.addEventListener('edit', (e) => {
const { SystemName, ProductName, ATAName, ModelStructName, InterfaceName } = e.detail; const variableForm = this.shadowRoot.querySelector('variable-form');
const interfaceData = this.data.find(item => if (variableForm) {
item.SystemName === SystemName && // 确保在设置编辑数据之前先设置选项
item.ProductName === ProductName && variableForm.setOptions(this.products, this.atas);
item.ATAName === ATAName && // 先显示表单,再设置模式和数据
item.ModelStructName === ModelStructName &&
item.InterfaceName === InterfaceName
);
if (interfaceData) {
variableForm.setMode('edit', interfaceData);
variableForm.show(); variableForm.show();
variableForm.setMode('edit', e.detail);
} }
}); });
// 表格删除按钮事件 // 表格删除按钮事件
dataTable.addEventListener('delete', async (e) => { dataTable.addEventListener('delete', async (e) => {
const { SystemName, ProductName, ATAName, ModelStructName, InterfaceName } = e.detail; const { SystemName, ProductName, ATAName, ModelStructName, InterfaceName } = e.detail;
if (!confirm('确定要删除这个接口吗?')) {
return;
}
try { try {
const response = await fetch(`/api/interface/delete?systemName=${SystemName}&productName=${ProductName}&ataName=${ATAName}&modelStructName=${ModelStructName}&interfaceName=${InterfaceName}`, { const response = await fetch(`/api/interface/delete?systemName=${SystemName}&productName=${ProductName}&ataName=${ATAName}&modelStructName=${ModelStructName}&interfaceName=${InterfaceName}`, {
@ -168,25 +191,99 @@ class InterfaceConfig extends HTMLElement {
const url = interfaceData.mode === 'add' ? '/api/interface/add' : '/api/interface/update'; const url = interfaceData.mode === 'add' ? '/api/interface/add' : '/api/interface/update';
const method = interfaceData.mode === 'add' ? 'POST' : 'PUT'; const method = interfaceData.mode === 'add' ? 'POST' : 'PUT';
// 如果是编辑模式,需要添加原始数据用于更新
const requestData = interfaceData.mode === 'edit' ? {
currentData: {
SystemName: 'XNSim',
ProductName: interfaceData.ProductName,
ATAName: interfaceData.ATAName,
ModelStructName: interfaceData.ModelStructName,
InterfaceName: interfaceData.InterfaceName,
InterfaceType: interfaceData.InterfaceType,
InterfaceOption: interfaceData.InterfaceOption || 1,
InterfaceIsArray: interfaceData.InterfaceIsArray,
InterfaceArraySize_1: interfaceData.InterfaceArraySize_1,
InterfaceArraySize_2: interfaceData.InterfaceArraySize_2,
InterfaceNotes: interfaceData.InterfaceNotes
},
originalData: {
SystemName: 'XNSim',
ProductName: interfaceData.originalProductName || interfaceData.ProductName,
ATAName: interfaceData.originalATAName || interfaceData.ATAName,
ModelStructName: interfaceData.originalModelStructName || interfaceData.ModelStructName,
InterfaceName: interfaceData.originalInterfaceName || interfaceData.InterfaceName
}
} : {
SystemName: 'XNSim',
ProductName: interfaceData.ProductName,
ATAName: interfaceData.ATAName,
ModelStructName: interfaceData.ModelStructName,
InterfaceName: interfaceData.InterfaceName,
InterfaceType: interfaceData.InterfaceType,
InterfaceOption: interfaceData.InterfaceOption || 1,
InterfaceIsArray: interfaceData.InterfaceIsArray,
InterfaceArraySize_1: interfaceData.InterfaceArraySize_1,
InterfaceArraySize_2: interfaceData.InterfaceArraySize_2,
InterfaceNotes: interfaceData.InterfaceNotes
};
const response = await fetch(url, { const response = await fetch(url, {
method, method,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(interfaceData) body: JSON.stringify(requestData)
}); });
const responseText = await response.text();
let responseData;
try {
responseData = JSON.parse(responseText);
} catch (e) {
throw new Error('服务器响应格式错误');
}
if (!response.ok) { if (!response.ok) {
throw new Error('保存失败'); throw new Error(responseData.error || '保存失败');
} }
await this.loadData(); await this.loadData();
alert('保存成功'); alert('保存成功');
} catch (error) { } catch (error) {
console.error('保存接口时出错:', error); console.error('保存接口时出错:', error);
alert('保存接口失败'); alert('保存接口失败: ' + error.message);
} }
}); });
// 搜索事件
toolbar.addEventListener('search', (e) => {
const keyword = e.detail.keyword;
dataTable.dispatchEvent(new CustomEvent('search', {
detail: { keyword },
bubbles: true,
composed: true
}));
});
// 重置搜索事件
toolbar.addEventListener('reset-search', async () => {
// 重新加载数据
await this.loadData();
// 触发重置事件
dataTable.dispatchEvent(new CustomEvent('reset-search', {
bubbles: true,
composed: true
}));
});
// 高级搜索事件
toolbar.addEventListener('advanced-search', (e) => {
dataTable.dispatchEvent(new CustomEvent('advanced-search', {
detail: e.detail,
bubbles: true,
composed: true
}));
});
} }
async loadData() { async loadData() {
@ -196,7 +293,24 @@ class InterfaceConfig extends HTMLElement {
throw new Error('获取数据失败'); throw new Error('获取数据失败');
} }
this.data = await response.json(); this.data = await response.json();
// 从接口数据中提取构型和ATA章节列表
this.products = [...new Set(this.data.map(item => item.ProductName))];
this.atas = [...new Set(this.data.map(item => item.ATAName))];
this.updateTable(); this.updateTable();
this.updateAdvancedSearchOptions();
// 通知所有子组件数据已更新
const importDialog = this.shadowRoot.querySelector('import-dialog');
if (importDialog) {
importDialog.setOptions(this.products, this.atas);
}
const variableForm = this.shadowRoot.querySelector('variable-form');
if (variableForm) {
variableForm.setOptions(this.products, this.atas);
}
} catch (error) { } catch (error) {
console.error('加载数据时出错:', error); console.error('加载数据时出错:', error);
alert('加载数据失败'); alert('加载数据失败');
@ -210,6 +324,21 @@ class InterfaceConfig extends HTMLElement {
} }
} }
updateAdvancedSearchOptions() {
const toolbar = this.shadowRoot.querySelector('interface-toolbar');
if (!toolbar) return;
// 使用已提取的构型和ATA章节列表
const structs = [...new Set(this.data.map(item => item.ModelStructName))];
// 更新下拉选项
toolbar.updateFilterOptions({
products: this.products,
atas: this.atas,
structs
});
}
render() { render() {
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>
@ -238,6 +367,7 @@ class InterfaceConfig extends HTMLElement {
<interface-toolbar></interface-toolbar> <interface-toolbar></interface-toolbar>
<interface-data-table></interface-data-table> <interface-data-table></interface-data-table>
<variable-form></variable-form> <variable-form></variable-form>
<import-dialog></import-dialog>
</div> </div>
`; `;
} }

View File

@ -6,182 +6,366 @@ class InterfaceDataTable extends HTMLElement {
this.filteredData = []; this.filteredData = [];
this.selectedRows = new Set(); this.selectedRows = new Set();
this.searchKeyword = ''; this.searchKeyword = '';
this.filters = {
productName: '',
ataName: '',
structName: ''
};
this.pageSize = 10;
this.currentPage = 1;
this.isLoading = false;
this.filterTimeout = null;
this.eventListeners = new Map(); // 存储事件监听器
this.debouncedFilter = this.debounce(this.filterData.bind(this), 300);
} }
connectedCallback() { connectedCallback() {
this.render(); this.render();
this.addEventListeners(); this.addEventListeners();
this.updateProductFilter();
} }
setData(data) { addEventListeners() {
this.data = data; const selectAllCheckbox = this.shadowRoot.querySelector('#selectAll');
this.updateProductFilter(); const rowCheckboxes = this.shadowRoot.querySelectorAll('.row-checkbox');
this.renderTable(); const editButtons = this.shadowRoot.querySelectorAll('.edit-btn');
} const deleteButtons = this.shadowRoot.querySelectorAll('.delete-btn');
setSearchKeyword(keyword) { selectAllCheckbox.addEventListener('change', (e) => {
this.searchKeyword = keyword.toLowerCase(); const isChecked = e.target.checked;
this.updateProductFilter(); rowCheckboxes.forEach(checkbox => {
this.renderTable(); checkbox.checked = isChecked;
} const index = parseInt(checkbox.dataset.index);
if (isChecked) {
this.selectedRows.add(index);
} else {
this.selectedRows.delete(index);
}
});
});
updateProductFilter() { rowCheckboxes.forEach(checkbox => {
const headerTools = document.querySelector('header-tools'); checkbox.addEventListener('change', (e) => {
if (headerTools) { const index = parseInt(e.target.dataset.index);
const selectedProduct = headerTools.selectedProduct; if (e.target.checked) {
this.filterData(selectedProduct); this.selectedRows.add(index);
} else { } else {
// 如果没有找到 header-tools 组件,显示所有数据 this.selectedRows.delete(index);
}
selectAllCheckbox.checked = this.selectedRows.size === rowCheckboxes.length;
});
});
editButtons.forEach(button => {
button.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index);
const item = this.filteredData[index];
this.dispatchEvent(new CustomEvent('edit', {
detail: item,
bubbles: true,
composed: true
}));
});
});
deleteButtons.forEach(button => {
button.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index);
const item = this.filteredData[index];
if (confirm(`确定要删除接口 "${item.InterfaceName}" 吗?`)) {
this.dispatchEvent(new CustomEvent('delete', {
detail: item,
bubbles: true,
composed: true
}));
}
});
});
// 监听搜索事件
this.addEventListener('search', async (e) => {
this.searchKeyword = e.detail.keyword.toLowerCase();
await this.filterData();
});
// 监听重置搜索事件
this.addEventListener('reset-search', async () => {
// 重置所有筛选条件
this.searchKeyword = '';
this.filters = {
productName: '',
ataName: '',
structName: ''
};
// 恢复原始数据
this.filteredData = [...this.data]; this.filteredData = [...this.data];
// 重新渲染表格
this.render();
});
// 监听高级搜索事件
this.addEventListener('advanced-search', async (e) => {
this.filters = e.detail;
await this.filterData();
});
// 添加分页事件监听器
const firstPageBtn = this.shadowRoot.querySelector('#firstPage');
const prevPageBtn = this.shadowRoot.querySelector('#prevPage');
const nextPageBtn = this.shadowRoot.querySelector('#nextPage');
const lastPageBtn = this.shadowRoot.querySelector('#lastPage');
const pageSizeSelect = this.shadowRoot.querySelector('#pageSize');
if (firstPageBtn) {
firstPageBtn.addEventListener('click', () => this.setPage(1));
}
if (prevPageBtn) {
prevPageBtn.addEventListener('click', () => this.setPage(this.currentPage - 1));
}
if (nextPageBtn) {
nextPageBtn.addEventListener('click', () => this.setPage(this.currentPage + 1));
}
if (lastPageBtn) {
lastPageBtn.addEventListener('click', () => this.setPage(this.getTotalPages()));
}
if (pageSizeSelect) {
pageSizeSelect.addEventListener('change', (e) => {
this.pageSize = parseInt(e.target.value);
this.currentPage = 1; // 重置到第一页
this.render();
});
} }
} }
filterData(selectedProduct) { // 防抖函数
// 首先根据构型过滤 debounce(func, wait) {
let filteredByProduct = selectedProduct && selectedProduct !== '' return (...args) => {
? this.data.filter(item => item.ProductName === selectedProduct) if (this.filterTimeout) {
: [...this.data]; clearTimeout(this.filterTimeout);
}
this.filterTimeout = setTimeout(() => {
func.apply(this, args);
this.filterTimeout = null;
}, wait);
};
}
// 然后根据搜索关键词过滤 async filterData() {
if (this.searchKeyword) { if (this.isLoading) return;
this.filteredData = filteredByProduct.filter(item => this.setLoading(true);
item.ATAName.toLowerCase().includes(this.searchKeyword) ||
item.ModelStructName.toLowerCase().includes(this.searchKeyword) || try {
item.InterfaceName.toLowerCase().includes(this.searchKeyword) await new Promise(resolve => {
); if (this.filterTimeout) {
} else { clearTimeout(this.filterTimeout);
this.filteredData = filteredByProduct; }
this.filterTimeout = setTimeout(() => {
requestAnimationFrame(() => {
// 首先根据高级搜索条件过滤
let filtered = [...this.data];
if (this.filters.productName) {
filtered = filtered.filter(item =>
item.ProductName === this.filters.productName
);
}
if (this.filters.ataName) {
filtered = filtered.filter(item =>
item.ATAName === this.filters.ataName
);
}
if (this.filters.structName) {
filtered = filtered.filter(item =>
item.ModelStructName === this.filters.structName
);
}
// 然后根据搜索关键词过滤
if (this.searchKeyword) {
this.filteredData = filtered.filter(item =>
item.InterfaceName.toLowerCase().includes(this.searchKeyword)
);
} else {
this.filteredData = filtered;
}
this.currentPage = 1; // 重置到第一页
this.render();
resolve();
});
}, 300);
});
} finally {
this.setLoading(false);
}
}
async setData(data) {
this.setLoading(true);
try {
// 清理旧数据
this.data = [];
this.filteredData = [];
this.selectedRows.clear();
// 设置新数据
this.data = data;
this.filteredData = [...data];
this.currentPage = 1;
this.render();
} finally {
this.setLoading(false);
} }
} }
getSelectedRows() { getSelectedRows() {
return Array.from(this.selectedRows); return Array.from(this.selectedRows).map(index => this.filteredData[index]);
} }
addEventListeners() { formatArraySize(item) {
this.shadowRoot.addEventListener('click', (e) => { if (item.InterfaceIsArray === 1) {
const row = e.target.closest('tr'); if (item.InterfaceArraySize_2 && item.InterfaceArraySize_2 > 1) {
if (!row) return; return `${item.InterfaceArraySize_1}×${item.InterfaceArraySize_2}`;
if (e.target.type === 'checkbox') {
const id = row.dataset.id;
if (e.target.checked) {
this.selectedRows.add(id);
} else {
this.selectedRows.delete(id);
}
this.updateSelectedRows();
} }
}); return `${item.InterfaceArraySize_1}`;
}
// 监听构型变化事件 return '1';
document.addEventListener('product-change', () => {
this.updateProductFilter();
this.renderTable();
});
// 监听搜索事件
document.addEventListener('search', (e) => {
this.setSearchKeyword(e.detail.keyword);
});
} }
updateSelectedRows() { getTotalPages() {
const checkboxes = this.shadowRoot.querySelectorAll('input[type="checkbox"]'); return Math.ceil(this.filteredData.length / this.pageSize);
checkboxes.forEach(checkbox => {
checkbox.checked = this.selectedRows.has(checkbox.closest('tr').dataset.id);
});
} }
renderTable() { getCurrentPageData() {
const tbody = this.shadowRoot.querySelector('tbody'); const start = (this.currentPage - 1) * this.pageSize;
tbody.innerHTML = ''; const end = start + this.pageSize;
return this.filteredData.slice(start, end).map((item, index) => ({
...item,
_displayIndex: start + index
}));
}
this.filteredData.forEach(item => { setPage(page) {
const tr = document.createElement('tr'); if (page < 1 || page > this.getTotalPages()) return;
tr.dataset.id = `${item.ProductName}-${item.ATAName}-${item.ModelStructName}-${item.InterfaceName}`; this.currentPage = page;
this.render();
}
tr.innerHTML = ` setLoading(loading) {
<td><input type="checkbox"></td> this.isLoading = loading;
<td>${item.ProductName}</td> this.render();
<td>${item.ATAName}</td>
<td>${item.ModelStructName}</td>
<td>${item.InterfaceName}</td>
<td>${item.InterfaceType}</td>
<td>${item.InterfaceOption === 1 ? '是' : '否'}</td>
<td>${item.InterfaceIsArray === 1 ? '是' : '否'}</td>
<td>${item.InterfaceArraySize_1 || ''}</td>
<td>${item.InterfaceArraySize_2 || ''}</td>
<td>${item.InterfaceNotes || ''}</td>
<td>
<button class="edit-btn">编辑</button>
<button class="delete-btn">删除</button>
</td>
`;
tr.querySelector('.edit-btn').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('edit', {
detail: {
ProductName: item.ProductName,
ATAName: item.ATAName,
ModelStructName: item.ModelStructName,
InterfaceName: item.InterfaceName
}
}));
});
tr.querySelector('.delete-btn').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
ProductName: item.ProductName,
ATAName: item.ATAName,
ModelStructName: item.ModelStructName,
InterfaceName: item.InterfaceName
}
}));
});
tbody.appendChild(tr);
});
} }
render() { render() {
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>
:host { .data-table {
display: block;
width: 100%;
overflow-x: auto;
}
table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 14px; margin-top: 10px;
table-layout: fixed;
position: relative;
} }
th, td { .loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(2px);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.data-table th,
.data-table td {
border: 1px solid #ddd;
padding: 8px; padding: 8px;
text-align: left; text-align: left;
border-bottom: 1px solid #ddd; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
th { .data-table th {
background-color: #f5f5f5; background-color: #f5f5f5;
font-weight: bold; font-weight: bold;
} }
tr:hover { .data-table tr:nth-child(even) {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
.edit-btn, .delete-btn { .data-table tr:hover {
background-color: #f0f0f0;
}
.checkbox-cell {
width: 20px;
text-align: center;
}
.product-cell {
width: 60px;
}
.ata-cell {
width: 60px;
}
.struct-cell {
width: 150px;
}
.interface-cell {
width: 220px;
}
.type-cell {
width: 100px;
}
.size-cell {
width: 60px;
}
.notes-cell {
width: 220px;
}
.action-cell {
width: 80px;
}
.action-btn {
padding: 4px 8px; padding: 4px 8px;
margin: 0 4px; margin: 0 4px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
box-shadow: none;
} }
.edit-btn { .edit-btn {
@ -194,43 +378,290 @@ class InterfaceDataTable extends HTMLElement {
color: white; color: white;
} }
input[type="checkbox"] { .action-btn:hover {
opacity: 0.9;
}
.action-btn:focus {
outline: none;
box-shadow: none;
}
.action-btn:active {
outline: none;
box-shadow: none;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
gap: 10px;
}
.pagination button {
padding: 5px 10px;
border: 1px solid #ddd;
background-color: white;
cursor: pointer; cursor: pointer;
border-radius: 4px;
}
.pagination button:hover {
background-color: #f0f0f0;
}
.pagination button:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.pagination .page-info {
margin: 0 10px;
}
.pagination .page-size-select {
margin-left: 10px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
} }
</style> </style>
<table> <div style="position: relative;">
<thead> <table class="data-table">
<tr> <thead>
<th><input type="checkbox" id="select-all"></th> <tr>
<th>构型</th> <th class="checkbox-cell">
<th>ATA章节</th> <input type="checkbox" id="selectAll">
<th>模型结构体名</th> </th>
<th>接口</th> <th class="product-cell">构型</th>
<th>接口类型</th> <th class="ata-cell">ATA章节</th>
<th>是否可选</th> <th class="struct-cell">接口结构体名</th>
<th>是否为数组</th> <th class="interface-cell">接口名称</th>
<th>第一维大小</th> <th class="type-cell">数据类型</th>
<th>第二维大小</th> <th class="size-cell">数据大小</th>
<th>备注</th> <th class="notes-cell">备注</th>
<th>操作</th> <th class="action-cell">操作</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody>
</table> ${this.getCurrentPageData().map((item) => `
<tr>
<td class="checkbox-cell">
<input type="checkbox" class="row-checkbox" data-index="${item._displayIndex}">
</td>
<td>${item.ProductName || ''}</td>
<td>${item.ATAName || ''}</td>
<td>${item.ModelStructName || ''}</td>
<td>${item.InterfaceName || ''}</td>
<td>${item.InterfaceType || ''}</td>
<td>${this.formatArraySize(item)}</td>
<td>${item.InterfaceNotes || ''}</td>
<td>
<button type="button" class="action-btn edit-btn" data-index="${item._displayIndex}">编辑</button>
<button type="button" class="action-btn delete-btn" data-index="${item._displayIndex}">删除</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
${this.isLoading ? `
<div class="loading-overlay">
<div class="loading-spinner"></div>
</div>
` : ''}
</div>
<div class="pagination">
<button id="firstPage" ${this.currentPage === 1 ? 'disabled' : ''}>首页</button>
<button id="prevPage" ${this.currentPage === 1 ? 'disabled' : ''}>上一页</button>
<span class="page-info"> ${this.currentPage} / ${this.getTotalPages()} </span>
<button id="nextPage" ${this.currentPage === this.getTotalPages() ? 'disabled' : ''}>下一页</button>
<button id="lastPage" ${this.currentPage === this.getTotalPages() ? 'disabled' : ''}>末页</button>
<select class="page-size-select" id="pageSize">
<option value="10" ${this.pageSize === 10 ? 'selected' : ''}>10/</option>
<option value="20" ${this.pageSize === 20 ? 'selected' : ''}>20/</option>
<option value="50" ${this.pageSize === 50 ? 'selected' : ''}>50/</option>
<option value="100" ${this.pageSize === 100 ? 'selected' : ''}>100/</option>
</select>
</div>
`; `;
this.bindEventListeners();
}
this.shadowRoot.querySelector('#select-all').addEventListener('change', (e) => { // 修改事件监听器的添加方式
const checkboxes = this.shadowRoot.querySelectorAll('input[type="checkbox"]:not(#select-all)'); addEventListenerWithCleanup(element, event, handler) {
checkboxes.forEach(checkbox => { if (!this.eventListeners.has(element)) {
checkbox.checked = e.target.checked; this.eventListeners.set(element, new Map());
const id = checkbox.closest('tr').dataset.id; }
if (e.target.checked) { const elementListeners = this.eventListeners.get(element);
this.selectedRows.add(id);
} else { // 如果已经存在相同的事件监听器,先移除
this.selectedRows.delete(id); if (elementListeners.has(event)) {
} const handlers = elementListeners.get(event);
handlers.forEach(h => element.removeEventListener(event, h));
handlers.clear();
} else {
elementListeners.set(event, new Set());
}
elementListeners.get(event).add(handler);
element.addEventListener(event, handler);
}
// 清理事件监听器
cleanupEventListeners() {
this.eventListeners.forEach((elementListeners, element) => {
elementListeners.forEach((handlers, event) => {
handlers.forEach(handler => {
element.removeEventListener(event, handler);
});
}); });
}); });
this.eventListeners.clear();
}
bindEventListeners() {
// 清理之前的事件监听器
this.cleanupEventListeners();
const selectAllCheckbox = this.shadowRoot.querySelector('#selectAll');
const rowCheckboxes = this.shadowRoot.querySelectorAll('.row-checkbox');
const editButtons = this.shadowRoot.querySelectorAll('.edit-btn');
const deleteButtons = this.shadowRoot.querySelectorAll('.delete-btn');
// 全选复选框事件
if (selectAllCheckbox) {
const selectAllHandler = (e) => {
const isChecked = e.target.checked;
rowCheckboxes.forEach(checkbox => {
checkbox.checked = isChecked;
const index = parseInt(checkbox.dataset.index);
if (isChecked) {
this.selectedRows.add(index);
} else {
this.selectedRows.delete(index);
}
});
};
this.addEventListenerWithCleanup(selectAllCheckbox, 'change', selectAllHandler);
}
// 行复选框事件
rowCheckboxes.forEach(checkbox => {
const checkboxHandler = (e) => {
const index = parseInt(e.target.dataset.index);
if (e.target.checked) {
this.selectedRows.add(index);
} else {
this.selectedRows.delete(index);
}
if (selectAllCheckbox) {
selectAllCheckbox.checked = this.selectedRows.size === rowCheckboxes.length;
}
};
this.addEventListenerWithCleanup(checkbox, 'change', checkboxHandler);
});
// 编辑按钮事件
editButtons.forEach(button => {
const editHandler = (e) => {
const index = parseInt(e.target.dataset.index);
const item = this.filteredData[index];
if (item) {
this.dispatchEvent(new CustomEvent('edit', {
detail: { ...item },
bubbles: true,
composed: true
}));
}
};
this.addEventListenerWithCleanup(button, 'click', editHandler);
});
// 删除按钮事件
deleteButtons.forEach(button => {
const deleteHandler = (e) => {
const index = parseInt(e.target.dataset.index);
const item = this.filteredData[index];
if (item && confirm(`确定要删除接口 "${item.InterfaceName}" 吗?`)) {
this.dispatchEvent(new CustomEvent('delete', {
detail: { ...item },
bubbles: true,
composed: true
}));
}
};
this.addEventListenerWithCleanup(button, 'click', deleteHandler);
});
// 搜索事件
const searchHandler = async (e) => {
this.searchKeyword = e.detail.keyword.toLowerCase();
this.setLoading(true);
await this.debouncedFilter();
};
this.addEventListenerWithCleanup(this, 'search', searchHandler);
// 重置搜索事件
const resetSearchHandler = async () => {
this.searchKeyword = '';
this.filters = {
productName: '',
ataName: '',
structName: ''
};
this.filteredData = [...this.data];
this.render();
};
this.addEventListenerWithCleanup(this, 'reset-search', resetSearchHandler);
// 高级搜索事件
const advancedSearchHandler = async (e) => {
this.filters = e.detail;
this.setLoading(true);
await this.debouncedFilter();
};
this.addEventListenerWithCleanup(this, 'advanced-search', advancedSearchHandler);
// 分页按钮事件
const firstPageBtn = this.shadowRoot.querySelector('#firstPage');
const prevPageBtn = this.shadowRoot.querySelector('#prevPage');
const nextPageBtn = this.shadowRoot.querySelector('#nextPage');
const lastPageBtn = this.shadowRoot.querySelector('#lastPage');
const pageSizeSelect = this.shadowRoot.querySelector('#pageSize');
if (firstPageBtn) {
this.addEventListenerWithCleanup(firstPageBtn, 'click', () => this.setPage(1));
}
if (prevPageBtn) {
this.addEventListenerWithCleanup(prevPageBtn, 'click', () => this.setPage(this.currentPage - 1));
}
if (nextPageBtn) {
this.addEventListenerWithCleanup(nextPageBtn, 'click', () => this.setPage(this.currentPage + 1));
}
if (lastPageBtn) {
this.addEventListenerWithCleanup(lastPageBtn, 'click', () => this.setPage(this.getTotalPages()));
}
if (pageSizeSelect) {
this.addEventListenerWithCleanup(pageSizeSelect, 'change', (e) => {
this.pageSize = parseInt(e.target.value);
this.currentPage = 1;
this.render();
});
}
}
// 组件销毁时清理
disconnectedCallback() {
if (this.filterTimeout) {
clearTimeout(this.filterTimeout);
this.filterTimeout = null;
}
this.cleanupEventListeners();
this.data = [];
this.filteredData = [];
this.selectedRows.clear();
} }
} }

View File

@ -0,0 +1,321 @@
class ImportDialog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.data = [];
this.products = [];
this.atas = [];
}
connectedCallback() {
this.render();
this.addEventListeners();
}
// 接收父组件传递的选项数据
setOptions(products, atas) {
this.products = products;
this.atas = atas;
this.updateProductOptions();
this.updateAtaOptions();
}
updateProductOptions() {
const select = this.shadowRoot.querySelector('#product-select');
select.innerHTML = `
<option value="">请选择构型</option>
${this.products.map(product => `
<option value="${product}">${product}</option>
`).join('')}
`;
}
updateAtaOptions() {
const select = this.shadowRoot.querySelector('#ata-select');
select.innerHTML = `
<option value="">请选择ATA章节</option>
${this.atas.map(ata => `
<option value="${ata}">${ata}</option>
`).join('')}
`;
}
show(data) {
this.data = data;
this.updateTable();
this.style.display = 'block';
}
hide() {
this.style.display = 'none';
}
updateTable() {
const tbody = this.shadowRoot.querySelector('tbody');
const lowercaseCheckbox = this.shadowRoot.querySelector('#lowercase-checkbox');
const shouldLowercase = lowercaseCheckbox.checked;
tbody.innerHTML = this.data.map(item => `
<tr>
<td>${shouldLowercase ? item.name.toLowerCase() : item.name}</td>
<td>${item.variableType || '-'}</td>
<td>${item.structName}</td>
<td>${item.isArray ? '是' : '否'}</td>
<td>${item.arraySize1 || '-'}</td>
<td>${item.arraySize2 || '-'}</td>
<td>${item.description}</td>
</tr>
`).join('');
}
addEventListeners() {
const closeBtn = this.shadowRoot.querySelector('.close-btn');
const confirmBtn = this.shadowRoot.querySelector('.confirm-btn');
const cancelBtn = this.shadowRoot.querySelector('.cancel-btn');
const lowercaseCheckbox = this.shadowRoot.querySelector('#lowercase-checkbox');
closeBtn.addEventListener('click', () => this.hide());
cancelBtn.addEventListener('click', () => this.hide());
// 添加小写复选框的事件监听
lowercaseCheckbox.addEventListener('change', () => {
this.updateTable();
});
confirmBtn.addEventListener('click', () => {
const productSelect = this.shadowRoot.querySelector('#product-select');
const ataSelect = this.shadowRoot.querySelector('#ata-select');
const modelInput = this.shadowRoot.querySelector('#model-input');
if (!productSelect.value) {
alert('请选择构型');
return;
}
if (!ataSelect.value) {
alert('请选择ATA章节');
return;
}
if (!modelInput.value.trim()) {
alert('请输入模型名称');
return;
}
const modelName = modelInput.value.trim();
// 处理数据,转换为数据库格式
const processedData = this.data.map(item => ({
SystemName: 'XNSim', // 系统名称固定为XNSim
ProductName: productSelect.value,
ATAName: ataSelect.value,
ModelStructName: `${modelName}_${item.structName}`,
InterfaceName: lowercaseCheckbox.checked ? item.name.toLowerCase() : item.name,
InterfaceType: item.variableType || '',
InterfaceOption: 1,
InterfaceIsArray: item.isArray ? 1 : 0, // 确保是数字类型
InterfaceArraySize_1: item.arraySize1 || 0, // 确保是数字类型
InterfaceArraySize_2: item.arraySize2 || 0, // 确保是数字类型
InterfaceNotes: item.description || '' // 确保不是null
}));
// 验证数据
const invalidData = processedData.find(item => {
return !item.InterfaceType ||
(item.InterfaceIsArray === 1 && (!item.InterfaceArraySize_1 || item.InterfaceArraySize_1 <= 1));
});
if (invalidData) {
alert('数据格式不正确,请检查:\n1. 变量类型不能为空\n2. 如果是数组第一维大小必须大于1');
return;
}
this.dispatchEvent(new CustomEvent('confirm-import', {
detail: processedData,
bubbles: true,
composed: true
}));
this.hide();
});
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.dialog {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
min-width: 800px;
max-width: 90%;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 1.2em;
font-weight: bold;
}
.close-btn {
background: none;
border: none;
font-size: 1.5em;
cursor: pointer;
padding: 0;
color: #666;
}
.options {
margin-bottom: 20px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.option-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.option-group label {
font-weight: bold;
}
select, input[type="text"] {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin-top: 24px;
}
.content {
flex: 1;
overflow: auto;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f5f5f5;
position: sticky;
top: 0;
}
.footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
button {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.confirm-btn {
background-color: #4CAF50;
color: white;
}
.cancel-btn {
background-color: #f44336;
color: white;
}
</style>
<div class="dialog">
<div class="header">
<div class="title">导入数据预览</div>
<button class="close-btn">&times;</button>
</div>
<div class="options">
<div class="option-group">
<label for="product-select">构型</label>
<select id="product-select">
<option value="">请选择构型</option>
</select>
</div>
<div class="option-group">
<label for="ata-select">ATA章节</label>
<select id="ata-select">
<option value="">请选择ATA章节</option>
</select>
</div>
<div class="option-group">
<label for="model-input">模型名称</label>
<input type="text" id="model-input" placeholder="请输入模型名称">
</div>
<div class="option-group">
<div class="checkbox-group">
<input type="checkbox" id="lowercase-checkbox">
<label for="lowercase-checkbox">小写所有变量名</label>
</div>
</div>
</div>
<div class="content">
<table>
<thead>
<tr>
<th>变量名称</th>
<th>变量类型</th>
<th>结构体</th>
<th>是否数组</th>
<th>第一维大小</th>
<th>第二维大小</th>
<th>描述</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="footer">
<button class="confirm-btn">确认导入</button>
<button class="cancel-btn">取消</button>
</div>
</div>
`;
}
}
customElements.define('import-dialog', ImportDialog);

View File

@ -1,7 +1,8 @@
class Toolbar extends HTMLElement { class InterfaceToolbar extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.showAdvancedSearch = false;
} }
connectedCallback() { connectedCallback() {
@ -10,55 +11,112 @@ class Toolbar extends HTMLElement {
} }
addEventListeners() { addEventListeners() {
const searchBtn = this.shadowRoot.querySelector('.search-btn');
const resetBtn = this.shadowRoot.querySelector('.reset-btn');
const addBtn = this.shadowRoot.querySelector('.add-btn'); const addBtn = this.shadowRoot.querySelector('.add-btn');
const deleteBtn = this.shadowRoot.querySelector('.delete-btn'); const deleteBtn = this.shadowRoot.querySelector('.delete-btn');
const downloadBtn = this.shadowRoot.querySelector('.download-btn');
const importBtn = this.shadowRoot.querySelector('.import-btn'); const importBtn = this.shadowRoot.querySelector('.import-btn');
const searchInput = this.shadowRoot.querySelector('.search-input');
const searchBtn = this.shadowRoot.querySelector('.search-btn');
const resetBtn = this.shadowRoot.querySelector('.reset-btn');
const advancedSearchBtn = this.shadowRoot.querySelector('.advanced-search-btn');
const advancedSearchPanel = this.shadowRoot.querySelector('.advanced-search-panel');
const productSelect = this.shadowRoot.querySelector('#filterProduct');
const ataSelect = this.shadowRoot.querySelector('#filterAta');
const structSelect = this.shadowRoot.querySelector('#filterStruct');
const importModal = this.shadowRoot.querySelector('#importModal');
const importExcelBtn = this.shadowRoot.querySelector('#importExcelBtn');
const importIdlBtn = this.shadowRoot.querySelector('#importIdlBtn');
const cancelImportBtn = this.shadowRoot.querySelector('#cancelImportBtn');
addBtn.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('add-variable'));
});
deleteBtn.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('delete-variable'));
});
importBtn.addEventListener('click', () => {
importModal.style.display = 'block';
});
importExcelBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.xlsx,.xls';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
this.dispatchEvent(new CustomEvent('import-data', {
detail: { file, type: 'excel' }
}));
importModal.style.display = 'none';
}
};
input.click();
});
importIdlBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.idl';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
this.dispatchEvent(new CustomEvent('import-data', {
detail: { file, type: 'idl' }
}));
importModal.style.display = 'none';
}
};
input.click();
});
cancelImportBtn.addEventListener('click', () => {
importModal.style.display = 'none';
});
// 点击模态框外部关闭
importModal.addEventListener('click', (e) => {
if (e.target === importModal) {
importModal.style.display = 'none';
}
});
searchBtn.addEventListener('click', () => { searchBtn.addEventListener('click', () => {
const searchInput = this.shadowRoot.querySelector('.search-input'); const keyword = searchInput.value.trim();
this.dispatchEvent(new CustomEvent('search', { this.dispatchEvent(new CustomEvent('search', {
detail: { keyword: searchInput.value } detail: { keyword }
})); }));
}); });
resetBtn.addEventListener('click', () => { resetBtn.addEventListener('click', () => {
const searchInput = this.shadowRoot.querySelector('.search-input'); // 重置搜索框
searchInput.value = ''; searchInput.value = '';
// 重置高级搜索下拉框
productSelect.value = '';
ataSelect.value = '';
structSelect.value = '';
// 触发重置事件
this.dispatchEvent(new CustomEvent('reset-search')); this.dispatchEvent(new CustomEvent('reset-search'));
}); });
addBtn.addEventListener('click', () => { advancedSearchBtn.addEventListener('click', () => {
const form = document.querySelector('variable-form'); this.showAdvancedSearch = !this.showAdvancedSearch;
if (form) { advancedSearchPanel.style.display = this.showAdvancedSearch ? 'flex' : 'none';
form.setMode('add'); advancedSearchBtn.classList.toggle('active');
form.show();
}
}); });
deleteBtn.addEventListener('click', () => { // 监听高级搜索的筛选条件变化
this.dispatchEvent(new CustomEvent('delete-selected')); [productSelect, ataSelect, structSelect].forEach(select => {
}); select.addEventListener('change', () => {
this.dispatchEvent(new CustomEvent('advanced-search', {
downloadBtn.addEventListener('click', () => { detail: {
this.dispatchEvent(new CustomEvent('download-template')); productName: productSelect.value,
}); ataName: ataSelect.value,
structName: structSelect.value
importBtn.addEventListener('click', () => { }
const fileInput = this.shadowRoot.querySelector('.file-input');
fileInput.click();
});
const fileInput = this.shadowRoot.querySelector('.file-input');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
this.dispatchEvent(new CustomEvent('import-data', {
detail: { file }
})); }));
} });
}); });
} }
@ -67,65 +125,320 @@ class Toolbar extends HTMLElement {
<style> <style>
.toolbar { .toolbar {
display: flex; display: flex;
align-items: center;
gap: 10px; gap: 10px;
align-items: center;
padding: 10px; padding: 10px;
background-color: #f5f5f5; background-color: #f5f5f5;
border-bottom: 1px solid #ddd; border-radius: 4px;
} }
.search-container { .toolbar-group {
display: flex;
gap: 10px;
align-items: center;
}
.toolbar-group.left {
flex: 1;
}
.toolbar-group.center {
display: flex;
gap: 10px;
align-items: center;
}
.toolbar-group.right {
flex: 1;
justify-content: flex-end;
}
.search-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
} }
.search-input { .search-input {
padding: 6px 12px; padding: 6px 10px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
width: 200px; width: 200px;
} }
button { .btn {
padding: 6px 12px; padding: 6px 12px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
background-color: #4CAF50;
color: white;
cursor: pointer; cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
background-color: #fff;
color: #333;
} }
button:hover { .btn:hover {
background-color: #f0f0f0;
}
.add-btn {
background-color: #4CAF50;
color: white;
}
.add-btn:hover {
background-color: #45a049; background-color: #45a049;
} }
.delete-btn { .delete-btn {
background-color: #f44336; background-color: #f44336;
color: white;
} }
.delete-btn:hover { .delete-btn:hover {
background-color: #da190b; background-color: #da190b;
} }
.file-input { .import-btn {
background-color: #2196F3;
color: white;
}
.import-btn:hover {
background-color: #1976D2;
}
.icon-btn {
padding: 6px;
border: none;
border-radius: 4px;
cursor: pointer;
background: none;
color: #666;
display: flex;
align-items: center;
justify-content: center;
}
.icon-btn img {
width: 16px;
height: 16px;
}
.icon-btn:hover {
background-color: #e0e0e0;
}
.advanced-search-btn {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
background-color: #fff;
}
.advanced-search-btn.active {
background-color: #e0e0e0;
}
.advanced-search-panel {
display: none; display: none;
gap: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
margin-top: 10px;
}
.advanced-search-panel select {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 150px;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border-radius: 8px;
width: 800px;
max-width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-title {
font-size: 18px;
font-weight: bold;
}
.modal-body {
display: flex;
gap: 20px;
}
.import-section {
flex: 1;
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.import-section h3 {
margin-top: 0;
margin-bottom: 15px;
}
.import-btn-section {
margin-bottom: 15px;
}
.import-description {
font-size: 14px;
color: #666;
line-height: 1.5;
}
.modal-footer {
margin-top: 20px;
text-align: right;
}
.cancel-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #f5f5f5;
color: #333;
cursor: pointer;
}
.cancel-btn:hover {
background-color: #e0e0e0;
} }
</style> </style>
<div class="toolbar"> <div class="toolbar">
<div class="search-container"> <div class="toolbar-group left">
<input type="text" class="search-input" placeholder="搜索..."> <button class="btn add-btn">
<button class="search-btn">搜索</button> <i class="fas fa-plus"></i>
<button class="reset-btn">重置</button> 添加变量
</button>
<button class="btn delete-btn">
<i class="fas fa-trash"></i>
删除变量
</button>
</div>
<div class="toolbar-group center">
<button class="btn import-btn">
<i class="fas fa-upload"></i>
导入数据
</button>
</div>
<div class="toolbar-group right">
<div class="search-group">
<input type="text" class="search-input" placeholder="搜索接口名称...">
<button class="icon-btn search-btn" title="搜索">
<img src="assets/icons/png/search_b.png" alt="搜索">
</button>
<button class="icon-btn reset-btn" title="重置">
<img src="assets/icons/png/refresh_b.png" alt="重置">
</button>
<button class="advanced-search-btn">高级搜索</button>
</div>
</div>
</div>
<div class="advanced-search-panel">
<select id="filterProduct">
<option value="">选择构型</option>
</select>
<select id="filterAta">
<option value="">选择ATA章节</option>
</select>
<select id="filterStruct">
<option value="">选择接口结构体</option>
</select>
</div>
<div class="modal" id="importModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">导入数据</div>
</div>
<div class="modal-body">
<div class="import-section">
<h3>从ICD导入</h3>
<div class="import-btn-section">
<button class="btn import-btn" id="importExcelBtn">
<i class="fas fa-file-excel"></i>
选择Excel文件
</button>
</div>
<div class="import-description">
支持从Excel文件(.xlsx/.xls)导入接口数据<br>
请确保Excel文件格式正确包含所有必要的字段<br>
导入过程中请勿关闭页面
</div>
</div>
<div class="import-section">
<h3>从IDL导入</h3>
<div class="import-btn-section">
<button class="btn import-btn" id="importIdlBtn">
<i class="fas fa-file-code"></i>
选择IDL文件
</button>
</div>
<div class="import-description">
支持从IDL文件(.idl)导入接口数据<br>
请确保IDL文件格式正确包含所有必要的接口定义<br>
导入过程中请勿关闭页面
</div>
</div>
</div>
<div class="modal-footer">
<button class="cancel-btn" id="cancelImportBtn">取消</button>
</div>
</div> </div>
<button class="add-btn">添加变量</button>
<button class="delete-btn">删除变量</button>
<button class="download-btn">下载模板</button>
<button class="import-btn">导入数据</button>
<input type="file" class="file-input" accept=".xlsx,.xls,.csv">
</div> </div>
`; `;
} }
updateFilterOptions(options) {
const productSelect = this.shadowRoot.querySelector('#filterProduct');
const ataSelect = this.shadowRoot.querySelector('#filterAta');
const structSelect = this.shadowRoot.querySelector('#filterStruct');
// 更新构型选项
productSelect.innerHTML = '<option value="">选择构型</option>' +
options.products.map(product => `<option value="${product}">${product}</option>`).join('');
// 更新ATA章节选项
ataSelect.innerHTML = '<option value="">选择ATA章节</option>' +
options.atas.map(ata => `<option value="${ata}">${ata}</option>`).join('');
// 更新接口结构体选项
structSelect.innerHTML = '<option value="">选择接口结构体</option>' +
options.structs.map(struct => `<option value="${struct}">${struct}</option>`).join('');
}
} }
customElements.define('interface-toolbar', Toolbar); customElements.define('interface-toolbar', InterfaceToolbar);

View File

@ -4,6 +4,9 @@ class VariableForm extends HTMLElement {
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.mode = 'add'; this.mode = 'add';
this.interfaceData = null; this.interfaceData = null;
this.products = [];
this.ataChapters = [];
this.modelStructs = [];
} }
connectedCallback() { connectedCallback() {
@ -11,81 +14,252 @@ class VariableForm extends HTMLElement {
this.addEventListeners(); this.addEventListeners();
} }
// 接收父组件传递的选项数据
setOptions(products, atas) {
this.products = products;
this.ataChapters = atas;
this.updateProductOptions();
this.updateAtaOptions();
}
updateProductOptions() {
const select = this.shadowRoot.querySelector('#productName');
if (select) {
select.innerHTML = `
<option value="">请选择构型</option>
${this.products.map(product => `
<option value="${product}" ${this.interfaceData?.ProductName === product ? 'selected' : ''}>
${product}
</option>
`).join('')}
`;
}
}
updateAtaOptions() {
const select = this.shadowRoot.querySelector('#ataName');
if (select) {
select.innerHTML = `
<option value="">请选择ATA章节</option>
${this.ataChapters.map(chapter => `
<option value="${chapter}" ${this.interfaceData?.ATAName === chapter ? 'selected' : ''}>
${chapter}
</option>
`).join('')}
`;
}
}
async loadModelStructs(productName, ataName) {
try {
const response = await fetch(`/api/interface/struct/list?systemName=XNSim&productName=${productName}&ataName=${ataName}`);
if (!response.ok) {
throw new Error('获取模型结构体列表失败');
}
const data = await response.json();
this.modelStructs = data.map(item => item.ModelStructName);
this.updateModelStructSelect();
} catch (error) {
console.error('加载模型结构体列表失败:', error);
alert('加载模型结构体列表失败');
}
}
updateModelStructSelect() {
const select = this.shadowRoot.querySelector('#modelStructName');
if (select) {
select.innerHTML = `
<option value="">请选择模型结构体</option>
${this.modelStructs.map(struct => `
<option value="${struct}" ${this.interfaceData?.ModelStructName === struct ? 'selected' : ''}>
${struct}
</option>
`).join('')}
`;
}
}
setMode(mode, data = null) { setMode(mode, data = null) {
this.mode = mode; this.mode = mode;
this.interfaceData = data; this.interfaceData = data;
this.updateForm(); if (data) {
// 保存原始数据
this.originalData = {
ProductName: data.ProductName,
ATAName: data.ATAName,
ModelStructName: data.ModelStructName,
InterfaceName: data.InterfaceName
};
// 如果是在编辑模式下,需要先加载模型结构体列表
this.loadModelStructs(data.ProductName, data.ATAName).then(() => {
this.render();
this.addEventListeners();
this.show();
});
} else {
this.render();
this.addEventListeners();
this.show();
}
} }
show() { show() {
this.shadowRoot.querySelector('.form-dialog').style.display = 'block'; const overlay = this.shadowRoot.querySelector('.form-overlay');
if (overlay) {
overlay.style.display = 'block';
}
} }
hide() { hide() {
this.shadowRoot.querySelector('.form-dialog').style.display = 'none'; const overlay = this.shadowRoot.querySelector('.form-overlay');
} if (overlay) {
overlay.style.display = 'none';
updateForm() {
const form = this.shadowRoot.querySelector('form');
if (this.mode === 'edit' && this.interfaceData) {
form.querySelector('[name="systemName"]').value = this.interfaceData.SystemName;
form.querySelector('[name="productName"]').value = this.interfaceData.ProductName;
form.querySelector('[name="ataName"]').value = this.interfaceData.ATAName;
form.querySelector('[name="modelStructName"]').value = this.interfaceData.ModelStructName;
form.querySelector('[name="interfaceName"]').value = this.interfaceData.InterfaceName;
form.querySelector('[name="interfaceType"]').value = this.interfaceData.InterfaceType;
form.querySelector('[name="interfaceOption"]').value = this.interfaceData.InterfaceOption;
form.querySelector('[name="interfaceIsArray"]').checked = this.interfaceData.InterfaceIsArray;
form.querySelector('[name="interfaceArraySize_1"]').value = this.interfaceData.InterfaceArraySize_1 || '';
form.querySelector('[name="interfaceArraySize_2"]').value = this.interfaceData.InterfaceArraySize_2 || '';
form.querySelector('[name="interfaceNotes"]').value = this.interfaceData.InterfaceNotes || '';
} else {
form.reset();
form.querySelector('[name="systemName"]').value = 'XNSim';
form.querySelector('[name="productName"]').value = 'C909';
} }
} }
addEventListeners() { addEventListeners() {
const form = this.shadowRoot.querySelector('form'); const form = this.shadowRoot.querySelector('form');
const closeBtn = this.shadowRoot.querySelector('.close-btn');
const cancelBtn = this.shadowRoot.querySelector('.cancel-btn'); const cancelBtn = this.shadowRoot.querySelector('.cancel-btn');
const productSelect = this.shadowRoot.querySelector('#productName');
const ataSelect = this.shadowRoot.querySelector('#ataName');
const interfaceNameInput = this.shadowRoot.querySelector('#interfaceName');
const isArrayCheckbox = this.shadowRoot.querySelector('#interfaceIsArray');
const arraySize1Input = this.shadowRoot.querySelector('#interfaceArraySize_1');
const arraySize2Input = this.shadowRoot.querySelector('#interfaceArraySize_2');
// 监听构型和ATA章节的变化
productSelect.addEventListener('change', () => {
const productName = productSelect.value;
const ataName = ataSelect.value;
if (productName && ataName) {
this.loadModelStructs(productName, ataName);
}
});
ataSelect.addEventListener('change', () => {
const productName = productSelect.value;
const ataName = ataSelect.value;
if (productName && ataName) {
this.loadModelStructs(productName, ataName);
}
});
// 验证C++命名规范
interfaceNameInput.addEventListener('input', (e) => {
const value = e.target.value;
// C++命名规范:以字母或下划线开头,只能包含字母、数字和下划线
const isValid = /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value);
if (!isValid) {
e.target.setCustomValidity('接口名称必须以字母或下划线开头,只能包含字母、数字和下划线');
} else {
e.target.setCustomValidity('');
}
});
// 验证数组大小
arraySize1Input.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
if (value <= 1) {
e.target.setCustomValidity('第一维大小必须大于1');
} else {
e.target.setCustomValidity('');
}
});
arraySize2Input.addEventListener('input', (e) => {
const value = e.target.value;
if (value && parseInt(value) <= 1 && parseInt(value) !== 0) {
e.target.setCustomValidity('第二维大小必须大于1');
} else {
e.target.setCustomValidity('');
}
});
form.addEventListener('submit', (e) => { form.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(form); const formData = new FormData(form);
const interfaceData = { const data = {
SystemName: formData.get('systemName'), SystemName: 'XNSim',
ProductName: formData.get('productName'), ProductName: formData.get('productName'),
ATAName: formData.get('ataName'), ATAName: formData.get('ataName'),
ModelStructName: formData.get('modelStructName'), ModelStructName: formData.get('modelStructName'),
InterfaceName: formData.get('interfaceName'), InterfaceName: formData.get('interfaceName'),
InterfaceType: formData.get('interfaceType'), InterfaceType: formData.get('interfaceType'),
InterfaceOption: formData.get('interfaceOption'), InterfaceOption: 1,
InterfaceIsArray: formData.get('interfaceIsArray') === 'on', InterfaceIsArray: formData.get('interfaceIsArray') ? 1 : 0,
InterfaceArraySize_1: formData.get('interfaceArraySize_1') || null, InterfaceArraySize_1: formData.get('interfaceArraySize_1'),
InterfaceArraySize_2: formData.get('interfaceArraySize_2') || null, InterfaceArraySize_2: formData.get('interfaceArraySize_2'),
InterfaceNotes: formData.get('interfaceNotes') || null InterfaceNotes: formData.get('interfaceNotes')
}; };
this.dispatchEvent(new CustomEvent('save-success', { // 如果是编辑模式,添加原始数据
detail: { if (this.mode === 'edit' && this.originalData) {
mode: this.mode, data.originalProductName = this.originalData.ProductName;
...interfaceData data.originalATAName = this.originalData.ATAName;
} data.originalModelStructName = this.originalData.ModelStructName;
})); data.originalInterfaceName = this.originalData.InterfaceName;
}
// 验证必填字段
const requiredFields = [
'SystemName',
'ProductName',
'ATAName',
'ModelStructName',
'InterfaceName',
'InterfaceType'
];
const missingFields = requiredFields.filter(field => !data[field]);
if (missingFields.length > 0) {
alert(`请填写以下必填字段:${missingFields.join(', ')}`);
return;
}
// 如果是数组类型,验证数组大小
if (data.InterfaceIsArray === 1) {
if (!data.InterfaceArraySize_1 || data.InterfaceArraySize_1 <= 1) {
alert('第一维大小必须大于1');
return;
}
if (data.InterfaceArraySize_2 && data.InterfaceArraySize_2 <= 1 && data.InterfaceArraySize_2 !== 0) {
alert('第二维大小必须大于1或等于0');
return;
}
}
this.dispatchEvent(new CustomEvent('save-success', {
detail: { ...data, mode: this.mode }
}));
this.hide(); this.hide();
}); });
closeBtn.addEventListener('click', () => this.hide()); cancelBtn.addEventListener('click', () => {
cancelBtn.addEventListener('click', () => this.hide()); this.hide();
});
// 监听数组大小输入框的变化
isArrayCheckbox.addEventListener('change', () => {
arraySize1Input.disabled = !isArrayCheckbox.checked;
arraySize2Input.disabled = !isArrayCheckbox.checked;
if (!isArrayCheckbox.checked) {
arraySize1Input.value = '';
arraySize2Input.value = '';
}
});
} }
render() { render() {
const interfaceTypes = [
'char', 'octet', 'short', 'unsigned short',
'long', 'unsigned long', 'long long', 'unsigned long long',
'float', 'double', 'long double', 'boolean'
];
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>
.form-dialog { .form-overlay {
display: none; display: none;
position: fixed; position: fixed;
top: 0; top: 0;
@ -96,38 +270,26 @@ class VariableForm extends HTMLElement {
z-index: 1000; z-index: 1000;
} }
.form-content { .form-container {
position: absolute; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
background-color: white; background-color: white;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 500px; width: 500px;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
} }
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.form-title { .form-title {
margin: 0 0 20px 0;
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
} }
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
}
.form-group { .form-group {
margin-bottom: 15px; margin-bottom: 15px;
} }
@ -138,10 +300,7 @@ class VariableForm extends HTMLElement {
font-weight: bold; font-weight: bold;
} }
input[type="text"], select, input[type="text"], input[type="number"] {
input[type="number"],
select,
textarea {
width: 100%; width: 100%;
padding: 8px; padding: 8px;
border: 1px solid #ddd; border: 1px solid #ddd;
@ -149,20 +308,24 @@ class VariableForm extends HTMLElement {
box-sizing: border-box; box-sizing: border-box;
} }
textarea { .checkbox-group {
height: 100px; display: flex;
resize: vertical; align-items: center;
gap: 5px;
} }
.form-footer { .checkbox-group input[type="checkbox"] {
width: auto;
}
.button-group {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 10px; gap: 10px;
margin-top: 20px; margin-top: 20px;
} }
.save-btn, button {
.cancel-btn {
padding: 8px 16px; padding: 8px 16px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
@ -178,67 +341,109 @@ class VariableForm extends HTMLElement {
background-color: #f44336; background-color: #f44336;
color: white; color: white;
} }
button:hover {
opacity: 0.9;
}
.array-size-group {
display: flex;
gap: 10px;
align-items: center;
}
.array-size-group input[type="number"] {
width: 100px;
}
</style> </style>
<div class="form-dialog"> <div class="form-overlay">
<div class="form-content"> <div class="form-container">
<div class="form-header"> <h2 class="form-title">${this.mode === 'add' ? '添加接口' : '编辑接口'}</h2>
<div class="form-title">${this.mode === 'add' ? '添加接口' : '编辑接口'}</div>
<button class="close-btn">&times;</button>
</div>
<form> <form>
<div class="form-group"> <div class="form-group">
<label>系统名称</label> <label for="productName">构型</label>
<input type="text" name="systemName" required> <select id="productName" name="productName" required>
</div> <option value="">请选择构型</option>
<div class="form-group"> ${this.products.map(product => `
<label>产品名称</label> <option value="${product}" ${this.interfaceData?.ProductName === product ? 'selected' : ''}>
<input type="text" name="productName" required> ${product}
</div> </option>
<div class="form-group"> `).join('')}
<label>ATA章节</label>
<input type="text" name="ataName" required>
</div>
<div class="form-group">
<label>模型结构名</label>
<input type="text" name="modelStructName" required>
</div>
<div class="form-group">
<label>接口名</label>
<input type="text" name="interfaceName" required>
</div>
<div class="form-group">
<label>接口类型</label>
<select name="interfaceType" required>
<option value="int">int</option>
<option value="float">float</option>
<option value="double">double</option>
<option value="bool">bool</option>
<option value="string">string</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>接口选项</label> <label for="ataName">ATA章节</label>
<input type="text" name="interfaceOption" required> <select id="ataName" name="ataName" required>
<option value="">请选择ATA章节</option>
${this.ataChapters.map(chapter => `
<option value="${chapter}" ${this.interfaceData?.ATAName === chapter ? 'selected' : ''}>
${chapter}
</option>
`).join('')}
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label> <label for="modelStructName">接口结构体名</label>
<input type="checkbox" name="interfaceIsArray"> <select id="modelStructName" name="modelStructName" required>
是否为数组 <option value="">请选择接口结构体</option>
</label> ${this.modelStructs.map(struct => `
<option value="${struct}" ${this.interfaceData?.ModelStructName === struct ? 'selected' : ''}>
${struct}
</option>
`).join('')}
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>数组大小1</label> <label for="interfaceName">接口名称</label>
<input type="number" name="interfaceArraySize_1"> <input type="text" id="interfaceName" name="interfaceName" required
value="${this.interfaceData?.InterfaceName || ''}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>数组大小2</label> <label for="interfaceType">数据类型</label>
<input type="number" name="interfaceArraySize_2"> <select id="interfaceType" name="interfaceType" required>
<option value="">请选择数据类型</option>
${interfaceTypes.map(type => `
<option value="${type}" ${this.interfaceData?.InterfaceType === type ? 'selected' : ''}>
${type}
</option>
`).join('')}
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>备注</label> <div class="checkbox-group">
<textarea name="interfaceNotes"></textarea> <input type="checkbox" id="interfaceIsArray" name="interfaceIsArray"
${this.interfaceData?.InterfaceIsArray === 1 ? 'checked' : ''}>
<label for="interfaceIsArray">是否为数组</label>
</div>
</div> </div>
<div class="form-footer">
<div class="form-group">
<label>数组大小</label>
<div class="array-size-group">
<input type="number" id="interfaceArraySize_1" name="interfaceArraySize_1"
value="${this.interfaceData?.InterfaceArraySize_1 || ''}"
${this.interfaceData?.InterfaceIsArray !== 1 ? 'disabled' : ''}
placeholder="第一维">
<span>×</span>
<input type="number" id="interfaceArraySize_2" name="interfaceArraySize_2"
value="${this.interfaceData?.InterfaceArraySize_2 || ''}"
${this.interfaceData?.InterfaceIsArray !== 1 ? 'disabled' : ''}
placeholder="第二维">
</div>
</div>
<div class="form-group">
<label for="interfaceNotes">备注</label>
<input type="text" id="interfaceNotes" name="interfaceNotes"
value="${this.interfaceData?.InterfaceNotes || ''}">
</div>
<div class="button-group">
<button type="button" class="cancel-btn">取消</button> <button type="button" class="cancel-btn">取消</button>
<button type="submit" class="save-btn">保存</button> <button type="submit" class="save-btn">保存</button>
</div> </div>

View File

@ -0,0 +1,69 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { parseICDFile } = require('../utils/icd-parser');
// 确保uploads目录存在
const uploadsDir = path.join(__dirname, '..', 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// 配置文件上传
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadsDir);
},
filename: function (req, file, cb) {
cb(null, Date.now() + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
// 检查文件扩展名
const ext = path.extname(file.originalname).toLowerCase();
if (ext !== '.xlsx') {
return cb(new Error('只允许上传.xlsx文件'));
}
cb(null, true);
}
});
// 导入ICD文件
router.post('/import', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: '未找到上传的文件'
});
}
const result = await parseICDFile(req.file.path);
// 删除临时文件
fs.unlinkSync(req.file.path);
res.json({
success: true,
data: result
});
} catch (error) {
// 如果文件存在,删除它
if (req.file && fs.existsSync(req.file.path)) {
fs.unlinkSync(req.file.path);
}
console.error('导入ICD文件时出错:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
module.exports = router;

View File

@ -3,7 +3,16 @@ const path = require('path');
const fs = require('fs').promises; const fs = require('fs').promises;
const xlsx = require('xlsx'); const xlsx = require('xlsx');
const multer = require('multer'); const multer = require('multer');
const { getDataInterfaces, addDataInterface, updateDataInterface, deleteDataInterface } = require('../utils/db-utils'); const {
getDataInterfaces,
addDataInterface,
updateDataInterface,
deleteDataInterface,
getDataInterfaceStructs,
addDataInterfaceStruct,
updateDataInterfaceStruct,
deleteDataInterfaceStruct
} = require('../utils/db-utils');
const router = express.Router(); const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
@ -87,6 +96,52 @@ router.delete('/delete', async (req, res) => {
} }
}); });
// 获取接口结构体列表
router.get('/struct/list', async (req, res) => {
try {
const { systemName, productName, ataName } = req.query;
const structs = await getDataInterfaceStructs(systemName, productName, ataName);
res.json(structs);
} catch (error) {
console.error('获取接口结构体列表失败:', error);
res.status(500).json({ error: '获取接口结构体列表失败' });
}
});
// 添加接口结构体
router.post('/struct/add', async (req, res) => {
try {
const result = await addDataInterfaceStruct(req.body);
res.json(result);
} catch (error) {
console.error('添加接口结构体失败:', error);
res.status(500).json({ error: error.message || '添加接口结构体失败' });
}
});
// 更新接口结构体
router.put('/struct/update', async (req, res) => {
try {
const result = await updateDataInterfaceStruct(req.body);
res.json(result);
} catch (error) {
console.error('更新接口结构体失败:', error);
res.status(500).json({ error: error.message || '更新接口结构体失败' });
}
});
// 删除接口结构体
router.delete('/struct/delete', async (req, res) => {
try {
const { systemName, productName, ataName, modelStructName } = req.query;
const result = await deleteDataInterfaceStruct(systemName, productName, ataName, modelStructName);
res.json(result);
} catch (error) {
console.error('删除接口结构体失败:', error);
res.status(500).json({ error: error.message || '删除接口结构体失败' });
}
});
// 下载模板 // 下载模板
router.get('/template', async (req, res) => { router.get('/template', async (req, res) => {
try { try {
@ -120,57 +175,40 @@ router.get('/template', async (req, res) => {
}); });
// 导入数据 // 导入数据
router.post('/import', upload.single('file'), async (req, res) => { router.post('/import', async (req, res) => {
try { try {
if (!req.file) { const importData = req.body;
return res.status(400).json({ error: '未上传文件' }); if (!Array.isArray(importData)) {
return res.status(400).json({ error: '导入数据格式错误' });
} }
const workbook = xlsx.read(req.file.buffer);
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const data = xlsx.utils.sheet_to_json(worksheet);
const results = []; const results = [];
for (const item of data) { for (const item of importData) {
try { try {
// 验证所有必填字段 // 验证所有必填字段
const requiredFields = { const requiredFields = [
'系统名称': 'SystemName', 'SystemName',
'产品名称': 'ProductName', 'ProductName',
'ATA章节': 'ATAName', 'ATAName',
'模型结构名': 'ModelStructName', 'ModelStructName',
'接口名': 'InterfaceName', 'InterfaceName',
'接口类型': 'InterfaceType', 'InterfaceType',
'接口选项': 'InterfaceOption', 'InterfaceOption',
'是否为数组': 'InterfaceIsArray', 'InterfaceIsArray',
'数组大小1': 'InterfaceArraySize_1', 'InterfaceArraySize_1',
'数组大小2': 'InterfaceArraySize_2', 'InterfaceArraySize_2',
'备注': 'InterfaceNotes' 'InterfaceNotes'
}; ];
// 检查所有必填字段是否存在 // 检查所有必填字段是否存在
for (const [excelField, dbField] of Object.entries(requiredFields)) { for (const field of requiredFields) {
if (item[excelField] === undefined || item[excelField] === null || item[excelField] === '') { if (item[field] === undefined || item[field] === null || item[field] === '') {
throw new Error(`字段 "${excelField}" 是必填字段`); throw new Error(`字段 "${field}" 是必填字段`);
} }
} }
const interfaceData = { const result = await addDataInterface(item);
SystemName: item['系统名称'], results.push({ success: true, data: item });
ProductName: item['产品名称'],
ATAName: item['ATA章节'],
ModelStructName: item['模型结构名'],
InterfaceName: item['接口名'],
InterfaceType: item['接口类型'],
InterfaceOption: item['接口选项'],
InterfaceIsArray: item['是否为数组'] === '是' ? 1 : 0,
InterfaceArraySize_1: item['数组大小1'],
InterfaceArraySize_2: item['数组大小2'],
InterfaceNotes: item['备注']
};
const result = await addDataInterface(interfaceData);
results.push({ success: true, data: interfaceData });
} catch (error) { } catch (error) {
results.push({ success: false, error: error.message, data: item }); results.push({ success: false, error: error.message, data: item });
} }

View File

@ -20,6 +20,7 @@ const simulationRoutes = require('./routes/run-simulation');
const udpMonitorRoutes = require('./routes/udp-monitor'); const udpMonitorRoutes = require('./routes/udp-monitor');
const productsRoutes = require('./routes/products'); const productsRoutes = require('./routes/products');
const interfaceRoutes = require('./routes/interface-config'); const interfaceRoutes = require('./routes/interface-config');
const icdImportRoutes = require('./routes/icd-import');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
@ -70,6 +71,7 @@ app.use('/api', simulationRoutes);
app.use('/api/udp-monitor', udpMonitorRoutes); app.use('/api/udp-monitor', udpMonitorRoutes);
app.use('/api', productsRoutes); app.use('/api', productsRoutes);
app.use('/api/interface', interfaceRoutes); app.use('/api/interface', interfaceRoutes);
app.use('/api/icd', icdImportRoutes);
// 主页路由 // 主页路由
app.get('/', (req, res) => { app.get('/', (req, res) => {

View File

@ -791,8 +791,14 @@ function updateDataInterface(interfaceData) {
'InterfaceArraySize_2' 'InterfaceArraySize_2'
]; ];
// 检查数据结构
if (!interfaceData.currentData || !interfaceData.originalData) {
throw new Error('数据格式错误:缺少 currentData 或 originalData');
}
// 验证 currentData 中的必填字段
for (const field of requiredFields) { for (const field of requiredFields) {
if (interfaceData[field] === undefined || interfaceData[field] === null) { if (interfaceData.currentData[field] === undefined || interfaceData.currentData[field] === null) {
throw new Error(`${field} 是必填字段`); throw new Error(`${field} 是必填字段`);
} }
} }
@ -810,10 +816,33 @@ function updateDataInterface(interfaceData) {
// 打开数据库连接 // 打开数据库连接
const db = new Database(dbPath); const db = new Database(dbPath);
// 更新接口 // 首先检查记录是否存在
const existingRecord = db.prepare(`
SELECT COUNT(*) as count FROM 'DataInterface'
WHERE SystemName = ? AND ProductName = ? AND ATAName = ?
AND ModelStructName = ? AND InterfaceName = ?
`).get(
interfaceData.originalData.SystemName,
interfaceData.originalData.ProductName,
interfaceData.originalData.ATAName,
interfaceData.originalData.ModelStructName,
interfaceData.originalData.InterfaceName
);
if (existingRecord.count === 0) {
db.close();
throw new Error('要更新的记录不存在');
}
// 更新接口,包括所有字段
const updateResult = db.prepare(` const updateResult = db.prepare(`
UPDATE 'DataInterface' UPDATE 'DataInterface'
SET InterfaceType = ?, SET SystemName = ?,
ProductName = ?,
ATAName = ?,
ModelStructName = ?,
InterfaceName = ?,
InterfaceType = ?,
InterfaceOption = ?, InterfaceOption = ?,
InterfaceIsArray = ?, InterfaceIsArray = ?,
InterfaceArraySize_1 = ?, InterfaceArraySize_1 = ?,
@ -822,21 +851,30 @@ function updateDataInterface(interfaceData) {
WHERE SystemName = ? AND ProductName = ? AND ATAName = ? WHERE SystemName = ? AND ProductName = ? AND ATAName = ?
AND ModelStructName = ? AND InterfaceName = ? AND ModelStructName = ? AND InterfaceName = ?
`).run( `).run(
interfaceData.InterfaceType, interfaceData.currentData.SystemName,
interfaceData.InterfaceOption, interfaceData.currentData.ProductName,
interfaceData.InterfaceIsArray, interfaceData.currentData.ATAName,
interfaceData.InterfaceArraySize_1, interfaceData.currentData.ModelStructName,
interfaceData.InterfaceArraySize_2, interfaceData.currentData.InterfaceName,
interfaceData.InterfaceNotes || null, interfaceData.currentData.InterfaceType,
interfaceData.SystemName, interfaceData.currentData.InterfaceOption,
interfaceData.ProductName, interfaceData.currentData.InterfaceIsArray,
interfaceData.ATAName, interfaceData.currentData.InterfaceArraySize_1,
interfaceData.ModelStructName, interfaceData.currentData.InterfaceArraySize_2,
interfaceData.InterfaceName interfaceData.currentData.InterfaceNotes || null,
interfaceData.originalData.SystemName,
interfaceData.originalData.ProductName,
interfaceData.originalData.ATAName,
interfaceData.originalData.ModelStructName,
interfaceData.originalData.InterfaceName
); );
db.close(); db.close();
if (updateResult.changes === 0) {
throw new Error('更新失败:没有记录被修改');
}
return { return {
success: true, success: true,
changes: updateResult.changes, changes: updateResult.changes,
@ -890,6 +928,62 @@ function deleteDataInterface(systemName, productName, ataName, modelStructName,
} }
} }
// 获取接口结构体列表
function getDataInterfaceStructs(systemName = 'XNSim', productName, ataName) {
try {
const xnCorePath = getXNCorePath();
if (!xnCorePath) {
throw new Error('XNCore环境变量未设置无法获取数据库路径');
}
const dbPath = xnCorePath + '/database/XNSim.db';
if (!dbPath) {
throw new Error('无法找到数据库文件');
}
// 打开数据库连接
const db = new Database(dbPath, { readonly: true });
// 根据是否传入 productName 构建不同的查询
let query;
let params;
if (!productName || productName === '') {
query = `
SELECT SystemName, ProductName, ATAName, ModelStructName
FROM 'DataInterface'
WHERE SystemName = ?
GROUP BY ModelStructName
ORDER BY ProductName, ATAName, ModelStructName
`;
params = [systemName];
} else {
query = `
SELECT SystemName, ProductName, ATAName, ModelStructName
FROM 'DataInterface'
WHERE SystemName = ? AND ProductName = ?
GROUP BY ModelStructName
ORDER BY ATAName, ModelStructName
`;
params = [systemName, productName];
}
if (ataName) {
query = query.replace('GROUP BY', 'AND ATAName = ? GROUP BY');
params.push(ataName);
}
const structs = db.prepare(query).all(...params);
db.close();
return structs;
} catch (error) {
console.error('获取接口结构体列表数据失败:', error.message);
throw error;
}
}
module.exports = { module.exports = {
getATAChapters, getATAChapters,
getModelsByChapterId, getModelsByChapterId,
@ -903,5 +997,6 @@ module.exports = {
getDataInterfaces, getDataInterfaces,
addDataInterface, addDataInterface,
updateDataInterface, updateDataInterface,
deleteDataInterface deleteDataInterface,
getDataInterfaceStructs
}; };

View File

@ -0,0 +1,98 @@
const XLSX = require('xlsx');
/**
* 解析数组维度字符串
* @param {string|number} dimensions - 数组维度字符串或数字 "10" "[10]" "[3][2]"
* @returns {Object} 包含数组维度的对象
*/
function parseArrayDimensions(dimensions) {
// 处理空值或1的情况
if (!dimensions || dimensions === '1' || dimensions === 1) {
return { isArray: false, arraySize1: 0, arraySize2: 0 };
}
// 确保dimensions是字符串
const dimStr = String(dimensions).trim();
// 尝试匹配形如 [10] 或 [3][2] 的格式
const bracketMatches = dimStr.match(/\[(\d+)\](?:\[(\d+)\])?/);
if (bracketMatches) {
const size1 = parseInt(bracketMatches[1]);
const size2 = bracketMatches[2] ? parseInt(bracketMatches[2]) : 0;
return {
isArray: true,
arraySize1: size1,
arraySize2: size2
};
}
// 尝试匹配纯数字格式
const numberMatch = dimStr.match(/^\d+$/);
if (numberMatch) {
const size = parseInt(numberMatch[0]);
return {
isArray: true,
arraySize1: size,
arraySize2: 0
};
}
// 如果都不匹配,则不是数组
return { isArray: false, arraySize1: 0, arraySize2: 0 };
}
/**
* 解析单个工作表
* @param {Object} worksheet - Excel工作表对象
* @param {string} structName - 结构体名称input/output
* @returns {Array} 解析后的数据数组
*/
function parseWorksheet(worksheet, structName) {
const data = XLSX.utils.sheet_to_json(worksheet);
return data.map(row => {
const dimensions = parseArrayDimensions(row['Variable Dimensions']);
return {
name: row['Input Variable Name'] || row['Output Variable Name'] || '',
description: row['Description'] || '',
structName: structName,
isArray: dimensions.isArray,
arraySize1: dimensions.arraySize1,
arraySize2: dimensions.arraySize2,
variableType: row['Variable Type'] || ''
};
});
}
/**
* 解析ICD Excel文件
* @param {string} filePath - Excel文件路径
* @returns {Promise<Array>} 解析后的数据数组
*/
async function parseICDFile(filePath) {
try {
const workbook = XLSX.readFile(filePath);
// 检查必要的工作表是否存在
if (!workbook.SheetNames.includes('Inputs') || !workbook.SheetNames.includes('Outputs')) {
throw new Error('ICD文件必须包含Inputs和Outputs两个工作表');
}
// 解析input工作表
const inputData = parseWorksheet(workbook.Sheets['Inputs'], 'input');
// 解析output工作表
const outputData = parseWorksheet(workbook.Sheets['Outputs'], 'output');
// 合并两个工作表的数据
return [...inputData, ...outputData];
} catch (error) {
console.error('解析ICD文件时出错:', error);
throw new Error('解析ICD文件失败: ' + error.message);
}
}
module.exports = {
parseICDFile
};