/* 文件路径: 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)
最新发布