diff --git a/Reject.html b/Reject.html index 81dd30a..8f7739f 100644 --- a/Reject.html +++ b/Reject.html @@ -3425,7 +3425,7 @@ function showImageViewer(imgElement) {
- 大图查看 + 大图查看
diff --git a/supply.html b/supply.html index 1b02b8c..591ca99 100644 --- a/supply.html +++ b/supply.html @@ -1344,7 +1344,7 @@ - + @@ -2058,58 +2058,73 @@ if (supply) { console.log('找到对应的货源:', supply.id, supply.productName); - // 计算下架时间:updated_at + autoOfflineHours(使用毫秒数计算,更精确) - const updatedAt = new Date(supply.updated_at || supply.created_at); - console.log('updatedAt:', updatedAt, 'supply.updated_at:', supply.updated_at, 'supply.created_at:', supply.created_at); - const autoOfflineHours = parseFloat(supply.autoOfflineHours) || 24; // 默认24小时,转换为数字 - console.log('autoOfflineHours:', autoOfflineHours, 'supply.autoOfflineHours:', supply.autoOfflineHours); - // 将小时转换为毫秒,使用setTime方法更精确 - const autoOfflineMs = autoOfflineHours * 60 * 60 * 1000; - const offlineTime = new Date(updatedAt.getTime() + autoOfflineMs); - console.log('offlineTime:', offlineTime); - const now = new Date(); - console.log('当前时间:', now); - const timeDiff = offlineTime - now; - console.log('时间差:', timeDiff, '毫秒'); - - if (timeDiff > 0) { - // 计算剩余下架时间 - const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000); + // 检查是否有显式设置的自动下架时间 + if (supply.autoOfflineHours && supply.autoOfflineHours !== '' && supply.autoOfflineHours !== null) { + console.log('有显式设置的自动下架时间'); + // 计算下架时间:updated_at + autoOfflineHours(使用毫秒数计算,更精确) + const updatedAt = new Date(supply.updated_at || supply.created_at); + console.log('updatedAt:', updatedAt, 'supply.updated_at:', supply.updated_at, 'supply.created_at:', supply.created_at); + const autoOfflineHours = parseFloat(supply.autoOfflineHours); + console.log('autoOfflineHours:', autoOfflineHours, 'supply.autoOfflineHours:', supply.autoOfflineHours); + // 将小时转换为毫秒,使用setTime方法更精确 + const autoOfflineMs = autoOfflineHours * 60 * 60 * 1000; + const offlineTime = new Date(updatedAt.getTime() + autoOfflineMs); + console.log('offlineTime:', offlineTime); - console.log('剩余下架时间:', days, '天', hours, '小时', minutes, '分钟', seconds, '秒'); + const now = new Date(); + console.log('当前时间:', now); + const timeDiff = offlineTime - now; + console.log('时间差:', timeDiff, '毫秒'); - // 更新显示 - let countdownText = ''; - if (days > 0) { - countdownText = `${days}天${hours}时${minutes}分`; - } else if (hours > 0) { - countdownText = `${hours}时${minutes}分`; + if (timeDiff > 0) { + // 计算剩余下架时间 + const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000); + + console.log('剩余下架时间:', days, '天', hours, '小时', minutes, '分钟', seconds, '秒'); + + // 更新显示 + let countdownText = ''; + if (days > 0) { + countdownText = `${days}天${hours}时${minutes}分`; + } else if (hours > 0) { + countdownText = `${hours}时${minutes}分`; + } else { + countdownText = `${minutes}分${seconds}秒`; + } + + // 根据元素类型设置不同的显示格式 + if (element.classList.contains('countdown-badge')) { + element.innerHTML = `⏰ ${countdownText}`; + element.style.background = 'linear-gradient(135deg, #ff6b6b, #ee5a6f)'; + element.style.boxShadow = '0 2px 4px rgba(255, 107, 107, 0.3)'; + } else if (element.classList.contains('countdown')) { + element.innerHTML = `剩余下架时间: ${countdownText}`; + } + console.log('更新倒计时显示为:', countdownText); } else { - countdownText = `${minutes}分${seconds}秒`; - } - - // 根据元素类型设置不同的显示格式 - if (element.classList.contains('countdown-badge')) { - element.innerHTML = `⏰ ${countdownText}`; - element.style.background = 'linear-gradient(135deg, #ff6b6b, #ee5a6f)'; - element.style.boxShadow = '0 2px 4px rgba(255, 107, 107, 0.3)'; - } else if (element.classList.contains('countdown')) { - element.innerHTML = `剩余下架时间: ${countdownText}`; + // 时间到了 + console.log('时间已到,显示已下架'); + if (element.classList.contains('countdown-badge')) { + element.innerHTML = '⏰ 已下架'; + element.style.background = '#8c8c8c'; + element.style.boxShadow = '0 2px 4px rgba(140, 140, 140, 0.3)'; + } else if (element.classList.contains('countdown')) { + element.innerHTML = '剩余下架时间: 已下架'; + } } - console.log('更新倒计时显示为:', countdownText); } else { - // 时间到了 - console.log('时间已到,显示已下架'); + // 没有设置自动下架时间,隐藏倒计时或显示无倒计时 + console.log('没有设置自动下架时间,隐藏倒计时'); if (element.classList.contains('countdown-badge')) { - element.innerHTML = '⏰ 已下架'; - element.style.background = '#8c8c8c'; - element.style.boxShadow = '0 2px 4px rgba(140, 140, 140, 0.3)'; + element.innerHTML = '⏰ 无倒计时'; + element.style.background = '#52c41a'; + element.style.boxShadow = '0 2px 4px rgba(82, 196, 26, 0.3)'; } else if (element.classList.contains('countdown')) { - element.innerHTML = '剩余下架时间: 已下架'; + element.innerHTML = '剩余下架时间: 无'; } } } else { @@ -2141,19 +2156,26 @@ // 检查所有状态为published的货源,不管前端显示什么 if (supply.status === 'published') { console.log('处理上架状态的货源:', supply.id); - // 计算下架时间:updated_at + autoOfflineHours(使用毫秒数计算,更精确) - const updatedAt = new Date(supply.updated_at || supply.created_at); - const autoOfflineHours = parseFloat(supply.autoOfflineHours) || 24; // 默认24小时,转换为数字 - console.log('autoOfflineHours:', autoOfflineHours); - // 将小时转换为毫秒,使用setTime方法更精确 - const autoOfflineMs = autoOfflineHours * 60 * 60 * 1000; - const offlineTime = new Date(updatedAt.getTime() + autoOfflineMs); - console.log('计算的下架时间:', offlineTime, '当前时间:', now); - if (now >= offlineTime) { - // 时间到了,修改状态 - console.log('时间到了,调用updateSupplyStatus:', supply.id, '从published改为sold_out'); - await updateSupplyStatus(supply.id, 'sold_out'); + // 只有在显式设置了自动下架时间的情况下才应用自动下架逻辑 + if (supply.autoOfflineHours && supply.autoOfflineHours !== '' && supply.autoOfflineHours !== null) { + console.log('有显式设置的自动下架时间'); + // 计算下架时间:updated_at + autoOfflineHours(使用毫秒数计算,更精确) + const updatedAt = new Date(supply.updated_at || supply.created_at); + const autoOfflineHours = parseFloat(supply.autoOfflineHours); // 只转换,不设置默认值 + console.log('autoOfflineHours:', autoOfflineHours); + // 将小时转换为毫秒,使用setTime方法更精确 + const autoOfflineMs = autoOfflineHours * 60 * 60 * 1000; + const offlineTime = new Date(updatedAt.getTime() + autoOfflineMs); + console.log('计算的下架时间:', offlineTime, '当前时间:', now); + + if (now >= offlineTime) { + // 时间到了,修改状态 + console.log('时间到了,调用updateSupplyStatus:', supply.id, '从published改为sold_out'); + await updateSupplyStatus(supply.id, 'sold_out'); + } + } else { + console.log('没有显式设置自动下架时间,跳过自动下架检查'); } } } @@ -2222,29 +2244,89 @@ } }); + // 编辑页面拖拽上传事件 + const editUploadArea = document.getElementById('editUploadImages'); + if (editUploadArea) { + editUploadArea.addEventListener('dragenter', (e) => { + e.preventDefault(); + e.stopPropagation(); + editUploadArea.style.border = '2px dashed #1677ff'; + editUploadArea.style.backgroundColor = '#f0f8ff'; + editUploadArea.style.padding = '20px'; + editUploadArea.style.borderRadius = '8px'; + }); + + editUploadArea.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + + editUploadArea.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + editUploadArea.style.border = 'none'; + editUploadArea.style.backgroundColor = 'transparent'; + editUploadArea.style.padding = '0'; + }); + + editUploadArea.addEventListener('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + editUploadArea.style.border = 'none'; + editUploadArea.style.backgroundColor = 'transparent'; + editUploadArea.style.padding = '0'; + + const files = Array.from(e.dataTransfer.files); + // 过滤出图片文件 + const imageFiles = files.filter(file => file.type.startsWith('image/')); + + if (imageFiles.length > 0) { + handleEditDroppedImages(imageFiles); + } + }); + } + // 复制粘贴上传事件 document.addEventListener('paste', (e) => { // 检查是否在创建货源模态框中 const createSupplyModal = document.getElementById('createSupplyModal'); - if (createSupplyModal.style.display !== 'flex') { - return; - } + const editSupplyModal = document.getElementById('editSupplyModal'); + + // 检查是否在创建或编辑货源模态框中 + if (createSupplyModal && createSupplyModal.style.display === 'flex') { + // 检查是否有粘贴的图片 + const items = e.clipboardData.items; + const imageFiles = []; + + for (let i = 0; i < items.length; i++) { + if (items[i].type.startsWith('image/')) { + const file = items[i].getAsFile(); + if (file) { + imageFiles.push(file); + } + } + } - // 检查是否有粘贴的图片 - const items = e.clipboardData.items; - const imageFiles = []; + if (imageFiles.length > 0) { + handleDroppedImages(imageFiles); + } + } else if (editSupplyModal && editSupplyModal.style.display === 'flex') { + // 检查是否有粘贴的图片 + const items = e.clipboardData.items; + const imageFiles = []; - for (let i = 0; i < items.length; i++) { - if (items[i].type.startsWith('image/')) { - const file = items[i].getAsFile(); - if (file) { - imageFiles.push(file); + for (let i = 0; i < items.length; i++) { + if (items[i].type.startsWith('image/')) { + const file = items[i].getAsFile(); + if (file) { + imageFiles.push(file); + } } } - } - if (imageFiles.length > 0) { - handleDroppedImages(imageFiles); + if (imageFiles.length > 0) { + handleEditDroppedImages(imageFiles); + } } }); } @@ -2276,6 +2358,38 @@ } } + // 处理编辑页面拖拽或粘贴的图片 + async function handleEditDroppedImages(imageFiles) { + for (let i = 0; i < imageFiles.length; i++) { + if (editCurrentImages.length >= 2) { + alert('最多只能上传2张图片'); + break; + } + + const file = imageFiles[i]; + const reader = new FileReader(); + + reader.onload = async function(e) { + let imageUrl = e.target.result; + + try { + // 为图片添加水印 + imageUrl = await addWatermarkToImage(imageUrl); + + // 添加到当前编辑图片列表 + editCurrentImages.push(imageUrl); + // 更新显示 + updateEditImageDisplay(); + } catch (error) { + console.error('图片处理失败:', error); + alert('图片处理失败,请重试'); + } + }; + + reader.readAsDataURL(file); + } + } + // 规格选择功能 let allSpecOptions = ['净重47+', '净重46-47', '净重45-46', '净重44-45', '净重43-44', '净重42-43', '净重41-42', '净重40-41', '净重39-40', '净重38-39', '净重37-39', '净重37-38', '净重36-38', '净重36-37', '净重35-36', '净重34-35', '净重33-34', '净重32-33', '净重32-34', '净重31-32', '净重30-35', '净重30-34', '净重30-32', '净重30-31', '净重29-31', '净重29-30', '净重28-29', '净重28以下', '毛重52以上', '毛重50-51', '毛重48-49', '毛重47-48', '毛重46-47', '毛重45-47', '毛重45-46', '毛重44-45', '毛重43-44', '毛重42-43', '毛重41-42', '毛重40-41', '毛重38-39', '毛重36-37', '毛重34-35', '毛重32-33', '毛重30-31', '毛重30以下']; let filteredSpecOptions = [...allSpecOptions]; @@ -3603,28 +3717,29 @@ // 更新联系人选择下拉框 function updateContactSelects() { + // 更新创建表单的联系人选择框 const contactSelect = document.getElementById('contactId'); - if (!contactSelect) return; - - // 清空现有选项 - contactSelect.innerHTML = ''; - - // 添加默认选项 - const defaultOption = document.createElement('option'); - defaultOption.value = ''; - defaultOption.textContent = '请选择联系人'; - contactSelect.appendChild(defaultOption); - - // 添加联系人选项,只显示有电话号码的联系人 - contacts.forEach(contact => { - // 确保联系人有电话号码 - if (contact.phoneNumber && contact.phoneNumber.trim() !== '') { - const option = document.createElement('option'); - option.value = contact.id; - option.textContent = `${contact.name} (${contact.phoneNumber})`; - contactSelect.appendChild(option); - } - }); + if (contactSelect) { + // 清空现有选项 + contactSelect.innerHTML = ''; + + // 添加默认选项 + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = '请选择联系人'; + contactSelect.appendChild(defaultOption); + + // 添加联系人选项,只显示有电话号码的联系人 + contacts.forEach(contact => { + // 确保联系人有电话号码 + if (contact.phoneNumber && contact.phoneNumber.trim() !== '') { + const option = document.createElement('option'); + option.value = contact.id; + option.textContent = `${contact.name} (${contact.phoneNumber})`; + contactSelect.appendChild(option); + } + }); + } } // 处理窗口大小变化 @@ -3950,7 +4065,7 @@
${supply.productName} ${status.text} - ⏰ 计算中... + ${supply.autoOfflineHours && supply.autoOfflineHours !== '' && supply.autoOfflineHours !== null ? `⏰ 计算中...` : ''}
@@ -3962,7 +4077,7 @@
地区: ${supply.region || '未设置'}
价格: ¥${supply.price || '0'}
创建时间: ${formatDate(supply.created_at)}
-
剩余下架时间: 计算中...
+ ${supply.autoOfflineHours && supply.autoOfflineHours !== '' && supply.autoOfflineHours !== null ? `
剩余下架时间: 计算中...
` : ''}
@@ -4122,10 +4237,21 @@ // 货源状态 document.getElementById('supplyStatus').value = supply.supplyStatus || ''; - // 联系人 + // 联系人 - 使用contacts数组查找完整信息 document.getElementById('contactId').value = supply.contactId || ''; - document.getElementById('contactIdDisplayText').textContent = supply.product_contact || '请选择联系人'; selectedContactId = supply.contactId || ''; + if (supply.contactId) { + // 从contacts数组(可能包含本地缓存数据)中查找联系人 + const contact = contacts.find(c => String(c.id) === String(supply.contactId)); + if (contact) { + document.getElementById('contactIdDisplayText').textContent = `${contact.name} (${contact.phoneNumber})`; + } else { + // 如果找不到联系人,使用supply中的数据或默认值 + document.getElementById('contactIdDisplayText').textContent = supply.product_contact || `联系人ID: ${supply.contactId}` || '请选择联系人'; + } + } else { + document.getElementById('contactIdDisplayText').textContent = '请选择联系人'; + } // 产品包装 document.getElementById('productingValue').value = supply.producting || ''; @@ -5422,10 +5548,6 @@ } // 显示编辑货源 - function showEditSupply(id) { - alert('编辑功能开发中'); - } - // 删除货源 async function deleteSupply(id) { if (confirm('确定要删除该货源吗?删除后将无法恢复。')) { @@ -5528,12 +5650,21 @@ document.getElementById('editSupplyStatus').value = supplyStatus; setEditSupplyStatus(supplyStatus); - // 分配联系人 - // 显示联系人名称而不是ID - const contactName = supply.product_contact || supply.contactId || '请选择联系人'; - document.getElementById('editContactIdDisplayText').textContent = contactName; + // 分配联系人 - 使用contacts数组查找完整信息 document.getElementById('editContactId').value = supply.contactId || ''; editSelectedContactId = supply.contactId || ''; + if (supply.contactId) { + // 从contacts数组(可能包含本地缓存数据)中查找联系人 + const contact = contacts.find(c => String(c.id) === String(supply.contactId)); + if (contact) { + document.getElementById('editContactIdDisplayText').textContent = `${contact.name} (${contact.phoneNumber})`; + } else { + // 如果找不到联系人,使用supply中的数据或默认值 + document.getElementById('editContactIdDisplayText').textContent = supply.product_contact || `联系人ID: ${supply.contactId}` || '请选择联系人'; + } + } else { + document.getElementById('editContactIdDisplayText').textContent = '请选择联系人'; + } document.getElementById('editDescription').value = supply.description || ''; document.getElementById('editRegionDisplayText').textContent = supply.region || '请选择地区'; document.getElementById('editRegionValue').value = supply.region || ''; @@ -6575,13 +6706,6 @@ } // 隐藏编辑货源模态框 - function hideEditSupplyModal() { - document.getElementById('editSupplyModal').style.display = 'none'; - currentEditSupplyId = null; - // 重置发布模式状态 - window.currentEditPublishMode = false; - } - // 处理点击模态框外部关闭 window.onclick = function(event) { // 确保只处理click事件,不处理拖拽选择文本导致的mouseup事件 diff --git a/test/api.test.js b/test/api.test.js new file mode 100644 index 0000000..5ebac20 --- /dev/null +++ b/test/api.test.js @@ -0,0 +1,224 @@ +const request = require('supertest'); +const expect = require('chai').expect; +const app = require('../Reject.js'); + +// 测试API端点 +describe('API Endpoints', () => { + // 测试数据库连接 + describe('GET /api/test-db', () => { + it('should return database connection status', async () => { + const res = await request(app) + .get('/api/test-db') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(200); + expect(res.body).to.be.an('object'); + expect(res.body).to.have.property('success'); + }); + }); + + // 测试获取货源列表 + describe('GET /api/supplies', () => { + it('should return supplies list', async () => { + const res = await request(app) + .get('/api/supplies') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(200); + expect(res.body).to.be.an('object'); + expect(res.body).to.have.property('success'); + expect(res.body).to.have.property('data'); + }); + }); + + // 测试获取供应商列表 + describe('GET /api/suppliers', () => { + it('should return suppliers list', async () => { + const res = await request(app) + .get('/api/suppliers') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(200); + expect(res.body).to.be.an('object'); + expect(res.body).to.have.property('success'); + expect(res.body).to.have.property('data'); + }); + }); + + // 测试获取联系人列表 + describe('GET /api/contacts', () => { + it('should return contacts list', async () => { + const res = await request(app) + .get('/api/contacts') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(200); + expect(res.body).to.be.an('object'); + expect(res.body).to.have.property('success'); + expect(res.body).to.have.property('data'); + }); + }); + + // 测试获取审核失败原因 + describe('GET /api/supplies/:id/reject-reason', () => { + it('should return reject reason for supply', async () => { + // 首先获取一个货源ID + const suppliesRes = await request(app) + .get('/api/supplies') + .set('Accept', 'application/json'); + + if (suppliesRes.body.data && suppliesRes.body.data.length > 0) { + const supplyId = suppliesRes.body.data[0].id; + const res = await request(app) + .get(`/api/supplies/${supplyId}/reject-reason`) + .set('Accept', 'application/json'); + + expect(res.status).to.equal(200); + expect(res.body).to.be.an('object'); + expect(res.body).to.have.property('success'); + } + }); + }); + + // 测试获取拒绝审核API路由注册 + describe('GET /api/supplies/:id/reject', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/supplies/1/reject') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取供应商审核通过API路由注册 + describe('GET /api/suppliers/:id/approve', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/suppliers/1/approve') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取供应商审核拒绝API路由注册 + describe('GET /api/suppliers/:id/reject', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/suppliers/1/reject') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取供应商开始合作API路由注册 + describe('GET /api/suppliers/:id/cooperate', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/suppliers/1/cooperate') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取供应商终止合作API路由注册 + describe('GET /api/suppliers/:id/terminate', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/suppliers/1/terminate') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取创建货源API路由注册 + describe('GET /api/supplies/create', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/supplies/create') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取图片上传API路由注册 + describe('GET /api/upload-image', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/upload-image') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取下架货源API路由注册 + describe('GET /api/supplies/:id/unpublish', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/supplies/1/unpublish') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取上架货源API路由注册 + describe('GET /api/supplies/:id/publish', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/supplies/1/publish') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取删除货源API路由注册 + describe('GET /api/supplies/:id/delete', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/supplies/1/delete') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取更新货源状态API路由注册 + describe('GET /api/supplies/:id', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/supplies/1') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); + + // 测试获取编辑货源API路由注册 + describe('GET /api/supplies/:id/edit', () => { + it('should return method not allowed for GET', async () => { + const res = await request(app) + .get('/api/supplies/1/edit') + .set('Accept', 'application/json'); + + expect(res.status).to.equal(405); // Method Not Allowed + }); + }); +}); + +// 测试服务器启动 +describe('Server', () => { + it('should be listening on port 3000', () => { + // 这个测试主要验证app对象是否被正确导出 + expect(app).to.be.an('object'); + expect(app).to.have.property('listen'); + expect(app.listen).to.be.a('function'); + }); +});