Slice Wood

本文介绍了一种通过二分查找算法来解决木材切割问题的方法,旨在将原木切割为尽可能长且数量固定的小段木头。文章详细解释了算法的具体实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

描述


木材厂有一些原木,现在想把这些木头切割成一些长度相同的小段木头,需要得到的小段的数目


是给定了。当然,我们希望得到的小段越长越好,你的任务是计算能够得到的小段木头的最大长


度。


木头长度的单位是厘米。原木的长度都是正整数,我们要求切割得到的小段木头的长度也要求是


正整数。


例如输入 A = {232, 124, 456}, targetNum = 7

意思是,有3段原木,长度分别是 232, 124, 456, 希望通过切割后得到7根木头,每根的长度一样(并且做到长度尽量长)。


// given len of each slice, return total number of slice could get
public int getSlices(int[] A, int eachLen) {
    int count = 0;
    for (int i = 0; i < A.length; i++) {
        count += A[i] / eachLen;
    }
    return count;
}

public int sliceWood(int[] A, int targetNum) {
    if (A == null || A.length == 0) {
        return 0;
    }
    
    int max = 0;
    for (int i = 0; i < A.length; i++) {
        if (A[i] > max) {
            max = A[i];
        }
    }
    
    int left = 0;
    int right = max;
    int mid;
    
    while (left + 1 < right) {
        mid = left + (right - left) / 2;
        int sliceNum = getSlices(A, mid);
        
        if (sliceNum == targetNum) {
        // 找到符合要求的长度后,不能立刻停止,而要继续往右找,因为需要做到每根长度是可能做到的最长
            left = mid;
        } else if (sliceNum < targetNum) {
            right = mid;
        } else {
            left = mid;
        }
    }
    
    int rightNum = getSlices(A, right);
    int leftNum = getSlices(A, left);
    
    if (rightNum == targetNum) {
        return right;
    }
    if (leftNum == targetNum) {
        return left;
    }
    
    return 0;
}


思路是,先找到所有原木中长度的最大值。

然后 left = 0, right = 原木最大值,进行二分查找。

但是, 找到一个符合要求的切割长度,却并不一定是最优解。因为题目要求做到这个切割长度是尽量长的。

所以在找到一个符合要求的切割长度后,任然继续向右找。

这样在跳出后,right 和 left 上肯定至少有一个保留有符合要求的,并且是最大的长度。

优先比较right, 因为如果right 和 left 都符合要求, right的长度更长

/* 文件路径: index.js */ /* 文件路径: pages/index/index.js */ // 首页逻辑文件 const app = getApp(); Page({ data: { // 统计数据 totalOrders: 0, totalProducts: 0, totalMaterials: 0, totalStock: 0, // 搜索条件 orderSearch: '', productSearch: '', materialSearch: '', woodSearch: '', thicknessSearch: '', minStock: '', maxStock: '', // 结果数据 loading: true, results: [], filteredResults: [], resultCount: 0, lastUpdate: '--:--:--', // 排序 sortField: null, sortDirection: 'asc', // 分页 currentPage: 1, pageSize: 10, totalPages: 1, // 当前页数据 currentPageData: [], // 板材详情 showMaterialDetail: false, materialDetail: null }, onLoad: function() { console.log('index页面加载'); // 获取全局数据管理器 if (app.globalData && app.globalData.dataManager) { this.dataManager = app.globalData.dataManager; console.log('获取到dataManager实例'); // 注册数据更新回调 this.dataManager.registerCallback('all', () => { console.log('收到数据更新回调'); this.updatePageData(); }); // 初始化页面数据 this.updatePageData(); // 主动加载数据 this.dataManager.fetchAll().then(() => { console.log('数据加载成功'); }).catch(err => { console.error('数据加载失败:', err); wx.showToast({ title: '数据加载失败', icon: 'none' }); }); } else { console.error('DataManager not available'); wx.showToast({ title: '数据管理器未初始化', icon: 'none' }); // 尝试重新初始化 if (app.initDataManager) { app.initDataManager(); } } }, onShow: function() { console.log('首页显示'); // 检查是否需要登录 if (app.globalData && app.globalData.needLogin) { console.log('检测到需要登录'); // 延迟500ms确保页面渲染完成 setTimeout(() => { app.checkWechatLogin(); }, 500); } }, // 更新页面数据 updatePageData: function() { if (!this.dataManager || !this.dataManager.data) return; this.setData({ loading: true }); // 更新统计数据 this.updateStats(); // 更新表格数据 this.updateTable(); // 更新最后更新时间 const now = new Date(); const lastUpdate = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`; this.setData({ lastUpdate }); }, // 更新统计数据 updateStats: function() { const data = this.dataManager.data; const totalOrders = data.dingdans?.length || 0; const totalProducts = data.chanpins?.length || 0; const totalMaterials = data.bancais?.length || 0; const totalStock = data.kucuns?.reduce((sum, kucun) => sum + kucun.shuliang, 0) || 0; this.setData({ totalOrders, totalProducts, totalMaterials, totalStock }); }, // 更新表格数据 updateTable: function() { console.log('开始更新表格数据'); const data = this.dataManager.data; console.log('原始数据:', JSON.stringify(data, null, 2)); let results = []; // 直接遍历dingdan_bancai表获取数据 if (data.dingdan_bancais) { data.dingdan_bancais.forEach(db => { // 获取关联信息 const dingdan = db.dingdan || {}; const chanpin = db.chanpin || {}; const zujian = db.zujian || {}; const bancai = db.bancai || {}; // 获取库存数量 const kucun = data.kucuns?.find(k => k.bancai && k.bancai.id === bancai.id); const stockQuantity = kucun ? kucun.shuliang : 0; // 构建板材信息 const materialInfo = this.getMaterialInfo(bancai); // 构建结果项 results.push({ id: `${db.id}`, orderNumber: dingdan.number || '无', productInfo: chanpin.bianhao || '独立订购', productQuantity: db.shuliang, component: zujian.name || '独立板材', material: materialInfo, materialPerComponent: 1, // 直接订购时,每件用量为1 materialOrderQuantity: db.shuliang, stockQuantity: stockQuantity, raw: { orderNumber: dingdan.number, productQuantity: db.shuliang, materialPerComponent: 1, materialOrderQuantity: db.shuliang, stockQuantity: stockQuantity, thickness: bancai.houdu || 0 }, bancaiId: bancai.id }); }); } // 应用排序 if (this.data.sortField !== null) { results = this.sortResults(results, this.data.sortField, this.data.sortDirection); } // 更新页面数据 this.setData({ results: results, filteredResults: results, resultCount: results.length, totalPages: Math.ceil(results.length / this.data.pageSize), currentPage: 1, loading: false }, () => { // 更新当前页数据 this.updateCurrentPageData(); }); // 应用筛选 this.applyFilters(); }, // 获取板材详细信息函数 getMaterialInfo: function(bancai) { if (!bancai) return '未知板材'; // 获取材质名称 const caizhiName = bancai.caizhi?.name || '未知材质'; // 获取木皮信息 const formatMupi = (mupi) => { if (!mupi) return null; return `${mupi.name}${mupi.you ? '(油)' : ''}`; }; const mupi1Str = formatMupi(bancai.mupi1) || '无'; const mupi2Str = formatMupi(bancai.mupi2) || '无'; const mupiInfo = mupi1Str + (mupi2Str !== '无' ? `/${mupi2Str}` : ''); // 厚度转换为字符串 const thickness = bancai.houdu ? `${bancai.houdu}mm` : '未知厚度'; return `${caizhiName}-${mupiInfo} (${thickness})`; }, // 搜索输入处理函数 onOrderSearchInput: function(e) { this.setData({ orderSearch: e.detail.value }); this.applyFilters(); }, onProductSearchInput: function(e) { this.setData({ productSearch: e.detail.value }); this.applyFilters(); }, onMaterialSearchInput: function(e) { this.setData({ materialSearch: e.detail.value }); this.applyFilters(); }, onWoodSearchInput: function(e) { this.setData({ woodSearch: e.detail.value }); // 支持中文和数字搜索 const searchTerm = e.detail.value.toLowerCase(); const searchKeywords = { '有油': '有油', '无油': '无油', 'you': '有油', 'wu': '无油', '1': '有油', '0': '无油', '(油)': '(油)' }; this.setData({ woodSearch: searchKeywords[searchTerm] || searchTerm }); this.applyFilters(); }, onThicknessSearchInput: function(e) { this.setData({ thicknessSearch: e.detail.value }); this.applyFilters(); }, onMinStockInput: function(e) { this.setData({ minStock: e.detail.value }); }, onMaxStockInput: function(e) { this.setData({ maxStock: e.detail.value }); }, // 库存范围搜索 onStockSearch: function() { this.applyFilters(); }, // 重置筛选 resetFilters: function() { this.setData({ orderSearch: '', productSearch: '', materialSearch: '', woodSearch: '', thicknessSearch: '', minStock: '', maxStock: '', sortField: null, sortDirection: 'asc', currentPage: 1 }); this.updateTable(); }, // 应用筛选条件 applyFilters: function() { let filtered = [...this.data.results]; // 应用订单号筛选 if (this.data.orderSearch) { const search = this.data.orderSearch.toLowerCase(); filtered = filtered.filter(item => item.orderNumber && item.orderNumber.toLowerCase().includes(search) ); } // 应用产品筛选 if (this.data.productSearch) { const search = this.data.productSearch.toLowerCase(); filtered = filtered.filter(item => item.productInfo && item.productInfo.toLowerCase().includes(search) ); } // 应用板材筛选 if (this.data.materialSearch) { const search = this.data.materialSearch.toLowerCase(); filtered = filtered.filter(item => item.material && item.material.toLowerCase().includes(search) ); } // 应用木皮筛选 if (this.data.woodSearch) { const search = this.data.woodSearch.toLowerCase(); filtered = filtered.filter(item => item.material && item.material.toLowerCase().includes(search) ); } // 应用厚度筛选 if (this.data.thicknessSearch) { const thickness = parseFloat(this.data.thicknessSearch); if (!isNaN(thickness)) { filtered = filtered.filter(item => { // 提取厚度数字部分进行匹配 const thicknessMatch = item.material.match(/(\d+\.?\d*)/); if (thicknessMatch) { const thicknessValue = parseFloat(thicknessMatch[1]); // 允许小数点误差 return Math.abs(thicknessValue - thickness) < 0.5; } return false; }); } } // 应用库存范围筛选 if (this.data.minStock !== '' || this.data.maxStock !== '') { const min = this.data.minStock !== '' ? parseInt(this.data.minStock) : Number.MIN_SAFE_INTEGER; const max = this.data.maxStock !== '' ? parseInt(this.data.maxStock) : Number.MAX_SAFE_INTEGER; filtered = filtered.filter(item => item.stockQuantity >= min && item.stockQuantity <= max ); } // 更新筛选结果 this.setData({ filteredResults: filtered, resultCount: filtered.length, totalPages: Math.ceil(filtered.length / this.data.pageSize), currentPage: 1 }, () => { this.updateCurrentPageData(); }); }, // 排序处理 sortBy: function(e) { const field = e.currentTarget.dataset.field; // 如果点击的是当前排序字段,则切换排序方向 if (field === this.data.sortField) { this.setData({ sortDirection: this.data.sortDirection === 'asc' ? 'desc' : 'asc' }); } else { // 否则,设置新的排序字段,默认升序 this.setData({ sortField: field, sortDirection: 'asc' }); } // 应用排序 const sorted = this.sortResults(this.data.filteredResults, this.data.sortField, this.data.sortDirection); this.setData({ filteredResults: sorted }, () => { // 更新当前页数据 this.updateCurrentPageData(); }); }, // 排序结果 sortResults: function(results, field, direction) { if (!field) return results; return [...results].sort((a, b) => { let valueA, valueB; // 根据排序字段获取值 switch (field) { case 'dingdan.number': valueA = a.orderNumber; valueB = b.orderNumber; break; case 'chanpin.bianhao': valueA = a.productInfo; valueB = b.productInfo; break; case 'dingdan_chanpin.shuliang': valueA = a.productQuantity; valueB = b.productQuantity; break; case 'zujian.name': valueA = a.component; valueB = b.component; break; case 'bancai.id': // 特殊处理:按厚度排序 return this.sortByThickness(a, b, direction); case 'chanpin_zujian.one_howmany': valueA = a.materialPerComponent; valueB = b.materialPerComponent; break; case 'orderUsage': valueA = a.materialOrderQuantity; valueB = b.materialOrderQuantity; break; case 'kucun.shuliang': valueA = a.stockQuantity; valueB = b.stockQuantity; break; default: valueA = a.orderNumber; valueB = b.orderNumber; } // 数值比较 if (['productQuantity', 'materialPerComponent', 'materialOrderQuantity', 'stockQuantity'].includes(field)) { return this.sortNumeric(valueA, valueB, direction); } // 字符串比较 return this.sortString(valueA, valueB, direction); }); }, // 按厚度排序(特殊处理) sortByThickness: function(a, b, direction) { return this.sortNumeric(a.raw.thickness, b.raw.thickness, direction); }, // 数值排序辅助函数 sortNumeric: function(aVal, bVal, direction) { const aNum = parseFloat(aVal) || 0; const bNum = parseFloat(bVal) || 0; return direction === 'asc' ? aNum - bNum : bNum - aNum; }, // 字符串排序辅助函数 sortString: function(aVal, bVal, direction) { const aStr = String(aVal).toLowerCase(); const bStr = String(bVal).toLowerCase(); if (aStr < bStr) return direction === 'asc' ? -1 : 1; if (aStr > bStr) return direction === 'asc' ? 1 : -1; return 0; }, // 分页处理 prevPage: function() { if (this.data.currentPage > 1) { this.setData({ currentPage: this.data.currentPage - 1 }, () => { this.updateCurrentPageData(); }); } }, nextPage: function() { if (this.data.currentPage < this.data.totalPages) { this.setData({ currentPage: this.data.currentPage + 1 }, () => { this.updateCurrentPageData(); }); } }, // 更新当前页数据 updateCurrentPageData: function() { const startIndex = (this.data.currentPage - 1) * this.data.pageSize; const endIndex = startIndex + this.data.pageSize; const currentPageData = this.data.filteredResults.slice(startIndex, endIndex).map((item, index) => { // 为每个项目添加一个唯一的ID,使用索引确保唯一性 return { ...item, uniqueId: `item-${startIndex + index}` }; }); this.setData({ currentPageData }); }, // 显示板材详情 showMaterialDetail: function(e) { const materialId = e.currentTarget.dataset.id; // 查找板材详情 const bancai = this.dataManager.data.bancais.find(b => b.id === materialId); if (!bancai) { wx.showToast({ title: '未找到板材信息', icon: 'none' }); return; } // 获取库存信息 const kucun = this.dataManager.data.kucuns ? this.dataManager.data.kucuns.find(k => k.bancai && k.bancai.id === materialId) : null; // 格式化木皮信息 const formatMupiDetail = (mupi) => { if (!mupi) return '无'; return `${mupi.name}${mupi.you ? '(油)' : ''}`; }; // 获取相关订单 const relatedOrders = this.getRelatedOrders(bancai); // 设置详情数据 this.setData({ materialDetail: { id: bancai.id, material: bancai.caizhi?.name || '未知', thickness: bancai.houdu ? bancai.houdu.toFixed(1) + 'mm' : '未知', stock: kucun ? kucun.shuliang : 0, mupi1: formatMupiDetail(bancai.mupi1), mupi2: formatMupiDetail(bancai.mupi2), relatedOrders: relatedOrders.map(order => ({ number: order.number, xiadan: order.xiadan ? this.formatDate(order.xiadan) : '-', jiaohuo: order.jiaohuo ? this.formatDate(order.jiaohuo) : '-' })) }, showMaterialDetail: true }); }, // 关闭板材详情 closeMaterialDetail: function() { this.setData({ showMaterialDetail: false }); }, // 获取板材相关订单 getRelatedOrders: function(bancai) { const data = this.dataManager.data; const orderMap = new Map(); // 使用Map进行订单去重 // 查找直接订单组件关联 const dingdan_bancais = data.dingdan_bancais || []; dingdan_bancais.forEach(db => { if (db.bancai && db.bancai.id === bancai.id && db.dingdan) { orderMap.set(db.dingdan.id, db.dingdan); } }); // 转换为数组并排序(按下单日期降序) return Array.from(orderMap.values()).sort((a, b) => { const dateA = a.xiadan ? new Date(a.xiadan) : new Date(0); const dateB = b.xiadan ? new Date(b.xiadan) : new Date(0); return dateB - dateA; // 最近的订单在前 }); }, // 格式化日期 formatDate: function(dateString) { if (!dateString) return ''; const date = new Date(dateString); return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; } }); ================================================================================ /* 文件路径: index.wxml */ <!-- 首页页面结构 --> <view class="container"> <!-- 标题部分 --> <view class="header-section"> <view class="title">板材库存管理系统</view> <view class="subtitle">查询订单、产品、板材及库存信息</view> </view> <!-- 统计卡片区域 --> <view class="stats-container"> <view class="stat-card"> <view class="stat-title">总订单数</view> <view class="stat-value">{{totalOrders}}</view> <view class="stat-icon"><icon type="success" size="20"/></view> </view> <view class="stat-card"> <view class="stat-title">产品总数</view> <view class="stat-value">{{totalProducts}}</view> <view class="stat-icon"><icon type="info" size="20"/></view> </view> <view class="stat-card"> <view class="stat-title">板材总数</view> <view class="stat-value">{{totalMaterials}}</view> <view class="stat-icon"><icon type="waiting" size="20"/></view> </view> </view> <!-- 搜索区域 --> <view class="search-section"> <view class="search-header"> <icon type="search" size="18"/> <text>高级搜索</text> </view> <view class="search-body"> <view class="search-row"> <!-- 订单搜索 --> <view class="search-item"> <view class="search-control"> <icon type="search" size="16"/> <input type="text" placeholder="搜索订单号..." bindinput="onOrderSearchInput"/> </view> </view> <!-- 产品搜索 --> <view class="search-item"> <view class="search-control"> <icon type="search" size="16"/> <input type="text" placeholder="搜索产品编号..." bindinput="onProductSearchInput"/> </view> </view> </view> <view class="search-row"> <!-- 板材搜索 --> <view class="search-item"> <view class="search-control"> <icon type="search" size="16"/> <input type="text" placeholder="搜索板材ID或材质..." bindinput="onMaterialSearchInput"/> </view> </view> <!-- 木皮搜索 --> <view class="search-item"> <view class="search-control"> <icon type="search" size="16"/> <input type="text" placeholder="搜索木皮名称..." bindinput="onWoodSearchInput"/> </view> </view> </view> <view class="search-row"> <!-- 厚度搜索 --> <view class="search-item"> <view class="search-control"> <icon type="search" size="16"/> <input type="digit" placeholder="厚度(mm)" bindinput="onThicknessSearchInput"/> </view> </view> <!-- 库存范围搜索 --> <view class="search-item"> <view class="stock-range"> <input type="number" placeholder="最小库存" bindinput="onMinStockInput"/> <input type="number" placeholder="最大库存" bindinput="onMaxStockInput"/> <button class="search-btn" bindtap="onStockSearch"><icon type="search" size="16"/></button> </view> </view> </view> <!-- 重置按钮 --> <view class="search-actions"> <button class="reset-btn" bindtap="resetFilters"> <icon type="cancel" size="16"/> 重置筛选 </button> </view> </view> </view> <!-- 结果区域 --> <view class="results-section"> <view class="results-header"> <view class="results-title"> <icon type="success" size="18"/> <text>查询结果</text> </view> <view class="results-info"> <text>{{resultCount}} 条记录</text> <text class="update-time">数据更新时间: {{lastUpdate}}</text> </view> </view> <view class="results-body"> <!-- 表格头部 --> <view class="table-header"> <view class="th" bindtap="sortBy" data-field="dingdan.number">订单号</view> <view class="th" bindtap="sortBy" data-field="chanpin.bianhao">产品信息</view> <view class="th" bindtap="sortBy" data-field="dingdan_chanpin.shuliang">产品数量</view> <view class="th" bindtap="sortBy" data-field="zujian.name">组件</view> <view class="th" bindtap="sortBy" data-field="bancai.id">板材</view> <view class="th" bindtap="sortBy" data-field="chanpin_zujian.one_howmany">单件用量</view> <view class="th" bindtap="sortBy" data-field="orderUsage">订单用量</view> <view class="th" bindtap="sortBy" data-field="kucun.shuliang">库存数量</view> <view class="th">操作</view> </view> <!-- 表格内容 --> <view class="table-body"> <block wx:if="{{loading}}"> <view class="loading-row"> <view class="loading-spinner"></view> <text>正在加载数据,请稍候...</text> </view> </block> <block wx:elif="{{results.length === 0}}"> <view class="no-results"> <icon type="info" size="40"/> <view class="no-results-text">没有找到匹配的记录</view> <view class="no-results-hint">请尝试调整您的搜索条件</view> </view> </block> <block wx:else> <!-- 调试信息 --> <view class="debug-info"> <text>currentPageData长度: {{currentPageData.length}}</text> <text>results长度: {{results.length}}</text> </view> <view class="table-row" wx:for="{{currentPageData}}" wx:key="uniqueId"> <view class="td">{{item.orderNumber}}</view> <view class="td">{{item.productInfo}}</view> <view class="td">{{item.productQuantity}}</view> <view class="td">{{item.component}}</view> <view class="td">{{item.material}}</view> <view class="td">{{item.materialPerComponent}}</view> <view class="td">{{item.materialOrderQuantity}}</view> <view class="td">{{item.stockQuantity}}</view> <view class="td"> <button class="detail-btn" bindtap="showMaterialDetail" data-id="{{item.bancaiId}}">详情</button> </view> </view> </block> </view> <!-- 分页控件 --> <view class="pagination" wx:if="{{!loading && results.length > 0}}"> <button class="page-btn" disabled="{{currentPage === 1}}" bindtap="prevPage">上一页</button> <view class="page-info">{{currentPage}}/{{totalPages}}</view> <button class="page-btn" disabled="{{currentPage === totalPages}}" bindtap="nextPage">下一页</button> </view> </view> </view> </view> <!-- 板材详情弹窗 --> <view class="material-modal {{showMaterialDetail ? 'show' : ''}}"> <view class="modal-content"> <view class="modal-header"> <text>板材详情</text> <icon type="cancel" size="20" bindtap="closeMaterialDetail"/> </view> <view class="modal-body"> <!-- 详情内容 --> <block wx:if="{{materialDetail}}"> <view class="detail-item"> <text class="detail-label">板材ID:</text> <text class="detail-value">{{materialDetail.id}}</text> </view> <view class="detail-item"> <text class="detail-label">材质:</text> <text class="detail-value">{{materialDetail.material}}</text> </view> <view class="detail-item"> <text class="detail-label">木皮:</text> <text class="detail-value">{{materialDetail.wood}}</text> </view> <view class="detail-item"> <text class="detail-label">厚度:</text> <text class="detail-value">{{materialDetail.thickness}}mm</text> </view> <view class="detail-item"> <text class="detail-label">库存:</text> <text class="detail-value">{{materialDetail.stock}}</text> </view> </block> </view> <view class="modal-footer"> <button bindtap="closeMaterialDetail">关闭</button> </view> </view> </view> ================================================================================ /* 文件路径: MiniProgramDataManager.js */ /** * 微信小程序数据管理器 * 基于DataManager.js的逻辑,但适配微信小程序环境 */ // 解析数据引用关系的辅助函数 /** * 解析数据中的引用关系 * * 该函数用于处理嵌套的数据结构,将数据中的引用关系解析为实际的对象引用。 * 它会遍历数据中的所有实体,查找属性名中包含的数字(如"item1"), * 并尝试将这些属性值替换为对应类型数据中的实际对象引用。 * * @param {Object} data - 包含嵌套引用的原始数据对象 * @returns {Object} 处理后的数据对象,其中引用已被解析为实际对象 */ function resolveDataReferences(data) { const keys = Object.keys(data); for (const key of keys) { const entities = data[key]; for (const entity of entities) { for (const attribute in entity) { if (entity.hasOwnProperty(attribute)) { // 修复:统一使用复数形式查找引用类型 let refType = attribute.replace(/\d/g, ''); // 尝试直接查找复数形式 if (!data[refType] && data[`${refType}s`]) { refType = `${refType}s`; } if (Array.isArray(entity[attribute])) { entity[attribute] = entity[attribute].map(item => data[refType]?.find(updateItem => updateItem.id === item.id) || item ); } else if (typeof entity[attribute] === "object" && entity[attribute] !== null) { entity[attribute] = data[refType]?.find(updateItem => updateItem.id === entity[attribute].id) || entity[attribute]; } } } } } return data; } // 解析单个实体的数据引用 /** * 解析数据引用关系 * * 该函数用于处理实体对象与数据源之间的引用关系,自动匹配并更新实体中的引用字段。 * * @param {Object} entity - 需要处理的实体对象 * @param {Object} data - 包含引用数据的数据源对象 * @returns {Object} 处理后的实体对象 * * 功能说明: * 1. 遍历实体对象的每个属性 * 2. 如果属性值是数组,则尝试在数据源中查找匹配项更新数组元素 * 3. 如果属性值是对象,则尝试在数据源中查找匹配项更新该对象 * 4. 自动处理单复数形式的数据源键名 */ function resolveDataReference(entity, data) { for (const attribute in entity) { if (entity.hasOwnProperty(attribute)) { // 修复:统一使用复数形式查找引用类型 let refType = attribute.replace(/\d/g, ''); // 尝试直接查找复数形式 if (!data[refType] && data[`${refType}s`]) { refType = `${refType}s`; } if (Array.isArray(entity[attribute])) { entity[attribute] = entity[attribute].map(item => data[refType]?.find(updateItem => updateItem.id === item.id) || item ); } else if (typeof entity[attribute] === "object" && entity[attribute] !== null) { entity[attribute] = data[refType]?.find(updateItem => updateItem.id === entity[attribute].id) || entity[attribute]; } } } return entity; } class LazyLoader { constructor(dataManager) { this.dataManager = dataManager; this.resolvedCache = new Map(); // 缓存已解析的实体 } /** * 创建实体代理 * @param {Object} entity 实体对象 * @param {string} entityType 实体类型 * @returns {Proxy} 返回代理后的实体 */ createProxy(entity, entityType) { const handler = { get: (target, prop) => { // 1. 处理特殊属性直接返回 if (prop.startsWith('_') || typeof target[prop] === 'function') { return target[prop]; } const value = target[prop]; // 2. 处理数组属性 if (Array.isArray(value)) { return value.map(item => this.resolveReference(item, prop) ); } // 3. 处理对象属性 if (typeof value === 'object' && value !== null) { return this.resolveReference(value, prop); } // 4. 返回普通属性值 return value; } }; return new Proxy(entity, handler); } /** * 解析引用关系(核心逻辑) * 根据resolveDataReference函数逻辑实现 */ resolveReference(ref, propName) { // 1. 检查缓存 const cacheKey = `${propName}_${ref.id}`; if (this.resolvedCache.has(cacheKey)) { return this.resolvedCache.get(cacheKey); } // 2. 确定引用类型(与resolveDataReference相同逻辑) let refType = propName.replace(/\d/g, ''); const rawData = this.dataManager._rawData; // 处理复数形式(与resolveDataReference相同) if (!rawData[refType] && rawData[`${refType}s`]) { refType = `${refType}s`; } // 3. 查找引用实体(与resolveDataReference相同) const refEntities = rawData[refType]; if (!refEntities) return ref; const resolved = refEntities.find(e => e.id === ref.id); if (!resolved) return ref; // 4. 创建代理并缓存 const proxy = this.createProxy(resolved, refType); this.resolvedCache.set(cacheKey, proxy); return proxy; } /** * 清除缓存(数据更新时调用) */ clearCache() { this.resolvedCache.clear(); } } class MiniProgramDataManager { // 修复:合并重复的构造函数 constructor(baseUrl) { this.baseUrl = baseUrl; this.debug = true; // 调试模式开关 this.requestCount = 0; // 请求计数器 // 数据结构定义 this._rawData = { bancais: [], dingdans: [], mupis: [], chanpins: [], kucuns: [], dingdan_bancais: [], chanpin_zujians: [], zujians: [], caizhis: [], dingdan_chanpins: [], users: [], jinhuos: [], _lastModified: null, _lastSync: null }; // 初始化网络状态 this.networkAvailable = false; this.checkNetwork().then(type => { this.networkAvailable = type !== 'none'; }); this.lazyLoader = new LazyLoader(this); // 仅从本地存储加载数据,不自动同步 this.loadDataFromStorage(); this.isSyncing = false; this.lastSync = null; this.callbacks = { all: [], bancais: [], dingdan: [], mupi: [], chanpin: [], kucun: [], chanpin_zujian: [], dingdan_bancai: [], zujian: [], caizhi: [], dingdan_chanpin: [], user: [], jinhuo: [] }; this.syncQueue = Promise.resolve(); this.entiyeText = { bancai: '板材已存在', dingdan: '订单已存在', mupi: '木皮已存在', chanpin: '产品已存在', kucun: '已有库存记录', chanpin_zujian: '产品已有该组件', dingdan_bancai: '', zujian: '组件已定义过了', caizhi: '材质已定义过了', dingdan_chanpin: '订单下已有该产品', user: '' }; this.syncInterval = 5 * 60 * 1000; // 5分钟 this.storageKey = 'miniProgramData'; // 本地存储的键名 } // 修改数据获取方法 get data() { const handler = { get: (target, prop) => { // 处理特殊属性 if (prop.startsWith('_')) { return target[prop]; } // 处理数组类型的实体集合 if (Array.isArray(target[prop])) { return target[prop].map(item => this.lazyLoader.createProxy(item, prop.replace(/s$/, '')) ); } return target[prop]; }, set: (target, prop, value) => { target[prop] = value; return true; } }; return new Proxy(this._rawData, handler); } // 添加显式初始化方法 async initialize() { // 启动自动同步 this.startAutoSync(); // 执行首次数据同步 await this.syncData(); } /** * 启动自动同步定时器 * 每隔syncInterval毫秒检查并执行数据同步 * 如果已有同步任务进行中则跳过 */ startAutoSync() { if (this.autoSyncTimer) clearInterval(this.autoSyncTimer); this.autoSyncTimer = setInterval(() => { if (!this.isSyncing) this.syncData(); }, this.syncInterval); } /** * 停止自动同步 */ stopAutoSync() { clearInterval(this.autoSyncTimer); } /** * 检查网络状态 */ checkNetwork() { return new Promise((resolve) => { wx.getNetworkType({ success: (res) => { resolve(res.networkType); }, fail: () => { resolve('unknown'); } }); }); } /** * 获取所有数据(全量或增量) * @async * @param {string} [since] - 增量获取的时间戳,不传则全量获取 * @returns {Promise<boolean>} 是否获取成功 * @description * - 根据since参数决定全量或增量获取数据 * - 增量获取时会合并新数据到现有数据 * - 全量获取会直接替换现有数据 * - 成功后会更新同步时间并保存到本地存储 * - 失败时会触发错误回调,若无历史数据则抛出错误 */ async fetchAll(since) { try { console.log(since ? `增量获取数据(自${since})...` : '全量获取数据...'); const params = since ? { since } : {}; const result = await this.request('/app/all', 'GET', params); const resolvedData =result ; // 更新networkData Object.keys(this._rawData).forEach(key => { if (key.startsWith('_')) return; if (resolvedData[key]) { if (since) { // 增量更新: 合并新数据到现有数据 resolvedData[key].forEach(newItem => { const index = this._rawData[key].findIndex(item => item.id === newItem.id); if (index >= 0) { this._rawData[key][index] = newItem; } else { this._rawData[key].push(newItem); } }); } else { // 全量更新: 直接替换 this._rawData[key] = resolvedData[key]; } } }); // 更新同步时间 this.lastSync = new Date(); this._rawData._lastSync = this.lastSync.toISOString(); // 保存到本地存储 this.saveDataToStorage(); this.triggerCallbacks('refresh', 'all', this.data); return true; } catch (error) { console.error('Fetch error:', error); this.triggerCallbacks('fetch_error', 'all', { error }); // 失败时尝试使用本地数据 if (!this.lastSync) { throw new Error('初始化数据获取失败'); } return false; } } /** * 微信小程序API请求封装 */ request(url, method = 'GET', data = null, retryCount = 3) { return new Promise((resolve, reject) => { const makeRequest = (attempt) => { const fullUrl = `${this.baseUrl}${url}`; if (this.debug) { console.log(`[请求] ${method} ${fullUrl}`, { attempt, data, timestamp: new Date().toISOString() }); } wx.request({ url: fullUrl, method, data, header: { 'Content-Type': 'application/json' }, success: (res) => { if (this.debug) { console.log(`[响应] ${fullUrl}`, { status: res.statusCode, data: res.data, headers: res.header }); } // 修复:更灵活的响应格式处理 if (!res.data) { const err = new Error('空响应数据'); if (attempt < retryCount) { this.retryRequest(makeRequest, attempt, retryCount, err); } else { reject(err); } return; } // 修复:支持多种成功状态码和响应格式 const isSuccess = res.statusCode >= 200 && res.statusCode < 300; const hasData = res.data && (res.data.data !== undefined || typeof res.data === 'object'); if (isSuccess && hasData) { resolve(res.data.data || res.data); } else { const errMsg = res.data.message || res.data.text || 'API错误'; const err = new Error(errMsg); if (attempt < retryCount) { this.retryRequest(makeRequest, attempt, retryCount, err); } else { reject(err); } } }, fail: (err) => { if (this.debug) { console.error(`[失败] ${fullUrl}`, err); } const error = new Error(`网络请求失败: ${err.errMsg || '未知错误'}`); if (attempt < retryCount) { this.retryRequest(makeRequest, attempt, retryCount, error); } else { reject(error); } } }); }; makeRequest(1); }); } retryRequest(makeRequest, attempt, retryCount, error) { const delay = 1000 * attempt; console.warn(`请求失败 (${attempt}/${retryCount}), ${delay}ms后重试:`, error.message); setTimeout(() => makeRequest(attempt + 1), delay); } /** * 注册回调函数 */ registerCallback(entity, callback) { if (!this.callbacks[entity]) { this.callbacks[entity] = []; } this.callbacks[entity].push(callback); } /** * 注销回调函数 */ unregisterCallback(entity, callback) { if (!this.callbacks[entity]) return; const index = this.callbacks[entity].indexOf(callback); if (index !== -1) { this.callbacks[entity].splice(index, 1); } } /** * 触发回调函数 */ triggerCallbacks(operation, entity, data) { this.callbacks.all.forEach(cb => cb(operation, entity, data)); if (this.callbacks[entity]) { this.callbacks[entity].forEach(cb => cb(operation, data)); } } /** * 检查重复实体 */ checkDuplicate(entity, data) { // 修复:确保引用已解析 const resolvedData = resolveDataReference(data, this.data); switch (entity) { case 'bancai': return this.data.bancais.some(b => b.houdu === resolvedData.houdu && b.caizhi?.id === resolvedData.caizhi?.id && b.mupi1?.id === resolvedData.mupi1?.id && b.mupi2?.id === resolvedData.mupi2?.id ); case 'caizhi': return this.data.caizhis.some(c => c.name === resolvedData.name); case 'mupi': return this.data.mupis.some(m => m.name === resolvedData.name && m.you === resolvedData.you); case 'chanpin': return this.data.chanpins.some(c => c.bianhao === resolvedData.bianhao); case 'zujian': return this.data.zujians.some(z => z.name === resolvedData.name); case 'dingdan': return this.data.dingdans.some(d => d.number === resolvedData.number); case 'chanpin_zujian': return this.data.chanpin_zujians.some(cz => cz.chanpin?.id === resolvedData.chanpin?.id && cz.zujian?.id === resolvedData.zujian?.id ); case 'dingdan_chanpin': return this.data.dingdan_chanpins.some(dc => dc.dingdan?.id === resolvedData.dingdan?.id && dc.chanpin?.id === resolvedData.chanpin?.id ); case 'dingdan_bancai': return this.data.dingdan_bancais.some(db => db.dingdan?.id === resolvedData.dingdan?.id && db.chanpin?.id === resolvedData.chanpin?.id && db.zujian?.id === resolvedData.zujian?.id && db.bancai?.id === resolvedData.bancai?.id ); case 'user': return this.data.users.some(u => u.name === resolvedData.name); default: return false; } } /** * CRUD操作通用方法 */ async crudOperation(operation, entity, data) { try { // 使用微信请求API替代fetch const result = await this.request(`/app/${operation}/${entity}`, 'POST', data); this.updateLocalData(operation, entity, result || data); this.triggerCallbacks(operation, entity, result || data); return result; } catch (error) { console.error('CRUD error:', error); this.triggerCallbacks(`${operation}_error`, entity, { data, error: error.message }); throw error; } } /** * 更新本地数据 */ /** * 更新本地数据 * @param {string} operation - 操作类型: 'add' | 'update' | 'delete' * @param {string} entity - 实体名称 * @param {Object} newData - 新数据对象(包含id字段) * @description 根据操作类型对本地数据进行增删改操作 */ updateLocalData(operation, entity, newData) { const key = `${entity}s`; const entities = this._rawData[key]; // 确保新数据的引用已解析 const resolvedData = resolveDataReference(newData, this._rawData); switch (operation) { case 'add': entities.push(resolvedData); break; case 'update': const index = entities.findIndex(item => item.id === resolvedData.id); if (index !== -1) { // 修复:使用对象展开操作符确保属性完整覆盖 entities[index] = { ...entities[index], ...resolvedData }; } else { entities.push(resolvedData); } break; case 'delete': const deleteIndex = entities.findIndex(item => item.id === resolvedData.id); if (deleteIndex !== -1) { entities.splice(deleteIndex, 1); } break; } // 更新最后修改时间 this._rawData._lastModified = new Date().toISOString(); this.lazyLoader.clearCache(); // 保存修改后的数据到本地存储 this.saveDataToStorage(); } /** * 同步数据 */ /** * 同步数据方法 * 该方法用于异步获取所有数据,并处理同步过程中的并发请求 * 如果同步正在进行中,会将请求标记为待处理(pendingSync) * 同步完成后会自动处理待处理的请求 * @async * @throws {Error} 当获取数据失败时会抛出错误并记录日志 */ async syncData() { if (this.isSyncing) { this.pendingSync = true; return; } this.isSyncing = true; try { // 1. 先加载本地数据 this.loadDataFromStorage(); // 2. 获取最后同步时间,用于增量更新 const since = this._rawData._lastSync || null; // 3. 获取增量数据 await this.fetchAll(since); // 4. 保存更新后的数据到本地存储 this.saveDataToStorage(); // 5. 触发数据更新回调 this.triggerCallbacks('refresh', 'all', this.data); } catch (error) { console.error('Sync failed:', error); this.triggerCallbacks('sync_error', 'all', { error }); // 失败时尝试使用本地数据 if (!this._rawData._lastSync) { throw new Error('初始化数据同步失败'); } } finally { this.isSyncing = false; if (this.pendingSync) { this.pendingSync = false; this.syncData(); } } } /** * 从本地存储加载数据 * 使用微信小程序的同步存储API获取之前保存的数据 */ loadDataFromStorage() { try { const storedData = wx.getStorageSync(this.storageKey); if (storedData) { // 修复:加载到_rawData而非data代理对象 this._rawData = storedData; } } catch (error) { console.error('加载本地存储数据失败:', error); // 提供默认空数据 this._rawData = { bancais: [], dingdans: [], mupis: [], chanpins: [], kucuns: [], dingdan_bancais: [], chanpin_zujians: [], zujians: [], caizhis: [], dingdan_chanpins: [], users: [], jinhuos: [], _lastModified: null, _lastSync: null }; } } /** * 保存数据到本地存储 * 使用微信小程序的同步存储API持久化当前数据 */ saveDataToStorage() { try { // 修复:保存_rawData而非localData wx.setStorageSync(this.storageKey, this._rawData); } catch (error) { console.error('保存数据到本地存储失败:', error); // 提示用户或执行降级策略 wx.showToast({ title: '数据保存失败,请稍后重试', icon: 'none' }); } } /** * 添加实体 */ /** * 添加实体数据 * @async * @param {string} entity - 实体类型 * @param {Object} data - 要添加的实体数据 * @returns {Promise} 返回CRUD操作结果 * @throws {Error} 如果数据已存在则抛出错误 */ async addEntity(entity, data) { if (this.checkDuplicate(entity, data)) { const errorMsg = `${this.entiyeText[entity]}`; this.triggerCallbacks('duplicate_error', entity, { data, error: errorMsg }); throw new Error(errorMsg); } return this.crudOperation('add', entity, data); } /** * 更新实体 */ async updateEntity(entity, data) { return this.crudOperation('update', entity, data); } /** * 删除实体 */ async deleteEntity(entity, id) { return this.crudOperation('delete', entity, { id }); } getBancaisForZujian(zujianId) { const dingdan_bancais = this.data.dingdan_bancais.filter(db => db.zujian?.id == zujianId); return dingdan_bancais.map(db => db.bancai).filter(Boolean); } /** * 获取板材的库存信息 */ getKucunForBancai(bancaiId) { return this.data.kucuns.find(k => k.bancai?.id == bancaiId); } } // 导出模块 module.exports = MiniProgramDataManager; [响应] http://192.168.1.4:8080/app/all {status: 200, data: {…}, headers: Proxy} index.js? [sm]:56 收到数据更新回调 index.js? [sm]:136 开始更新表格数据 MiniProgramDataManager.js? [sm]:359 Fetch error: TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'bancai' -> object with constructor 'Array' | index 0 -> object with constructor 'Object' --- property 'caizhi' closes the circle at JSON.stringify (<anonymous>) at li.updateTable (index.js? [sm]:138) at li.updatePageData (index.js? [sm]:109) at index.js? [sm]:57 at MiniProgramDataManager.js? [sm]:479 at Array.forEach (<anonymous>) at MiniProgramDataManager.triggerCallbacks (MiniProgramDataManager.js? [sm]:479) at MiniProgramDataManager._callee2$ (MiniProgramDataManager.js? [sm]:356) at s (regeneratorRuntime.js?forceSync=true:1) at Generator.<anonymous> (regeneratorRuntime.js?forceSync=true:1)(env: Windows,mp,1.06.2412050; lib: 3.8.10) _callee2$ @ MiniProgramDataManager.js? [sm]:359 s @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 Promise.then (async) asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 fetchAll @ MiniProgramDataManager.js? [sm]:319 onLoad @ index.js? [sm]:64 index.js? [sm]:56 收到数据更新回调 index.js? [sm]:136 开始更新表格数据 index.js? [sm]:67 数据加载失败: TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'bancai' -> object with constructor 'Array' | index 0 -> object with constructor 'Object' --- property 'caizhi' closes the circle at JSON.stringify (<anonymous>) at li.updateTable (index.js? [sm]:138) at li.updatePageData (index.js? [sm]:109) at index.js? [sm]:57 at MiniProgramDataManager.js? [sm]:479 at Array.forEach (<anonymous>) at MiniProgramDataManager.triggerCallbacks (MiniProgramDataManager.js? [sm]:479) at MiniProgramDataManager._callee2$ (MiniProgramDataManager.js? [sm]:360) at s (regeneratorRuntime.js?forceSync=true:1) at Generator.<anonymous> (regeneratorRuntime.js?forceSync=true:1)(env: Windows,mp,1.06.2412050; lib: 3.8.10)
最新发布
07-15
<think>我们遇到了一个错误:`Converting circular structure to JSON`,这通常是因为在尝试将包含循环引用的对象转换为JSON字符串时发生的。 从错误信息中可以看出,问题出现在`updateTable`函数中,具体是在尝试打印原始数据时(`console.log('原始数据:', JSON.stringify(data, null, 2));`)。 循环引用发生在`data`对象中,具体路径是:`data.bancai[0].caizhi`引用了某个对象,而这个对象又通过某种方式引回了`data`中的某个对象,形成了循环。 由于我们只需要打印数据用于调试,我们可以使用一个自定义的替换函数来避免循环引用。 但是,我们注意到在`updateTable`函数中,我们尝试将整个`data`对象进行字符串化,而`data`对象是通过`this.dataManager.data`获取的,而`dataManager.data`是一个代理对象,它可能包含循环引用。 我们可以修改调试打印部分,避免打印整个数据,或者使用一个去除循环引用的函数。 然而,考虑到小程序环境,我们可能不在正式环境中打印大量数据,我们可以先注释掉这行调试打印。 但是,为了调试,我们可以使用一个安全的字符串化函数,例如: ```javascript function safeStringify(obj, indent = 2) { let cache = []; const retVal = JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (cache.includes(value)) { // 移除循环引用 return; } cache.push(value); } return value; }, indent); cache = null; return retVal; } ``` 但是,注意:这个函数可能会丢失一些数据,但至少不会报错。 不过,我们也可以考虑在开发环境下才打印,并且只打印我们关心的部分,比如只打印数据的长度。 因此,我们可以修改为: ```javascript console.log('原始数据:', `订单数: ${data.dingdans?.length || 0}, 产品数: ${data.chanpins?.length || 0}, 板材数: ${data.bancais?.length || 0}, 库存记录数: ${data.kucuns?.length || 0}`); ``` 这样就不会有循环引用的问题了。 另外,我们注意到在`updateTable`函数中,我们尝试构建`results`数组,其中使用了`this.getMaterialInfo(bancai)`,而在这个函数中,我们访问了`bancai.caizhi`等属性,这可能是通过代理对象访问的,而代理对象在`get`陷阱中可能又访问了其他属性,导致循环。 因此,我们需要避免在调试时打印整个对象。 修改步骤: 1. 将`updateTable`函数中的`console.log('原始数据:', ...)`改为只打印各数组的长度。 2. 如果仍然需要详细数据,可以只打印前几个元素,或者使用一个安全字符串化函数。 但是,由于错误发生在`updateTable`函数中,并且是在打印之后,所以错误可能出现在其他地方。根据错误堆栈,它是在`updateTable`函数中调用`JSON.stringify`时发生的。 因此,我们先注释掉这行打印,或者用安全的方式打印。 我们选择注释掉这行打印,因为在小程序开发工具中,打印大量数据可能会导致性能问题。 修改`updateTable`函数: 将: console.log('原始数据:', JSON.stringify(data, null, 2)); 改为: // console.log('原始数据:', JSON.stringify(data, null, 2)); console.log('原始数据: 订单数:', data.dingdans?.length, '产品数:', data.chanpins?.length, '板材数:', data.bancais?.length, '库存记录数:', data.kucuns?.length); 这样我们就避免了循环引用的问题。 另外,我们注意到在构建`results`数组时,我们使用了`this.getMaterialInfo(bancai)`,这个函数会访问`bancai.caizhi`和`bancai.mupi1`等,这些属性可能是通过代理返回的,而代理又可能返回了其他代理,但不会导致循环引用,除非数据本身有循环。 因此,我们主要解决调试打印的问题。 但是,错误信息中显示的错误发生在`updateTable`函数中,也就是在打印的时候,所以先解决打印问题。 此外,我们还要注意,在`getMaterialInfo`函数中,我们使用了递归调用吗?没有,所以不会导致循环。但是,如果`bancai.caizhi`又引用了`bancai`,那就会循环。不过我们的数据设计应该不会这样。 所以,我们先修改打印方式。 另外,我们注意到在`updateTable`函数中,我们使用了`data.dingdan_bancais`,然后遍历它,并访问`db.dingdan`、`db.chanpin`等,这些也是代理对象,但不会导致循环引用,除非数据设计有循环。 因此,我们修改打印方式,然后观察是否还会出现循环引用错误。 如果后续在构建`results`时仍然出现循环引用,那么我们需要检查数据中是否存在循环引用,并确保在构建结果时避免访问循环引用的属性。 但是,我们的`results`数组中的每一项都是我们新创建的对象,不会包含循环引用(因为我们只取了基本类型的值)。所以问题可能只在打印时出现。 因此,我们先修改打印方式。 修改后的代码: ```javascript updateTable: function() { console.log('开始更新表格数据'); const data = this.dataManager.data; // 修改为只打印长度,避免循环引用 console.log('原始数据: 订单数:', data.dingdans?.length, '产品数:', data.chanpins?.length, '板材数:', data.bancais?.length, '库存记录数:', data.kucuns?.length); ... // 其余代码不变 } ``` 同时,我们注意到在`updateTable`函数中,我们构建了`results`数组,然后设置到`data`中,最后又调用了`this.updateCurrentPageData()`,而在`updateCurrentPageData`中,我们使用了`this.data.filteredResults`,这个数据是`results`的副本,然后我们进行了分页。 所以,我们还需要确保在构建结果时没有循环引用。但是,我们构建的每个结果项都是普通对象,没有循环引用,因为`raw`属性中我们只存储了原始值(数字、字符串等)。 因此,修改后应该不会出现循环引用错误。 另外,我们还要注意在`getMaterialInfo`函数中,我们返回的字符串是拼接的,不会包含对象,所以是安全的。 所以,我们进行上述修改。 但是,我们还有一个地方可能打印对象:在`updatePageData`函数中,我们调用了`this.updateTable()`,而在此之前,我们调用了`this.updateStats()`,这个函数只是更新几个统计数字,不会打印对象。 因此,我们主要修改`updateTable`中的打印。 现在,我们修改代码并重新运行。 另外,我们注意到错误信息中还有一次错误发生在数据加载失败的回调中,这是因为我们在`fetchAll`失败时抛出了错误,然后被`catch`捕获,然后再次尝试更新页面数据,导致再次调用`updateTable`,然后再次打印,再次出现循环引用。 因此,我们修改后,应该可以避免这个错误。 如果问题仍然存在,我们可能需要检查数据本身是否有循环引用,并在访问数据时避免使用会导致循环引用的方式。 但是,由于我们使用了`DataManager`的代理,它可能会在访问属性时动态解析引用,这可能导致循环。例如,当我们访问`bancai.caizhi`时,`caizhi`对象又被代理,而`caizhi`对象中如果有引用回`bancai`的属性,就会形成循环。 因此,我们需要确保数据设计不会出现循环引用,或者在代理中避免循环。 但是,在我们的数据模型中,`caizhi`不应该引用`bancai`,所以这种情况不应该发生。 因此,我们先修改打印方式,然后观察。 修改后,重新运行。 如果问题解决,我们就继续;如果还有问题,我们再进一步分析。 另外,我们还可以在打印时使用`console.dir`而不是`JSON.stringify`,因为`console.dir`可以处理循环引用,但是小程序开发工具的控制台可能不支持。 所以,我们选择只打印长度。 现在,我们修改代码。</think>我分析了您提供的代码和错误日志,问题出在数据循环引用导致的`Converting circular structure to JSON`错误。这个错误发生在`updateTable`函数中尝试将`data`对象转为JSON字符串时,因为数据中存在循环引用(`bancai`对象的`caizhi`属性引用了自身)。 ### 解决方案 问题主要出现在`updateTable`函数中,特别是数据打印部分。以下是完整的修复方案: ```javascript:pages/index/index.js // 修改 updateTable 函数 updateTable: function() { console.log('开始更新表格数据'); const data = this.dataManager.data; // 替换原来的打印语句 - 避免循环引用问题 console.log('原始数据: 订单数:', data.dingdans?.length, '产品数:', data.chanpins?.length, '板材数:', data.bancais?.length, '库存记录数:', data.kucuns?.length); let results = []; // 直接遍历dingdan_bancai表获取数据 if (data.dingdan_bancais) { data.dingdan_bancais.forEach(db => { // 获取关联信息 const dingdan = db.dingdan || {}; const chanpin = db.chanpin || {}; const zujian = db.zujian || {}; const bancai = db.bancai || {}; // 获取库存数量 const kucun = data.kucuns?.find(k => k.bancai && k.bancai.id === bancai.id); const stockQuantity = kucun ? kucun.shuliang : 0; // 安全获取材质名称 - 避免循环引用 const caizhiName = bancai.caizhi && typeof bancai.caizhi === 'object' ? bancai.caizhi.name : bancai.caizhi || '未知材质'; // 构建板材信息 const materialInfo = `${caizhiName}-${this.getMupiInfo(bancai)} (${bancai.houdu || '未知'}mm)`; // 构建结果项 results.push({ id: `${db.id}`, orderNumber: dingdan.number || '无', productInfo: chanpin.bianhao || '独立订购', productQuantity: db.shuliang, component: zujian.name || '独立板材', material: materialInfo, materialPerComponent: 1, // 直接订购时,每件用量为1 materialOrderQuantity: db.shuliang, stockQuantity: stockQuantity, raw: { thickness: bancai.houdu || 0 }, bancaiId: bancai.id }); }); } // ... 其余代码保持不变 ... }, // 添加新的辅助函数 - 安全获取木皮信息 getMupiInfo: function(bancai) { if (!bancai) return '无'; const formatMupi = (mupi) => { if (!mupi || typeof mupi !== 'object') return null; return `${mupi.name}${mupi.you ? '(油)' : ''}`; }; const mupi1Str = formatMupi(bancai.mupi1) || '无'; const mupi2Str = formatMupi(bancai.mupi2) || '无'; return mupi1Str + (mupi2Str !== '无' ? `/${mupi2Str}` : ''); }, // 修改 getMaterialInfo 函数 - 避免循环引用 getMaterialInfo: function(bancai) { if (!bancai) return '未知板材'; // 安全获取材质名称 const caizhiName = bancai.caizhi && typeof bancai.caizhi === 'object' ? bancai.caizhi.name : bancai.caizhi || '未知材质'; return `${caizhiName}-${this.getMupiInfo(bancai)} (${bancai.houdu || '未知'}mm)`; }, ``` ### 问题分析及修复说明 1. **循环引用问题**: - 原始代码中尝试使用`JSON.stringify(data)`打印整个数据对象 - 数据中存在循环引用(`bancai`对象的`caizhi`属性引用了自身) - **修复**:替换为安全的数据统计打印,只显示各数据集的长度 2. **数据访问方式**: - 原始代码中直接访问`bancai.caizhi.name`可能导致循环引用 - **修复**:添加类型检查和安全访问方式: ```javascript const caizhiName = bancai.caizhi && typeof bancai.caizhi === 'object' ? bancai.caizhi.name : bancai.caizhi || '未知材质'; ``` 3. **木皮信息提取**: - 添加专门的`getMupiInfo`函数安全提取木皮信息 - 避免直接访问可能包含循环引用的深层对象 4. **简化数据结构**: - 简化了`raw`对象,只保留必要的厚度信息 - 移除了可能导致循环引用的嵌套对象 ### 其他优化建议 1. **在MiniProgramDataManager.js中添加安全序列化方法**: ```javascript:MiniProgramDataManager.js // 添加安全序列化方法 safeStringify(obj) { const cache = new Set(); return JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (cache.has(value)) return '[Circular]'; cache.add(value); } return value; }, 2); } // 在需要打印的地方使用 console.log('Data:', this.safeStringify(data)); ``` 2. **优化数据加载**: ```javascript:pages/index/index.js onLoad: function() { // 添加加载状态 wx.showLoading({ title: '加载数据中' }); // 在数据加载完成后 .finally(() => { wx.hideLoading(); }); } ``` ### 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值