最近在做一个文玩类小程序,核心需求是让用户能自由在手机上搭配手串。下面直接从功能维度梳理一下,这个项目到底完成了哪些事,并附上关键代码片段。
功能全景:用户能做哪些操作?
整个功能围绕"串珠子"这个核心行为展开,覆盖了从选材到保存的全流程。
1️⃣ 珠子添加(飞入动画)

用户点击底部珠子库中的任意珠子,珠子会从屏幕下方飞入手串的空缺位置。
飞入过程约0.4秒,带有透明度渐变
落点精确对应手串上的预留角度
动画结束后珠子自动归位到手串序列中
javascript
startBeadFlightAnimation(bead, callback) {
const startX = this.data.windowWidth / 2;
const startY = 800 / 750 * this.data.windowWidth;
const targetPos = this.calculateTargetPosition(bead.targetAngle);
this.setData({
flyingBead: { show: true, x: startX, y: startY, opacity: 0 }
});
setTimeout(() => {
this.setData({
flyingBead: { show: true, x: targetPos.x, y: targetPos.y, opacity: 0.8 }
});
}, 100);
setTimeout(() => {
this.setData({ flyingBead: { show: false } });
callback && callback();
}, 500);
}
2️⃣ 环形自动排布

所有珠子按照极坐标分布在圆形手串上,无需手动对齐。
根据珠子数量自动计算间距
若珠子总体积超过手串周长,自动等比缩小每颗珠子
每颗珠子记录独立的角度坐标,旋转时不乱位
核心代码(动态缩放与角度分配):
javascript
handleMultipleBeads(n, handchain, newBead) {
let allLength = 0;
for (let i = 0; i < n; i++) {
allLength += handchain[i].radius * 2 * 10;
}
allLength += newBead.radius * 2 * 10;
const perimeter = 510 * Math.PI;
let scale = perimeter / allLength;
if (scale > 1) scale = 1;
let spacing = (360 - (allLength * scale / perimeter * 360)) / (n + 1);
this.setData({ scale });
// 为每个珠子分配新的targetAngle
let accumulated = 0;
for (let i = 0; i < n; i++) {
handchain[i].targetAngle = 300 - (n - i) * spacing - accumulated;
accumulated += handchain[i].radius * scale * 10 / (perimeter / 360);
}
}
3️⃣ 单指旋转查看

用户用手指在手串区域滑动,整串手串会跟随手指方向旋转。
360°无死角查看
旋转角度实时跟随手指位移
旋转过程中所有珠子保持相对位置不变
核心代码(旋转角度增量计算):
javascript
onTouchMove(e) {
const touch = e.touches[0];
const currentAngle = this.getAngle(touch.clientX, touch.clientY);
let angleDiff = currentAngle - this.currentAngle;
if (angleDiff > 180) angleDiff -= 360;
if (angleDiff < -180) angleDiff += 360;
const newHandchain = this.data.handchain.map(bead => ({
...bead,
targetAngle: bead.targetAngle + angleDiff
}));
this.setData({
handchain: newHandchain,
totalAngle: this.data.totalAngle + angleDiff
});
this.currentAngle = currentAngle;
}
4️⃣ 拖拽互换位置

长按某颗珠子,可以将其拖拽到另一个位置,其他珠子会自动让位。
系统自动判断顺时针还是逆时针交换更近
被拖拽的珠子半透明跟随手指
其余珠子平滑移动到新位置,无闪烁
核心代码(最短路径交换逻辑):
javascript
// 判断顺时针距离 vs 逆时针距离
const clockwiseDist = (targetArea - originalArea + n) % n;
const counterClockwiseDist = (originalArea - targetArea + n) % n;
if (clockwiseDist <= counterClockwiseDist) {
// 顺时针逐个交换相邻珠子角度
for (let i = originalArea; i !== targetArea; i = (i + 1) % n) {
swapAdjacentAngles(sortedHandchain, i, (i + 1) % n);
}
} else {
// 逆时针逐个交换
for (let i = originalArea; i !== targetArea; i = (i - 1 + n) % n) {
swapAdjacentAngles(sortedHandchain, i, (i - 1 + n) % n);
}
}
5️⃣ 拖远删除
将珠子拖到手串圆圈之外(超过设定阈值),松手即可删除。
删除后剩余珠子自动重新排列,填补空缺
有明确的视觉边界提示
核心代码(距离判断与删除):
javascript
touchBeadEnd(e) {
const distance = Math.sqrt(
Math.pow(this.data.draggingBead.x - this.data.handchainInfo.centerX, 2) +
Math.pow(this.data.draggingBead.y - this.data.handchainInfo.centerY, 2)
);
const maxDistance = 350 / 750 * this.data.windowWidth;
if (distance > maxDistance) {
this.removeBead(e); // 调用删除方法
} else {
// 复位珠子可见性
}
}
6️⃣ 状态持久化
用户搭配好的手串可以保存,下次进入自动恢复。
一键保存到本地缓存
支持加载上一次设计
支持一键重置清空
核心代码(存储与读取):
javascript
saveDesign() {
const design = { handchain: this.data.handchain, saveTime: new Date().toLocaleString() };
wx.setStorageSync('handchainDesign', design);
wx.showToast({ title: '设计已保存', icon: 'success' });
}
loadSavedDesign() {
const saved = wx.getStorageSync('handchainDesign');
if (saved && saved.handchain) {
this.setData({ handchain: saved.handchain });
}
}
交互闭环:从选择到完成
整个功能形成了一个完整的用户操作链路:
浏览珠子库 → 点击添加(飞入动画) → 旋转查看 → 拖拽调整顺序 → 拖远删除不满意的 → 保存设计
每一步操作都有对应的视觉反馈,用户无需学习成本,凭直觉即可完成一套手串的DIY。
技术要点简述
布局算法:极坐标 + 动态缩放,解决环形排列问题
动画系统:CSS transition + setTimeout 控制飞入和交换动画
拖拽逻辑:触摸点转角度 → 判断所在区间 → 最短路径交换
旋转处理:触摸角度差累加到所有珠子的目标角度
性能优化:减少setData频率,使用局部更新
总结
这套手串DIY功能从零开始搭建,完成了从交互设计到动画实现的全链路开发。核心价值在于:把一个传统的"选配"流程,变成了一个直观好玩的"动手"体验。
如果你也在做类似的商品自定义搭配功能,希望这份功能清单和代码片段能给你一些参考。欢迎评论区交流 👋