PSI进销存系统移动端小程序开发实战
移动端方案概述
PSI进销存系统支持多种移动端接入方式,包括微信小程序、H5移动端和原生APP。本文介绍移动端小程序的设计与实现,帮助企业实现随时随地的业务管理。
技术架构
移动端采用小程序 + H5混合开发模式:
| 终端类型 | 技术方案 | 适用场景 |
|---|---|---|
| 微信小程序 | Taro/uni-app | 快速上手、微信生态 |
| H5移动端 | Vue3 + Vant | 跨平台、无需审核 |
| 原生APP | Flutter/React Native | 性能要求高、离线使用 |
微信小程序开发
项目结构
psi-miniapp/ ├── src/ │ ├── pages/ │ │ ├── dashboard/ # 首页/控制台 │ │ ├── inventory/ # 库存管理 │ │ ├── purchase/ # 采购管理 │ │ ├── sales/ # 销售管理 │ │ ├── orders/ # 订单列表 │ │ ├── scan/ # 扫码功能 │ │ └── settings/ # 设置 │ ├── components/ │ │ ├── product-card/ # 商品卡片 │ │ ├── order-item/ # 订单项 │ │ ├── number-input/ # 数字输入 │ │ └── scan-button/ # 扫码按钮 │ ├── services/ │ │ ├── api.js # API封装 │ │ ├── auth.js # 认证服务 │ │ └── storage.js # 本地存储 │ ├── utils/ │ │ ├── format.js # 格式化工具 │ │ └── validation.js # 校验工具 │ └── app.js ├── project.config.json └── package.json
API服务封装
// services/api.js
const API_BASE_URL = 'https://api.psi.example.com';
const TOKEN_KEY = 'psi_token';
class ApiService {
request(options) {
const token = wx.getStorageSync(TOKEN_KEY);
return new Promise((resolve, reject) => {
wx.request({
url: API_BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...options.header
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else if (res.statusCode === 401) {
// token过期,重新登录
this.handleUnauthorized();
reject(new Error('未授权'));
} else {
reject(res.data);
}
},
fail: (err) => {
wx.showToast({
title: '网络请求失败',
icon: 'none'
});
reject(err);
}
});
});
}
// 登录
login(code) {
return this.request({
url: '/auth/login',
method: 'POST',
data: { code }
});
}
// 获取商品列表
getProducts(params) {
return this.request({
url: '/products',
data: params
});
}
// 创建销售订单
createSalesOrder(data) {
return this.request({
url: '/sales-orders',
method: 'POST',
data
});
}
// 扫码查询商品
scanProduct(barcode) {
return this.request({
url: '/products/barcode/' + barcode
});
}
}
export default new ApiService();
扫码出入库功能
// pages/scan/index.js
import ApiService from '../../services/api';
Page({
data: {
scanHistory: [],
currentProduct: null,
mode: 'in' // in:入库, out:出库
},
// 扫码
async handleScan() {
try {
const result = await wx.scanCode({
onlyFromCamera: true,
scanType: ['barCode', 'qrCode']
});
wx.showLoading({ title: '查询中...' });
const product = await ApiService.scanProduct(result.result);
this.setData({
currentProduct: product,
scanHistory: [product, ...this.data.scanHistory.slice(0, 9)]
});
wx.hideLoading();
} catch (err) {
wx.showModal({
title: '提示',
content: '未找到对应商品,是否手动输入?',
success: (res) => {
if (res.confirm) {
this.showManualInput();
}
}
});
}
},
// 确认出入库
async confirmStockChange() {
const { currentProduct, mode, quantity } = this.data;
if (!quantity || quantity <= 0) {
wx.showToast({
title: '请输入有效数量',
icon: 'none'
});
return;
}
try {
wx.showLoading({ title: '处理中...' });
const apiName = mode === 'in' ? 'stockIn' : 'stockOut';
await ApiService[apiName]({
productId: currentProduct.id,
quantity: parseInt(quantity),
warehouseId: this.data.warehouseId,
remark: this.data.remark
});
wx.showToast({
title: mode === 'in' ? '入库成功' : '出库成功',
icon: 'success'
});
// 清空当前商品
this.setData({
currentProduct: null,
quantity: ''
});
// 触发库存页面刷新
const pages = getCurrentPages();
const inventoryPage = pages.find(p => p.route === 'pages/inventory/index');
if (inventoryPage) {
inventoryPage.onShow();
}
} catch (err) {
wx.showToast({
title: err.message || '操作失败',
icon: 'none'
});
} finally {
wx.hideLoading();
}
},
// 切换模式
switchMode(e) {
this.setData({
mode: e.currentTarget.dataset.mode
});
}
});
离线数据同步
// services/sync.js
class OfflineSync {
constructor() {
this.pendingRequests = []; // 待同步请求
this.isSyncing = false;
}
// 保存离线操作
saveOfflineOperation(type, data) {
const operation = {
id: this.generateId(),
type,
data,
timestamp: Date.now(),
status: 'pending'
};
// 保存到本地存储
const pending = wx.getStorageSync('pending_operations') || [];
pending.push(operation);
wx.setStorageSync('pending_operations', pending);
// 尝试同步
this.trySync();
}
// 同步离线数据
async trySync() {
if (this.isSyncing) return;
const pending = wx.getStorageSync('pending_operations') || [];
if (pending.length === 0) return;
// 检查网络状态
const networkType = await this.getNetworkType();
if (networkType === 'none') {
console.log('无网络,延迟同步');
return;
}
this.isSyncing = true;
for (const operation of pending) {
try {
await this.syncOperation(operation);
// 同步成功,移除
this.removePendingOperation(operation.id);
} catch (err) {
console.error('同步失败:', err);
// 标记失败
this.markOperationFailed(operation.id, err.message);
}
}
this.isSyncing = false;
// 通知UI更新
wx.eventCenter.trigger('sync_complete');
}
// 同步单个操作
async syncOperation(operation) {
const apiMap = {
'stock_in': ApiService.stockIn,
'stock_out': ApiService.stockOut,
'order_create': ApiService.createOrder,
'order_update': ApiService.updateOrder
};
const apiFunc = apiMap[operation.type];
if (!apiFunc) {
throw new Error(`未知操作类型: ${operation.type}`);
}
return await apiFunc(operation.data);
}
// 获取网络状态
getNetworkType() {
return new Promise((resolve) => {
wx.getNetworkType({
success: (res) => resolve(res.networkType)
});
});
}
// 监听网络状态变化
watchNetworkChange() {
wx.onNetworkStatusChange((res) => {
if (res.isConnected) {
this.trySync();
}
});
}
}
移动端库存查询
// pages/inventory/index.js
Page({
data: {
inventoryList: [],
warehouseList: [],
currentWarehouse: null,
searchKeyword: '',
loading: false,
refreshing: false,
page: 1,
pageSize: 20,
hasMore: true
},
onLoad() {
this.loadWarehouses();
},
onShow() {
this.loadInventory();
},
// 加载仓库列表
async loadWarehouses() {
try {
const warehouses = await ApiService.getWarehouses();
this.setData({
warehouseList: warehouses,
currentWarehouse: warehouses[0]
});
} catch (err) {
console.error('加载仓库失败:', err);
}
},
// 加载库存数据
async loadInventory(loadMore = false) {
if (!loadMore) {
this.setData({ page: 1, loading: true });
}
try {
const params = {
warehouseId: this.data.currentWarehouse?.id,
keyword: this.data.searchKeyword,
page: this.data.page,
pageSize: this.data.pageSize
};
const result = await ApiService.getInventory(params);
this.setData({
inventoryList: loadMore
? [...this.data.inventoryList, ...result.items]
: result.items,
hasMore: result.hasMore,
page: this.data.page + 1,
loading: false,
refreshing: false
});
} catch (err) {
this.setData({ loading: false, refreshing: false });
wx.showToast({
title: '加载失败',
icon: 'none'
});
}
},
// 下拉刷新
onPullDownRefresh() {
this.setData({ refreshing: true });
this.loadInventory();
},
// 上拉加载更多
onReachBottom() {
if (this.data.hasMore && !this.data.loading) {
this.loadInventory(true);
}
},
// 切换仓库
switchWarehouse(e) {
const warehouse = e.currentTarget.dataset.warehouse;
this.setData({ currentWarehouse: warehouse });
this.loadInventory();
},
// 搜索
onSearch(e) {
this.setData({ searchKeyword: e.detail.value });
this.loadInventory();
},
// 查看商品详情
goToDetail(e) {
const productId = e.currentTarget.dataset.id;
wx.navigateTo({
url: `/pages/product/detail?id=${productId}`
});
}
});
消息推送集成
集成微信消息推送,实现业务提醒:
// services/notify.js
class NotifyService {
// 订阅消息
async subscribeMessage(templateId) {
return new Promise((resolve, reject) => {
wx.requestSubscribeMessage({
tmplIds: [templateId],
success: (res) => {
resolve(res[templateId]);
},
fail: reject
});
});
}
// 订单状态变化通知
async notifyOrderStatus(order, status) {
const templateId = 'ORDER_STATUS_TEMPLATE_ID';
const result = await this.subscribeMessage(templateId);
if (result !== 'accept') return;
await ApiService.sendNotify({
templateId,
data: {
thing1: { value: order.orderNo },
phrase2: { value: this.getStatusText(status) },
time3: { value: new Date().toLocaleString() }
},
page: `/pages/order/detail?id=${order.id}`
});
}
getStatusText(status) {
const map = {
pending: '待审核',
approved: '已审核',
processing: '处理中',
completed: '已完成',
cancelled: '已取消'
};
return map[status] || status;
}
// 库存预警通知
async notifyLowStock(product, currentStock) {
await ApiService.sendNotify({
templateId: 'LOW_STOCK_TEMPLATE_ID',
data: {
thing1: { value: product.name },
number2: { value: currentStock },
thing3: { value: `仓库: ${product.warehouseName}` }
},
page: `/pages/product/detail?id=${product.id}`
});
}
}
性能优化
- 分包加载:将不常用的页面放入分包,减少主包体积
- 图片优化:使用CDN加速,小程序端使用合适的图片格式
- 接口缓存:对不常变化的数据进行本地缓存
- 长列表优化:使用虚拟列表处理大量数据
- 骨架屏:加载过程中显示骨架,提升感知速度
总结
PSI移动端小程序提供了完整的进销存管理功能,包括扫码出入库、库存查询、订单管理等。通过离线数据同步和消息推送机制,确保用户在各种网络环境下都能高效工作。