diff --git a/antis-ncc-admin/src/views/lqTkjlb/Report.vue b/antis-ncc-admin/src/views/lqTkjlb/Report.vue index 721a356..5ee68a5 100644 --- a/antis-ncc-admin/src/views/lqTkjlb/Report.vue +++ b/antis-ncc-admin/src/views/lqTkjlb/Report.vue @@ -295,62 +295,37 @@ 暂无门店数据
- -
-
- 参与门店总数: - {{ storeData.length }}家 -
-
- 总目标数: - {{ getTotalStoreTarget() }} -
-
- 总完成数: - {{ getTotalStoreCompleted() }} -
-
- 总完成率: - {{ getTotalCompletionRate() }}% -
-
-
- - + + - - - - + + + + - + + + + -
@@ -776,15 +751,24 @@ export default { return rateB - rateA // 降序排列 }) }, - // 门店前三名 + // 门店前三名(按完成率排序) topStoreData() { return this.storeData.slice(0, 3) }, // 按完成率排序的门店数据(用于总门店排行榜) sortedStoreData() { - return [...this.storeData].sort((a, b) => { + const sorted = [...this.storeData].sort((a, b) => { return b.CompletionRate - a.CompletionRate // 按完成率降序排列 }) + // 添加总计行 + sorted.push({ + StoreName: '总计', + TotalTarget: this.getTotalStoreTarget(), + CompletedTarget: this.getTotalStoreCompleted(), + CompletionRate: this.getTotalCompletionRate(), + isTotal: true + }) + return sorted }, // 个人前三名 topPersonData() { @@ -1151,6 +1135,12 @@ export default { return date.toLocaleString('zh-CN') }, + // 格式化完成率,保留1位小数 + formatCompletionRate(rate) { + if (rate === null || rate === undefined) return '0.0' + return parseFloat(rate).toFixed(1) + }, + // 计算门店总目标 getStoreTotalTarget(store) { if (!store.TeamList) return 0 @@ -1259,7 +1249,7 @@ export default { const totalTarget = this.getTotalStoreTarget() const totalCompleted = this.getTotalStoreCompleted() if (totalTarget === 0) return 0 - return Math.round((totalCompleted / totalTarget) * 100) + return (totalCompleted / totalTarget) * 100 }, // 获取总完成率样式类 @@ -1512,28 +1502,40 @@ export default { header.style.backgroundColor = '#f8f9fa' }) - // 确保表格容器宽度足够 + // 确保表格容器和表格保持实际渲染宽度,避免被拉伸 const tableContainers = element.querySelectorAll('.store-ranking-table, .no-expansion-table, .store-table, .person-table') tableContainers.forEach(container => { - container.style.width = '100%' - container.style.minWidth = '100%' - container.style.maxWidth = 'none' + // 保持表格容器的原始宽度,避免被拉伸 + const originalWidth = container.offsetWidth || container.clientWidth + if (originalWidth) { + container.style.width = originalWidth + 'px' + container.style.minWidth = originalWidth + 'px' + container.style.maxWidth = originalWidth + 'px' + } container.style.overflow = 'visible' }) // 等待样式应用 await new Promise(resolve => setTimeout(resolve, 500)) - // 强制重新计算表格布局 + // 对于门店排行榜表格,保持auto宽度,不要拉伸 elementTables.forEach(table => { - // 强制重新计算表格宽度 - table.style.width = 'auto' - table.style.minWidth = 'auto' - // 触发重新布局 - table.offsetHeight - // 重新设置宽度 - table.style.width = '100%' - table.style.minWidth = '100%' + const tableContainer = table.closest('.store-ranking-table') + if (tableContainer) { + // 门店排行榜表格保持auto宽度 + table.style.width = 'auto' + table.style.minWidth = 'auto' + table.style.maxWidth = 'none' + } else { + // 其他表格保持原有逻辑 + table.style.width = 'auto' + table.style.minWidth = 'auto' + // 触发重新布局 + table.offsetHeight + // 重新设置宽度 + table.style.width = '100%' + table.style.minWidth = '100%' + } }) // 计算实际内容高度和宽度 @@ -1543,25 +1545,39 @@ export default { element.clientHeight ) - // 计算实际内容宽度,特别处理表格 - let actualWidth = Math.max( - element.scrollWidth, - element.offsetWidth, - element.clientWidth - ) - - // 检查表格是否需要更宽的宽度 - const widthCheckTables = element.querySelectorAll('.el-table') - widthCheckTables.forEach(table => { - const tableWidth = Math.max( - table.scrollWidth, - table.offsetWidth, - table.clientWidth - ) - if (tableWidth > actualWidth) { - actualWidth = tableWidth + // 对于门店排行榜,使用表格的实际宽度,而不是整个容器宽度 + const storeRankingTable = element.querySelector('.store-ranking-table .el-table') + let actualWidth + let isStoreRanking = false + + if (storeRankingTable) { + // 门店排行榜:使用表格的实际宽度,不添加额外边距 + actualWidth = storeRankingTable.offsetWidth || storeRankingTable.clientWidth || storeRankingTable.scrollWidth + isStoreRanking = true + } else { + // 其他情况:使用元素的实际渲染宽度 + actualWidth = element.offsetWidth || element.clientWidth + + // 对于其他表格,使用表格的实际宽度 + const widthCheckTables = element.querySelectorAll('.el-table') + widthCheckTables.forEach(table => { + const tableWidth = table.offsetWidth || table.clientWidth + if (tableWidth > actualWidth) { + actualWidth = tableWidth + } + }) + + // 确保宽度不超过容器的实际宽度 + const containerWidth = (element.parentElement && element.parentElement.offsetWidth) || element.offsetWidth + if (actualWidth > containerWidth) { + actualWidth = containerWidth } - }) + } + + // 进一步限制宽度,避免截图过大 + const maxWidth = Math.min(actualWidth, window.innerWidth || 1200) + // 只有门店排行榜除以2 + actualWidth = isStoreRanking ? maxWidth / 2 : maxWidth console.log('容器尺寸信息:', { scrollWidth: element.scrollWidth, @@ -1574,22 +1590,49 @@ export default { actualWidth: actualWidth }) + // 对于门店排行榜,创建一个只包含表格的临时容器用于截图 + let screenshotElement = element + let shouldRemoveTempContainer = false + + if (storeRankingTable) { + // 创建临时容器,只包含表格 + const tempContainer = document.createElement('div') + tempContainer.style.position = 'absolute' + tempContainer.style.left = '-9999px' + tempContainer.style.width = actualWidth + 'px' + tempContainer.style.padding = '0' + tempContainer.style.margin = '0' + tempContainer.style.backgroundColor = '#ffffff' + + // 克隆表格容器 + const tableContainer = storeRankingTable.closest('.store-ranking-table') + if (tableContainer) { + const clonedContainer = tableContainer.cloneNode(true) + // 确保克隆的容器宽度也是auto + clonedContainer.style.width = 'auto' + clonedContainer.style.minWidth = 'auto' + clonedContainer.style.maxWidth = 'none' + tempContainer.appendChild(clonedContainer) + document.body.appendChild(tempContainer) + screenshotElement = tempContainer + shouldRemoveTempContainer = true + } + } + // 配置截图选项 const options = { allowTaint: true, useCORS: true, - scale: 1.0, // 降低缩放比例提高兼容性 + scale: 2, backgroundColor: '#ffffff', - logging: true, // 开启日志便于调试 + logging: true, imageTimeout: 30000, removeContainer: true, - foreignObjectRendering: false, // 关闭foreignObject渲染提高兼容性 + foreignObjectRendering: false, scrollX: 0, scrollY: 0, width: actualWidth, - height: actualHeight, - windowWidth: actualWidth, - windowHeight: actualHeight, + height: shouldRemoveTempContainer ? screenshotElement.scrollHeight : actualHeight, ignoreElements: (element) => { // 忽略可能影响截图的元素 return element.classList.contains('el-loading-mask') || @@ -1644,12 +1687,28 @@ export default { // 确保表格容器宽度足够 const clonedTableContainers = clonedDoc.querySelectorAll('.store-ranking-table, .no-expansion-table, .store-table, .person-table') clonedTableContainers.forEach(container => { - container.style.width = '100%' - container.style.minWidth = '100%' - container.style.maxWidth = 'none' + // 保持表格容器的原始宽度,避免被拉伸 + const originalWidth = container.offsetWidth || container.clientWidth || container.scrollWidth + if (originalWidth) { + container.style.width = originalWidth + 'px' + container.style.minWidth = originalWidth + 'px' + container.style.maxWidth = originalWidth + 'px' + } else { + container.style.width = '100%' + container.style.minWidth = '100%' + container.style.maxWidth = 'none' + } container.style.overflow = 'visible' }) + // 对于门店排行榜表格,确保表格本身保持auto宽度 + const clonedStoreRankingTables = clonedDoc.querySelectorAll('.store-ranking-table .el-table') + clonedStoreRankingTables.forEach(table => { + table.style.width = 'auto' + table.style.minWidth = 'auto' + table.style.maxWidth = 'none' + }) + // 确保所有报表区域完整显示 const reportSections = clonedDoc.querySelectorAll('.report-section') reportSections.forEach(section => { @@ -1700,6 +1759,37 @@ export default { col.style.maxHeight = 'none' }) + // 确保排行榜卡片完整显示 + const rankingSections = clonedDoc.querySelectorAll('.ranking-section') + rankingSections.forEach(section => { + section.style.height = 'auto' + section.style.overflow = 'visible' + section.style.maxHeight = 'none' + section.style.display = 'block' + section.style.visibility = 'visible' + section.style.opacity = '1' + }) + + const rankingCards = clonedDoc.querySelectorAll('.ranking-card') + rankingCards.forEach(card => { + card.style.height = 'auto' + card.style.overflow = 'visible' + card.style.maxHeight = 'none' + card.style.display = 'flex' + card.style.visibility = 'visible' + card.style.opacity = '1' + card.style.position = 'static' + }) + + const rankingCardsContainer = clonedDoc.querySelectorAll('.ranking-cards') + rankingCardsContainer.forEach(container => { + container.style.display = 'flex' + container.style.visibility = 'visible' + container.style.opacity = '1' + container.style.height = 'auto' + container.style.overflow = 'visible' + }) + // 确保瀑布流容器完整显示 const waterfallContainer = clonedDoc.querySelector('.waterfall-container') if (waterfallContainer) { @@ -1786,13 +1876,18 @@ export default { console.log('开始生成截图,配置选项:', options) // 生成截图 - const canvas = await html2canvas.default(element, options) + const canvas = await html2canvas.default(screenshotElement, options) console.log('截图生成完成,画布尺寸:', { width: canvas.width, height: canvas.height }) + // 清理临时容器 + if (shouldRemoveTempContainer && screenshotElement.parentElement) { + screenshotElement.parentElement.removeChild(screenshotElement) + } + // 恢复原始样式 element.style.height = originalStyles.height element.style.overflow = originalStyles.overflow @@ -1815,6 +1910,10 @@ export default { this.$message.success('截图生成成功') } catch (error) { console.error('截图生成失败:', error) + // 清理临时容器(如果存在) + if (shouldRemoveTempContainer && screenshotElement && screenshotElement.parentElement) { + screenshotElement.parentElement.removeChild(screenshotElement) + } this.$message.error('截图生成失败: ' + error.message) } finally { this.screenshotLoading = false @@ -1859,9 +1958,9 @@ export default { .app-container { padding: 12px; // background-color: #f5f5f5; - min-height: 100vh; + // height: 100vh; overflow-y: scroll; - box-sizing: border-box; + // box-sizing: border-box; } .page-header { @@ -2833,35 +2932,66 @@ export default { .store-ranking-table { .el-table { - border-radius: 8px; - overflow: hidden; - font-size: 14px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - background: #ffffff; // 确保表格背景为纯白色 + border: 1px solid #e4e7ed; + font-size: 12px; + background: #ffffff; + border-collapse: separate; + border-spacing: 0; } .el-table th { - background: #f8f9fa; // 表头使用浅灰色背景 - color: #606266; - font-weight: 600; - padding: 12px 0; - font-size: 14px; + background: #92d04f !important; + color: #000 !important; + font-weight: 500; + padding: 6px 4px; + font-size: 12px; + border-bottom: 1px solid #7ab83a; + border-right: 1px solid #7ab83a; + line-height: 1.4; + + &:last-child { + border-right: none; + } + } + + // 确保表头背景色正确应用 + ::v-deep .el-table__header-wrapper { + .el-table__header { + th { + background-color: #92d04f !important; + color: #000 !important; + padding: 6px 4px; + font-size: 12px; + line-height: 1.4; + } + } } .el-table td { - padding: 10px 0; - font-size: 14px; - background: #ffffff; // 确保表格单元格背景为纯白色 + padding: 6px 4px; + font-size: 12px; + background: #ffffff; + border-bottom: 1px solid #f0f0f0; + border-right: 1px solid #f0f0f0; + color: #303133; + line-height: 1.4; + + &:last-child { + border-right: none; + } } - // 确保斑马纹效果正常显示 - .el-table--striped .el-table__body tr.el-table__row--striped td { - background: #fafafa; // 斑马纹使用更浅的灰色 + .el-table__body tr:hover > td { + background-color: #ffffff !important; + } + + .el-table__body tr:last-child td { + border-bottom: none; } .ranking-number { font-weight: 600; - font-size: 14px; + font-size: 12px; &.ranking-top { color: #F56C6C; @@ -2907,7 +3037,7 @@ export default { .ranking { font-weight: 600; - font-size: 14px; + font-size: 12px; &.ranking-top { color: #F56C6C; @@ -2921,6 +3051,35 @@ export default { color: #606266; } } + + // 总计行样式 + ::v-deep .el-table__body-wrapper { + .el-table__body { + tbody tr:last-child { + background-color: #92d04f !important; + + td { + background-color: #92d04f !important; + border-bottom: none !important; + padding: 6px 4px; + font-size: 12px; + line-height: 1.4; + + .total-label { + font-weight: 600; + color: #000; + font-size: 12px; + } + + .total-value { + font-weight: 600; + color: #000; + font-size: 12px; + } + } + } + } + } } // 未拓客人员表格样式 diff --git a/antis-ncc-admin/src/views/statisticsList/form10.vue b/antis-ncc-admin/src/views/statisticsList/form10.vue index 11b5178..5481a56 100644 --- a/antis-ncc-admin/src/views/statisticsList/form10.vue +++ b/antis-ncc-admin/src/views/statisticsList/form10.vue @@ -236,7 +236,7 @@ width="120"> @@ -492,22 +492,34 @@ export default { isStore: true, StoreId: store.StoreId, StoreName: store.StoreName, - children: [] + children: [], + // 门店合计字段(初始化为0) + SalesQuantity: 0, + SalesAmount: 0, + BillingCount: 0, + SalesCount: 0 } - // 如果有品项列表,创建品项子节点 + // 如果有品项列表,创建品项子节点并计算合计 if (store.ItemList && store.ItemList.length > 0) { store.ItemList.forEach(item => { - storeNode.children.push({ + const itemNode = { id: `item_${idCounter++}`, isStore: false, ItemId: item.ItemId, ItemName: item.ItemName, - SalesQuantity: item.SalesQuantity, - SalesAmount: item.SalesAmount, - BillingCount: item.BillingCount, - SalesCount: item.SalesCount - }) + SalesQuantity: item.SalesQuantity || 0, + SalesAmount: item.SalesAmount || 0, + BillingCount: item.BillingCount || 0, + SalesCount: item.SalesCount || 0 + } + storeNode.children.push(itemNode) + + // 累加门店合计值 + storeNode.SalesQuantity += Number(itemNode.SalesQuantity) || 0 + storeNode.SalesAmount += Number(itemNode.SalesAmount) || 0 + storeNode.BillingCount += Number(itemNode.BillingCount) || 0 + storeNode.SalesCount += Number(itemNode.SalesCount) || 0 }) } diff --git a/绿纤uni-app/pages/index/index.vue b/绿纤uni-app/pages/index/index.vue index a4c1e3a..7e7c6b1 100644 --- a/绿纤uni-app/pages/index/index.vue +++ b/绿纤uni-app/pages/index/index.vue @@ -7,32 +7,32 @@ {{ loadingText }} - + - + - - - 绿纤协同办公平台 - 高效协同,移动办公 - - - 门店: - {{ jsjinfo && jsjinfo.storeName?jsjinfo.storeName:'暂无'}} - - - 本月金三角: - {{ jsjinfo && jsjinfo.jsjName?jsjinfo.jsjName:'暂无'}} + + + 绿纤协同办公平台 + 高效协同,移动办公 + + + 门店: + {{ jsjinfo && jsjinfo.storeName?jsjinfo.storeName:'暂无'}} + + + 本月金三角: + {{ jsjinfo && jsjinfo.jsjName?jsjinfo.jsjName:'暂无'}} + - - + - - + @@ -43,7 +43,8 @@ {{ summaryData.inviteCount || 0 }} 邀约数 - + {{ summaryData.appointmentCount || 0 }} 预约数 @@ -82,7 +83,7 @@ 耗卡 - + @@ -101,14 +102,14 @@ 建档 - + 会员 - + @@ -127,7 +128,7 @@ 报表 - + @@ -137,34 +138,29 @@ - - 业绩数据 - - + 业绩数据 + + + + ¥{{ (performanceData.OrderAchievement || 0).toFixed(2) }} + 开卡业绩 - - {{ performanceData.AppointmentCount || 0 }} - 预约人数 + + ¥{{ (performanceData.ConsumeAchievement || 0).toFixed(2) }} + 消耗业绩 - --> - + + + ¥{{ (performanceData.BillingAmount || 0).toFixed(2) }} @@ -174,21 +170,16 @@ ¥{{ (performanceData.ConsumeAmount || 0).toFixed(2) }} 消耗金额 - - ¥{{ (performanceData.RefundAmount || 0).toFixed(2) }} 退卡金额 - - - - {{ performanceData.BillingCount || 0 }} + + {{ performanceData.ConsumeProjectCount || 0 }} 项目数 + + {{ performanceData.HeadCount || 0 }} 人头 @@ -197,596 +188,610 @@ {{ performanceData.PersonCount || 0 }} 人次 + + {{ performanceData.LaborCost || 0 }} + 手工费 + - + .container { + min-height: 100vh; + background: #e8f5e9; + } + + .status-bar { + background: linear-gradient(120deg, #43e97b 0%, #38f9d7 100%); + } + + .header { + background: linear-gradient(120deg, #43e97b 0%, #38f9d7 100%); + padding: 32rpx 0 48rpx 0; + position: relative; + box-shadow: 0 4rpx 24rpx 0 rgba(67, 233, 123, 0.08); + } + + .header-content { + text-align: center; + padding: 0 40rpx; + } + + .header-title { + color: #fff; + font-size: 36rpx; + font-weight: bold; + letter-spacing: 4rpx; + margin-bottom: 16rpx; + } + + .header-subtitle { + color: #e0f2f1; + font-size: 28rpx; + margin-bottom: 32rpx; + font-weight: 500; + } + + .info-section { + background: rgba(255, 255, 255, 0.15); + border-radius: 16rpx; + padding: 24rpx 32rpx; + margin: 0 auto; + max-width: 600rpx; + backdrop-filter: blur(10rpx); + } + + .info-item { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16rpx; + } + + .info-item:last-child { + margin-bottom: 0; + } + + .info-label { + color: #e0f2f1; + font-size: 26rpx; + font-weight: 500; + } + + .info-value { + color: #fff; + font-size: 26rpx; + font-weight: 600; + text-align: right; + flex: 1; + margin-left: 16rpx; + } + + .header-illustration { + width: 120rpx; + height: 120rpx; + margin: 0 auto; + background: rgba(255, 255, 255, 0.12); + border-radius: 32rpx; + display: flex; + align-items: center; + justify-content: center; + } + + .summary-card { + background: #fff; + border-radius: 28rpx; + box-shadow: 0 4rpx 24rpx 0 rgba(67, 233, 123, 0.08); + margin: -36rpx 40rpx 36rpx 40rpx; + display: flex; + justify-content: space-between; + padding: 28rpx 20rpx; + position: relative; + z-index: 2; + } + + .summary-item { + flex: 1; + text-align: center; + color: #388e3c; + cursor: pointer; + } + + .summary-item .num { + font-size: 36rpx; + font-weight: bold; + color: #43a047; + margin-bottom: 4rpx; + display: block; + } + + .summary-item .label { + font-size: 24rpx; + color: #6a9c6a; + } + + .section-title { + margin: 0 40rpx 16rpx 40rpx; + font-size: 32rpx; + color: #388e3c; + font-weight: bold; + letter-spacing: 2rpx; + } + + .func-card { + background: #fff; + border-radius: 28rpx; + box-shadow: 0 4rpx 24rpx 0 rgba(67, 233, 123, 0.08); + padding: 36rpx 0 16rpx 0; + margin: 0 40rpx 36rpx 40rpx; + } + + .icon-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 36rpx 0; + width: 100%; + justify-items: center; + } + + .icon-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #e8f5e9 60%, #c8e6c9 100%); + border-radius: 32rpx; + box-shadow: 0 4rpx 16rpx #c8e6c9; + padding: 28rpx 0 16rpx 0; + width: 140rpx; + height: 160rpx; + transition: box-shadow 0.18s, background 0.18s; + } + + .icon-btn:active { + box-shadow: 0 8rpx 32rpx #a5d6a7; + background: linear-gradient(135deg, #b2dfdb 60%, #e8f5e9 100%); + } + + .icon { + width: 64rpx; + height: 64rpx; + margin-bottom: 12rpx; + display: flex; + align-items: center; + justify-content: center; + } + + .icon-label { + font-size: 28rpx; + color: #388e3c; + margin-top: 4rpx; + letter-spacing: 2rpx; + } + + /* 加载效果样式 */ + .loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.95); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + backdrop-filter: blur(4px); + } + + .loading-content { + text-align: center; + } + + .loading-spinner { + width: 60rpx; + height: 60rpx; + border: 4rpx solid #e8f5e9; + border-top: 4rpx solid #43a047; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto; + } + + .loading-text { + margin-top: 20rpx; + color: #388e3c; + font-size: 32rpx; + font-weight: 500; + text-align: center; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } + } + + /* 业绩数据板块样式 */ + .performance-card { + background: #fff; + border-radius: 28rpx; + box-shadow: 0 4rpx 24rpx 0 rgba(67, 233, 123, 0.08); + padding: 32rpx 24rpx; + margin: 0 40rpx 36rpx 40rpx; + } + + .performance-row { + display: flex; + justify-content: space-between; + margin-bottom: 24rpx; + } + + .performance-row:last-child { + margin-bottom: 0; + } + + .performance-item { + flex: 1; + text-align: center; + padding: 16rpx 8rpx; + background: linear-gradient(135deg, #e8f5e9 60%, #c8e6c9 100%); + border-radius: 20rpx; + margin: 0 8rpx; + box-shadow: 0 2rpx 8rpx rgba(67, 233, 123, 0.1); + } + + .performance-item.highlight { + background: linear-gradient(135deg, #fff3e0 60%, #ffe0b2 100%); + box-shadow: 0 2rpx 8rpx rgba(255, 152, 0, 0.2); + } + + .performance-value { + font-size: 32rpx; + font-weight: bold; + color: #43a047; + margin-bottom: 8rpx; + } + + .performance-item.highlight .performance-value { + color: #f57c00; + } + + .performance-label { + font-size: 22rpx; + color: #6a9c6a; + } + + .performance-item.highlight .performance-label { + color: #e65100; + } + \ No newline at end of file diff --git a/绿纤uni-app/pages/member-consume/member-consume copy.vue b/绿纤uni-app/pages/member-consume/member-consume copy.vue new file mode 100644 index 0000000..5a570f5 --- /dev/null +++ b/绿纤uni-app/pages/member-consume/member-consume copy.vue @@ -0,0 +1,2963 @@ + + + + + \ No newline at end of file diff --git a/绿纤uni-app/pages/member-consume/member-consume.vue b/绿纤uni-app/pages/member-consume/member-consume.vue index 4371262..1caeee3 100644 --- a/绿纤uni-app/pages/member-consume/member-consume.vue +++ b/绿纤uni-app/pages/member-consume/member-consume.vue @@ -73,7 +73,7 @@ - @@ -826,21 +826,6 @@ async handlePxSelection(selectedOption) { if (this.currentRowIndex >= 0) { try { - console.error(selectedOption) - // 先校验已经选择的品项有没有这个 - const existingIndex = this.pxList.findIndex((item, index) => - index !== this.currentRowIndex && item.BillingItemId === selectedOption.BillingItemId - ); - if (existingIndex !== -1) { - uni.showToast({ - title: '该品项已存在,请勿重复添加', - icon: 'none', - duration: 2000 - }); - this.closeModal(); - return; - } - // 请求品项详细信息 const detailResult = await lxApi.getPxDetail(selectedOption.px); let qt2 = ""; @@ -1155,12 +1140,14 @@ // 更新品项次数 updatePxNumber(rowIndex, event) { const value = event.detail.value; + console.log('value',value) if (this.pxList[rowIndex]) { - let inputNumber = parseInt(value) || 1; + let inputNumber = parseInt(value); // 验证不能超过剩余次数 - if (inputNumber > this.pxList[rowIndex].RemainingCount) { - inputNumber = this.pxList[rowIndex].RemainingCount; + if (inputNumber <= 0 ) { + inputNumber = 1; + console.log('inputNumber',inputNumber) // 更新输入框显示的值 this.$nextTick(() => { const inputElement = event.target; @@ -1168,6 +1155,7 @@ inputElement.value = inputNumber; } }); + this.$forceUpdate(); } this.pxList[rowIndex].projectNumber = inputNumber; @@ -1633,15 +1621,94 @@ }); return; } + } + + // 验证相同品项的次数总和不能超过剩余次数 + // 按品项分组(优先使用 BillingItemId,如果没有则使用 px) + console.log('========== 开始验证相同品项次数总和 =========='); + console.log('品项列表:', this.pxList.map((px, idx) => ({ + 行号: idx + 1, + 品项名称: px.pxmc, + BillingItemId: px.BillingItemId, + px: px.px, + 次数: px.projectNumber, + 剩余次数: px.RemainingCount + }))); + + const pxGroups = new Map(); + for (let i = 0; i < this.pxList.length; i++) { + const px = this.pxList[i]; + // 生成唯一标识:优先使用 BillingItemId,否则使用 px + const key = px.BillingItemId; + + console.log(`处理第${i + 1}行: 品项=${px.pxmc}, BillingItemId=${key}, 次数=${px.projectNumber}, 剩余次数=${px.RemainingCount}`); + + if (!pxGroups.has(key)) { + pxGroups.set(key, { + items: [], + pxName: px.pxmc || '', + remainingCount: null + }); + console.log(` 创建新分组: key=${key}, 品项名称=${px.pxmc}`); + } + + const group = pxGroups.get(key); + group.items.push({ + index: i, + px: px, + projectNumber: px.projectNumber || 0 + }); + console.log(` 添加到分组: 当前分组有${group.items.length}个品项`); + + // 记录第一个有 RemainingCount 的值作为该品项的剩余次数 + if (group.remainingCount === null && px.RemainingCount !== undefined && px.RemainingCount !== null) { + group.remainingCount = px.RemainingCount; + console.log(` 设置剩余次数: ${px.RemainingCount}`); + } + } + + console.log('分组结果:', Array.from(pxGroups.entries()).map(([key, group]) => ({ + key: key, + 品项名称: group.pxName, + 剩余次数: group.remainingCount, + 包含行数: group.items.length, + 行号列表: group.items.map(item => item.index + 1), + 次数列表: group.items.map(item => item.projectNumber) + }))); + + // 验证每个分组的次数总和 + for (const [key, group] of pxGroups.entries()) { + console.log(`\n验证分组: key=${key}, 品项=${group.pxName}`); + + // 只验证有 RemainingCount 的品项(修改时已存在的品项可能没有此字段) + if (group.remainingCount === null) { + console.log(` 跳过验证: 该品项没有剩余次数字段(可能是修改时已存在的品项)`); + continue; + } - // 验证次数不能超过剩余次数 - if (px.RemainingCount && px.projectNumber > px.RemainingCount) { + // 计算该品项在所有行的次数总和 + const totalNumber = group.items.reduce((sum, item) => sum + Number(item.projectNumber), 0); + const rowNumbers = group.items.map(item => item.index + 1).join('、'); + + console.log(` 次数总和: ${totalNumber} (行号: ${rowNumbers})`); + console.log(` 剩余次数: ${group.remainingCount}`); + console.log(` 验证结果: ${totalNumber > group.remainingCount ? '❌ 失败' : '✅ 通过'}`); + + if (totalNumber > group.remainingCount) { + console.error(`验证失败: 品项"${group.pxName}"在第${rowNumbers}行的次数总和(${totalNumber})超过剩余次数(${group.remainingCount})`); uni.showToast({ - title: `第${i + 1}个品项的次数(${px.projectNumber})不能超过剩余次数(${px.RemainingCount})`, + title: `品项"${group.pxName}"在第${rowNumbers}行的次数总和(${totalNumber})不能超过剩余次数(${group.remainingCount})`, icon: 'none' }); return; } + } + + console.log('========== 相同品项次数总和验证通过 ==========\n'); + + // 继续验证其他信息 + for (let i = 0; i < this.pxList.length; i++) { + const px = this.pxList[i]; // 验证健康师(特殊处理:px为cell时,健康师和科技部老师至少选择一个) const isSpecialPx = px.beautyType == 'cell';