diff --git a/Release/database/XNSim.db b/Release/database/XNSim.db index fb097dc..ce1090b 100644 Binary files a/Release/database/XNSim.db and b/Release/database/XNSim.db differ diff --git a/XNSimHtml/assets/icons/png/download.png b/XNSimHtml/assets/icons/png/download.png new file mode 100644 index 0000000..c7a4392 Binary files /dev/null and b/XNSimHtml/assets/icons/png/download.png differ diff --git a/XNSimHtml/assets/icons/png/download_b.png b/XNSimHtml/assets/icons/png/download_b.png new file mode 100644 index 0000000..9d064f3 Binary files /dev/null and b/XNSimHtml/assets/icons/png/download_b.png differ diff --git a/XNSimHtml/components/FloatingChartWindow.js b/XNSimHtml/components/FloatingChartWindow.js index 630b869..09ca628 100644 --- a/XNSimHtml/components/FloatingChartWindow.js +++ b/XNSimHtml/components/FloatingChartWindow.js @@ -414,30 +414,27 @@ class FloatingChartWindow extends HTMLElement { handleMouseMove(e) { if (this.isDragging) { - // 限制拖动范围在当前页面区域 - const minX = 300; // 左侧工具栏宽度 - const minY = 100; // 顶部标签页栏高度 + // 获取约束 + const constraints = this.getAttribute('constraints') ? + JSON.parse(this.getAttribute('constraints')) : { + minX: 0, + maxX: window.innerWidth, + minY: 0, + maxY: window.innerHeight + }; - this.position = { - x: Math.max(minX, e.clientX - this.dragOffset.x), - y: Math.max(minY, e.clientY - this.dragOffset.y) - }; + // 计算新位置 + let newX = e.clientX - this.dragOffset.x; + let newY = e.clientY - this.dragOffset.y; + + // 应用约束 + newX = Math.max(constraints.minX, Math.min(constraints.maxX - this.size.width, newX)); + newY = Math.max(constraints.minY, Math.min(constraints.maxY - this.size.height, newY)); + + this.position = { x: newX, y: newY }; this.updatePosition(); } else if (this.isResizing) { - const deltaX = e.clientX - this.resizeStart.x; - const deltaY = e.clientY - this.resizeStart.y; - - this.size = { - width: Math.max(300, this.size.width + deltaX), - height: Math.max(200, this.size.height + deltaY) - }; - - this.resizeStart = { - x: e.clientX, - y: e.clientY - }; - - this.updateSize(); + this.handleResize(e); } } @@ -456,12 +453,29 @@ class FloatingChartWindow extends HTMLElement { handleResize(e) { if (this.isResizing) { + // 获取约束 + const constraints = this.getAttribute('constraints') ? + JSON.parse(this.getAttribute('constraints')) : { + minX: 0, + maxX: window.innerWidth, + minY: 0, + maxY: window.innerHeight + }; + const deltaX = e.clientX - this.resizeStart.x; const deltaY = e.clientY - this.resizeStart.y; + // 计算新尺寸 + let newWidth = Math.max(300, this.size.width + deltaX); + let newHeight = Math.max(200, this.size.height + deltaY); + + // 应用约束 + newWidth = Math.min(newWidth, constraints.maxX - this.position.x); + newHeight = Math.min(newHeight, constraints.maxY - this.position.y); + this.size = { - width: Math.max(300, this.size.width + deltaX), - height: Math.max(200, this.size.height + deltaY) + width: newWidth, + height: newHeight }; this.resizeStart = { diff --git a/XNSimHtml/components/data-collection.js b/XNSimHtml/components/data-collection.js index 1ddfaa0..bf7b0f3 100644 --- a/XNSimHtml/components/data-collection.js +++ b/XNSimHtml/components/data-collection.js @@ -9,9 +9,15 @@ class DataCollection extends HTMLElement { this.outputFile = null; // 存储输出文件名 this.collectStatus = 0; // 0-未加载脚本、1-已加载脚本、2-数据采集中、3-采集完成 this.isActive = false; // 组件是否激活 + this.chartWindows = new Map(); // 存储打开的图表窗口 // 保存事件处理函数的引用 this.handleClick = (e) => { + const btn = e.target.closest('#downloadBtn'); + if (btn) { + this.handleDownload(); + return; + } if (e.target.id === 'loadScriptBtn') { this.handleLoadScript(); } else if (e.target.id === 'startCollectBtn') { @@ -24,6 +30,13 @@ class DataCollection extends HTMLElement { this.handleFileSelect(e); } }; + + // 确保 FloatingChartWindow 组件已注册 + if (!customElements.get('floating-chart-window')) { + const script = document.createElement('script'); + script.src = './components/FloatingChartWindow.js'; + document.head.appendChild(script); + } } getCurrentSelection() { @@ -73,23 +86,47 @@ class DataCollection extends HTMLElement { } this.statusTimer = setInterval(async () => { try { - const res = await fetch('/api/dds-monitor/status'); - if (!res.ok) throw new Error('网络错误'); - const data = await res.json(); - if (data.isInitialized) { + // 检查监控状态 + const monitorRes = await fetch('/api/dds-monitor/status'); + if (!monitorRes.ok) throw new Error('网络错误'); + const monitorData = await monitorRes.json(); + if (monitorData.isInitialized) { this.monitorStatus = 1; } else { this.monitorStatus = 0; } + + // 如果正在采集中,检查采集状态 + if (this.collectStatus === 2) { + const collectRes = await fetch('/api/data-collect/status'); + if (!collectRes.ok) throw new Error('网络错误'); + const collectData = await collectRes.json(); + if (collectData.status === 0) { // 采集完成 + // 模拟点击停止采集按钮 + const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn'); + if (startCollectBtn) { + startCollectBtn.click(); + } + } + } + + this.updateMonitorStatus(); } catch (e) { this.monitorStatus = 2; } - this.updateMonitorStatus(); }, 1000); } disconnectedCallback() { this.isActive = false; + // 清理所有图表窗口 + this.chartWindows.forEach((window, windowId) => { + // 触发关闭事件,确保所有清理工作都完成 + window.dispatchEvent(new CustomEvent('close')); + window.remove(); + }); + this.chartWindows.clear(); + // 组件销毁时清理定时器 if (this.statusTimer) { clearInterval(this.statusTimer); this.statusTimer = null; @@ -162,21 +199,15 @@ class DataCollection extends HTMLElement { // 清空输入框 const scriptFileInput = this.shadowRoot.getElementById('scriptFileInput'); - const collectFreqInput = this.shadowRoot.getElementById('collectFreqInput'); - const collectDurationInput = this.shadowRoot.getElementById('collectDurationInput'); const outputFileInput = this.shadowRoot.getElementById('outputFileInput'); const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn'); + const downloadBtn = this.shadowRoot.getElementById('downloadBtn'); scriptFileInput.value = ''; - collectFreqInput.value = '100'; - collectDurationInput.value = '60'; outputFileInput.value = ''; - - // 启用输入框 - collectFreqInput.disabled = false; - collectDurationInput.disabled = false; - outputFileInput.disabled = false; startCollectBtn.disabled = true; + // 禁用下载按钮 + if (downloadBtn) downloadBtn.disabled = true; // 更新按钮文本 const loadScriptBtn = this.shadowRoot.getElementById('loadScriptBtn'); @@ -226,7 +257,7 @@ class DataCollection extends HTMLElement { const lines = fileContent.split('\n'); let duration = 60; // 默认值 - let frequency = 60; // 默认值 + let frequency = 100; // 默认值 let outputFile = 'collect_result.csv'; // 默认值 let structData = {}; // 存储结构体数据 let missingInterfaces = []; // 存储不存在的接口 @@ -332,6 +363,9 @@ class DataCollection extends HTMLElement { if (matches) { duration = parseInt(matches[1]); frequency = parseInt(matches[2]); + // 保存采集参数 + this.collectDuration = duration; + this.collectFreq = frequency; } } @@ -364,15 +398,12 @@ class DataCollection extends HTMLElement { if (collectFreqInput) { collectFreqInput.value = frequency; - collectFreqInput.disabled = true; } if (collectDurationInput) { collectDurationInput.value = duration; - collectDurationInput.disabled = true; } if (outputFileInput) { outputFileInput.value = outputFile; - outputFileInput.disabled = true; } if (startCollectBtn) { startCollectBtn.disabled = false; @@ -385,13 +416,6 @@ class DataCollection extends HTMLElement { // 存储结构体数据供后续使用 this.structData = structData; this.outputFile = outputFile; // 保存输出文件名 - - console.log('文件解析成功:', { - duration, - frequency, - outputFile, - structData - }); // 重新渲染组件以显示采集列表 this.render(); @@ -501,14 +525,16 @@ class DataCollection extends HTMLElement { color: #fff; font-size: 14px; cursor: pointer; - transition: background 0.2s; + transition: all 0.3s; } .action-btn:hover { background: #40a9ff; } .action-btn:disabled { background: #d9d9d9; + color: rgba(0, 0, 0, 0.25); cursor: not-allowed; + box-shadow: none; } /* 卸载脚本按钮样式 */ .action-btn.unload { @@ -517,6 +543,10 @@ class DataCollection extends HTMLElement { .action-btn.unload:hover { background: #ff7875; } + .action-btn.unload:disabled { + background: #d9d9d9; + color: rgba(0, 0, 0, 0.25); + } /* 停止采集按钮样式 */ .action-btn.stop { background: #faad14; @@ -524,6 +554,29 @@ class DataCollection extends HTMLElement { .action-btn.stop:hover { background: #ffc53d; } + .action-btn.stop:disabled { + background: #d9d9d9; + color: rgba(0, 0, 0, 0.25); + } + /* 图标按钮样式 */ + .action-btn.icon-btn { + background: none; + border: none; + padding: 0 2px; + width: 28px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + margin-left: 0; + } + .action-btn.icon-btn:disabled { + opacity: 0.4; + } + .action-btn.icon-btn:hover:not(:disabled) { + background: #f0f0f0; + border-radius: 4px; + } .input-row { display: flex; flex-direction: column; @@ -536,6 +589,12 @@ class DataCollection extends HTMLElement { min-width: 0; width: 100%; } + .input-group > div { + display: flex; + align-items: center; + gap: 4px; + width: 100%; + } .input-label { font-size: 13px; color: #666; @@ -549,7 +608,6 @@ class DataCollection extends HTMLElement { align-items: center; } .input-box { - flex: 1; width: 100%; padding: 0 8px; border: 1px solid #d9d9d9; @@ -678,15 +736,20 @@ class DataCollection extends HTMLElement {
采集频率 (Hz)
- +
采集时长 (秒)
- +
输出文件
- +
+ + +
@@ -700,8 +763,9 @@ class DataCollection extends HTMLElement {
${Object.entries(interfaces).map(([interfaceName, [size1, size2]]) => ` -
+
${interfaceName} + ${size1 > 0 ? `[${size1}${size2 > 0 ? `][${size2}` : ''}]` : ''}
`).join('')}
@@ -719,6 +783,16 @@ class DataCollection extends HTMLElement { // 更新监控状态 this.updateMonitorStatus(); + + // 在树型控件部分添加双击事件处理 + this.shadowRoot.querySelectorAll('.interface-item').forEach(itemEl => { + itemEl.ondblclick = (e) => { + const name = itemEl.getAttribute('data-interfacename'); + const struct = itemEl.getAttribute('data-modelstructname'); + console.log('双击接口:', name, struct); // 添加调试日志 + this.handleInterfaceDblClick(name, struct); + }; + }); } // 重新激活组件 @@ -728,10 +802,132 @@ class DataCollection extends HTMLElement { this.startStatusTimer(); } + async loadCollectResult() { + try { + const response = await fetch('/api/filesystem/read', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + filename: 'collect_result.csv' + }) + }); + + if (!response.ok) { + throw new Error(`读取文件失败: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + if (!result.success) { + throw new Error(result.message || '读取文件失败'); + } + + // 解析CSV数据 + const csvData = result.data; + if (!csvData) { + throw new Error('CSV数据为空'); + } + + const lines = csvData.split('\n'); + if (lines.length < 2) { + throw new Error('CSV文件格式错误:数据行数不足'); + } + + // 获取接口名称(第一行)并解析数组索引 + const interfaceNames = lines[0].split(',').map(name => { + const trimmedName = name.trim(); + // 匹配接口名称和数组索引 + const match = trimmedName.match(/^(.+?)\((\d+)(?:_(\d+))?\)$/); + if (match) { + return { + fullName: trimmedName, + baseName: match[1], + index1: parseInt(match[2], 10) - 1, // 转换为0基索引 + index2: match[3] ? parseInt(match[3], 10) - 1 : null // 转换为0基索引 + }; + } + return { + fullName: trimmedName, + baseName: trimmedName, + index1: null, + index2: null + }; + }); + + // 按接口名称整理数据 + const organizedData = {}; + + // 处理数据行 + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(',').map(value => parseFloat(value.trim())); + if (values.length === interfaceNames.length) { + // 第一列是时间 + const time = values[0]; + + interfaceNames.forEach(({ fullName, baseName, index1, index2 }, valueIndex) => { + if (valueIndex === 0) return; // 跳过时间列 + + if (index1 !== null) { + // 数组接口 + if (!organizedData[baseName]) { + organizedData[baseName] = { + isArray: true, + data: [], + times: [] // 添加时间数组 + }; + } + if (index2 !== null) { + // 二维数组 + if (!organizedData[baseName].data[index1]) { + organizedData[baseName].data[index1] = []; + } + if (!organizedData[baseName].data[index1][index2]) { + organizedData[baseName].data[index1][index2] = []; + } + organizedData[baseName].data[index1][index2].push(values[valueIndex]); + } else { + // 一维数组 + if (!organizedData[baseName].data[index1]) { + organizedData[baseName].data[index1] = []; + } + organizedData[baseName].data[index1].push(values[valueIndex]); + } + } else { + // 非数组接口 + if (!organizedData[fullName]) { + organizedData[fullName] = { + isArray: false, + data: [], + times: [] // 添加时间数组 + }; + } + organizedData[fullName].data.push(values[valueIndex]); + + // 添加时间到对应的接口数据中 + if (!organizedData[fullName].times.includes(time)) { + organizedData[fullName].times.push(time); + } + } + + // 添加时间到数组接口数据中 + if (index1 !== null && !organizedData[baseName].times.includes(time)) { + organizedData[baseName].times.push(time); + } + }); + } + } + + return organizedData; + } catch (error) { + console.error('加载采集结果失败:', error); + throw error; + } + } + async handleStartCollect() { // 检查监控状态 if (this.monitorStatus !== 1) { - alert('请先启动监控'); return; } @@ -742,6 +938,8 @@ class DataCollection extends HTMLElement { } const startCollectBtn = this.shadowRoot.getElementById('startCollectBtn'); + const loadScriptBtn = this.shadowRoot.getElementById('loadScriptBtn'); + const downloadBtn = this.shadowRoot.getElementById('downloadBtn'); // 如果正在采集,则停止采集 if (this.collectStatus === 2) { @@ -754,11 +952,25 @@ class DataCollection extends HTMLElement { if (result.success) { // 更新采集状态 - this.collectStatus = 1; // 改为已加载脚本状态 + this.collectStatus = 3; // 改为采集完成状态 // 更新按钮状态 startCollectBtn.textContent = '开始采集'; startCollectBtn.disabled = false; startCollectBtn.classList.remove('stop'); + // 启用卸载脚本按钮 + loadScriptBtn.disabled = false; + // 启用下载按钮 + if (downloadBtn) downloadBtn.disabled = false; + + // 加载采集结果 + try { + const collectData = await this.loadCollectResult(); + // 存储采集数据 + this.collectData = collectData; + } catch (error) { + console.error('加载采集数据失败:', error); + alert('加载采集数据失败: ' + error.message); + } } else { throw new Error(result.message); } @@ -791,6 +1003,10 @@ class DataCollection extends HTMLElement { // 更新按钮状态 startCollectBtn.textContent = '停止采集'; startCollectBtn.classList.add('stop'); + // 禁用卸载脚本按钮 + loadScriptBtn.disabled = true; + // 禁用下载按钮 + if (downloadBtn) downloadBtn.disabled = true; } else { throw new Error(result.message); } @@ -799,6 +1015,238 @@ class DataCollection extends HTMLElement { alert('启动采集失败: ' + error.message); } } + + async handleInterfaceDblClick(interfaceName, modelStructName) { + // 只有在采集完成状态才允许绘图 + if (this.collectStatus !== 3) { + return; + } + + // 检查是否已经存在该接口的图表窗口 + const windowId = `${interfaceName}_${modelStructName}`; + if (this.chartWindows.has(windowId)) { + // 如果窗口已存在,将其置顶 + const window = this.chartWindows.get(windowId); + window.setAttribute('z-index', Date.now()); + return; + } + + try { + // 获取左侧面板和工具栏的位置信息 + const leftPanel = this.shadowRoot.querySelector('.left-panel'); + const toolbar = this.shadowRoot.querySelector('.toolbar'); + const leftPanelRect = leftPanel.getBoundingClientRect(); + const toolbarRect = toolbar.getBoundingClientRect(); + + // 计算可用区域 + const minX = leftPanelRect.right; + const maxX = window.innerWidth; + const minY = toolbarRect.bottom; + const maxY = window.innerHeight; + + // 创建图表数据 + const chartData = { + labels: [], + datasets: [] + }; + + // 创建图表配置 + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + animation: false, + elements: { + point: { + radius: 0 + }, + line: { + tension: 0 + } + }, + scales: { + y: { + beginAtZero: false, + display: true, + ticks: { + callback: function(value) { + if (Math.abs(value) > 1000 || (Math.abs(value) < 0.1 && value !== 0)) { + return value.toExponential(1); + } + return value.toFixed(1); + }, + maxTicksLimit: 8 + } + }, + x: { + display: true, + type: 'linear', + ticks: { + callback: function(value) { + return value.toFixed(1); + } + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + }, + tooltip: { + enabled: true, + mode: 'index', + intersect: false, + callbacks: { + label: function(context) { + console.log('Tooltip context:', context); + console.log('Raw data:', context.raw); + + if (!context.raw) { + console.log('No raw data'); + return `${context.dataset.label}: N/A`; + } + + if (typeof context.raw.y === 'undefined') { + console.log('No y value in raw data'); + return `${context.dataset.label}: N/A`; + } + + const value = context.raw.y; + console.log('Value type:', typeof value, 'Value:', value); + + if (typeof value !== 'number') { + console.log('Value is not a number'); + return `${context.dataset.label}: N/A`; + } + + return `${context.dataset.label}: ${value.toFixed(1)}`; + } + } + } + } + }; + + // 创建浮动窗口组件 + const floatingWindow = document.createElement('floating-chart-window'); + if (!floatingWindow) { + throw new Error('创建浮动窗口组件失败'); + } + + // 添加数据点计数器 + floatingWindow.dataPointIndex = 0; + + // 先添加到页面 + document.body.appendChild(floatingWindow); + this.chartWindows.set(windowId, floatingWindow); + + // 设置浮动窗口的约束 + floatingWindow.setAttribute('constraints', JSON.stringify({ + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY + })); + + // 再设置属性 + floatingWindow.setAttribute('title', interfaceName); + floatingWindow.setAttribute('initial-position', JSON.stringify({ + x: minX + 20, // 在左侧面板右侧留出20px的间距 + y: minY + 20 // 在工具栏下方留出20px的间距 + })); + floatingWindow.setAttribute('initial-size', JSON.stringify({ width: 400, height: 300 })); + floatingWindow.setAttribute('chart-data', JSON.stringify(chartData)); + floatingWindow.setAttribute('chart-options', JSON.stringify(chartOptions)); + + // 更新图表数据 + if (this.collectData && this.collectData[interfaceName]) { + const interfaceData = this.collectData[interfaceName]; + if (interfaceData.isArray) { + // 数组接口 + if (interfaceData.data[0] && Array.isArray(interfaceData.data[0][0])) { + // 二维数组 + const datasets = []; + interfaceData.data.forEach((row, i) => { + row.forEach((values, j) => { + if (values && values.length > 0) { + datasets.push({ + label: `${interfaceName}[${i+1}][${j+1}]`, + data: values, + borderColor: `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`, + tension: 0 + }); + } + }); + }); + // 更新图表数据 + floatingWindow.updateChartData({ + labels: interfaceData.times, + datasets: datasets + }); + } else { + // 一维数组 + const datasets = []; + interfaceData.data.forEach((values, i) => { + if (values && values.length > 0) { + datasets.push({ + label: `${interfaceName}[${i+1}]`, + data: values, + borderColor: `rgb(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)})`, + tension: 0 + }); + } + }); + // 更新图表数据 + floatingWindow.updateChartData({ + labels: interfaceData.times, + datasets: datasets + }); + } + } else { + // 非数组接口 + if (interfaceData.data && interfaceData.data.length > 0) { + // 更新图表数据 + floatingWindow.updateChartData({ + labels: interfaceData.times, + datasets: [{ + label: interfaceName, + data: interfaceData.data, + borderColor: 'rgb(75, 192, 192)', + tension: 0 + }] + }); + } + } + } + + // 添加关闭事件处理 + floatingWindow.addEventListener('close', () => { + this.chartWindows.delete(windowId); + }); + } catch (error) { + console.error('创建图表窗口失败:', error); + alert('创建图表窗口失败,请查看控制台了解详情'); + } + } + + async handleDownload() { + if (!this.outputFile) { + alert('没有可下载的文件'); + return; + } + + try { + // 创建一个隐藏的a标签用于下载 + const link = document.createElement('a'); + link.href = `/api/filesystem/download?filename=${encodeURIComponent(this.outputFile)}`; + link.download = this.outputFile; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch (error) { + console.error('下载文件失败:', error); + alert('下载文件失败: ' + error.message); + } + } } customElements.define('data-collection', DataCollection); \ No newline at end of file diff --git a/XNSimHtml/routes/filesystem.js b/XNSimHtml/routes/filesystem.js index 84ceb77..051f992 100644 --- a/XNSimHtml/routes/filesystem.js +++ b/XNSimHtml/routes/filesystem.js @@ -358,4 +358,107 @@ router.get('/validate-csv-headers', async (req, res) => { } }); +// 读取CSV文件内容 +router.post('/read', async (req, res) => { + try { + const { filename } = req.body; + if (!filename) { + return res.status(400).json({ + success: false, + message: '未提供文件名' + }); + } + + // 获取上传目录路径 + const uploadPath = await getUploadPath(); + const filePath = path.join(uploadPath, filename); + + // 检查文件是否存在 + try { + await fsPromises.access(filePath); + } catch (error) { + return res.status(404).json({ + success: false, + message: '文件不存在' + }); + } + + // 读取文件内容 + const content = await fsPromises.readFile(filePath, 'utf8'); + + res.json({ + success: true, + data: content + }); + } catch (error) { + console.error('读取CSV文件失败:', error); + res.status(500).json({ + success: false, + message: '读取CSV文件失败: ' + error.message + }); + } +}); + +// 文件下载 +router.get('/download', async (req, res) => { + try { + const { filename } = req.query; + if (!filename) { + return res.status(400).json({ + success: false, + message: '未提供文件名' + }); + } + + // 获取上传目录路径 + const uploadPath = await getUploadPath(); + const filePath = path.join(uploadPath, filename); + + // 安全检查:确保文件在上传目录内 + if (!filePath.startsWith(uploadPath)) { + return res.status(403).json({ + success: false, + message: '无权访问该文件' + }); + } + + // 检查文件是否存在 + try { + await fsPromises.access(filePath); + } catch (error) { + return res.status(404).json({ + success: false, + message: '文件不存在' + }); + } + + // 设置响应头 + res.setHeader('Content-Disposition', `attachment; filename=${encodeURIComponent(filename)}`); + res.setHeader('Content-Type', 'application/octet-stream'); + + // 创建文件流并发送 + const fileStream = fs.createReadStream(filePath); + fileStream.pipe(res); + + // 处理流错误 + fileStream.on('error', (error) => { + console.error('文件下载失败:', error); + if (!res.headersSent) { + res.status(500).json({ + success: false, + message: '文件下载失败: ' + error.message + }); + } + }); + } catch (error) { + console.error('文件下载失败:', error); + if (!res.headersSent) { + res.status(500).json({ + success: false, + message: '文件下载失败: ' + error.message + }); + } + } +}); + module.exports = router; \ No newline at end of file