自己做h5网站东莞建筑业协会官网
自己做h5网站,东莞建筑业协会官网,网站制作方案要点,网站开发大多用什么编程语言1. 从静态展示到动态交互#xff1a;CircleProgress的潜力不止于此
很多刚开始用uniapp和uview的朋友#xff0c;可能都和我最初的想法一样#xff0c;觉得CircleProgress这个圆形进度条组件#xff0c;不就是显示个百分比嘛。传个percent值#xff0c;选个颜色#xff0…1. 从静态展示到动态交互CircleProgress的潜力不止于此很多刚开始用uniapp和uview的朋友可能都和我最初的想法一样觉得CircleProgress这个圆形进度条组件不就是显示个百分比嘛。传个percent值选个颜色往页面里一放完事。我刚开始做文件上传功能的时候也是这么干的进度条能转起来用户能看到进度就觉得已经可以了。但后来在几个真实项目里碰了钉子我才发现事情没那么简单。比如在一个健身App里用户完成每日训练目标后我们希望进度环能有一个“庆祝式”的动画从完成状态弹跳一下然后绽放出星星点点的效果。又比如在一个数据仪表盘里多个指标环需要联动一个环的增长会带动另一个环的变化形成一种数据流动的视觉叙事。这些场景下那个简单的、只会匀速转圈的进度环就显得有点“呆”了和用户之间缺乏更深层次的互动。其实uview的CircleProgress组件虽然API看起来简洁但它的底层是基于Canvas绘制的。Canvas是什么它就像一块画布给了我们极大的自由去控制每一帧的绘制。这意味着只要我们摸清了它的“脾气”就能在这块圆形画布上玩出很多花样实现那些能真正提升应用质感和用户体验的高级动画与交互。它绝不仅仅是一个“显示器”更可以成为一个“表演者”。这篇文章我就把自己在几个项目中折腾出来的关于CircleProgress高级动画和交互的心得和技巧掰开揉碎了分享给你。咱们不搞那些深奥的理论就聊怎么上手、怎么实现、怎么避开我踩过的那些坑。2. 核心动画技巧让进度“活”起来基础的进度变化组件已经通过duration参数帮我们做了线性动画。但线性动画看久了会显得机械。要让进度环更有生命力关键在于对动画过程进行更精细的控制。2.1 非线性动画与缓动函数直接跳变的进度很生硬而匀速线性动画又缺乏趣味。我们可以利用JavaScript的动画库如Tween.js或自己封装缓动函数来创造加速、减速、回弹等效果。举个例子我们希望进度从0%到80%的过程是先快后慢像汽车慢慢停稳一样。我们可以使用一个经典的“ease-out”缓动函数。这里我们不引入额外库自己实现一个简单的// 一个简单的 easeOutCubic 缓动函数 function easeOutCubic(t, b, c, d) { t / d; t--; return c * (t * t * t 1) b; } // 在vue的methods或setup中 startProgressAnimation(targetPercent) { const duration 2000; // 总动画时长2秒 const startPercent this.currentPercent; // 当前进度 const change targetPercent - startPercent; const startTime Date.now(); const animate () { const now Date.now(); const elapsed now - startTime; let progress elapsed / duration; if (progress 1) progress 1; // 应用缓动函数计算当前帧的进度值 const easedProgress easeOutCubic(progress, 0, 1, 1); this.currentPercent startPercent (change * easedProgress); if (progress 1) { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); }然后在模板里我们把动态计算出的currentPercent绑定给CircleProgress组件template view u-circle-progress :percentcurrentPercent :duration0 !-- 这里设为0因为我们自己控制动画 -- active-color#19be6b /u-circle-progress button clickstartProgressAnimation(80)开始缓动动画/button /view /template实测下来这样得到的进度增长动画末尾会有个非常自然的减速停顿比纯粹的线性动画看起来舒服多了。你可以尝试不同的缓动函数如easeInOutBack会产生一点回弹效果来匹配不同的操作感觉比如“完成”可以用弹跳“加载”可以用匀速“成功”可以用轻微 overshoot 再回落。2.2 分段动画与场景化设计有时候进度并非一次性完成而是分阶段的。例如一个任务包含“验证”、“处理”、“上传”三个阶段每个阶段占33%。我们可以让进度环在到达每个节点时有一个短暂的停顿或样式变化给用户清晰的阶段反馈。思路是将总目标进度拆分成一个动画队列。我们可以使用async/await配合setTimeout来模拟async function startMultiStageProgress() { const stages [ { percent: 33, label: 验证中..., color: #ff9900 }, { percent: 66, label: 处理中..., color: #19be6b }, { percent: 100, label: 上传完成, color: #2979ff } ]; for (let i 0; i stages.length; i) { const stage stages[i]; // 1. 更新进度值和提示文字 this.currentPercent stage.percent; this.stageLabel stage.label; // 2. 动态改变激活色注意active-color属性本身不支持动态响应需要技巧见下文 this.activeColor stage.color; // 3. 如果不是最后阶段等待一秒模拟该阶段耗时 if (i stages.length - 1) { await new Promise(resolve setTimeout(resolve, 1000)); } } }这里就引出一个关键点uview CircleProgress的active-color和inactive-color属性在文档中标注为“无法动态变更”。这是一个坑但并非无解。一个实用的技巧是通过v-if或key属性强制重新渲染组件。虽然会有一点性能开销但对于这种需要动态变色的场景是有效的template view !-- 通过key值变化当activeColor改变时组件会重新创建从而应用新的颜色 -- u-circle-progress v-ifshowProgress :keyprogress-${activeColor} :percentcurrentPercent :active-coloractiveColor :duration800 text classstage-label{{ stageLabel }}/text /u-circle-progress /view /template当activeColor变化时key值也随之变化Vue会认为这是一个新组件从而销毁旧的并创建新的新的组件就会使用我们传入的新颜色进行初始化绘制。这样我们就实现了分阶段变色动画。3. 高级交互实现与用户“对话”动画是单向的表演交互则是双向的对话。让CircleProgress能响应用户输入可以极大增强参与感。3.1 手势控制进度想象一下在一个音视频播放器里用户可以通过拖拽圆形进度条上的点来调节播放进度。虽然CircleProgress本身没有提供拖拽事件但我们可以通过在其上层覆盖一个透明的、可触摸的圆形区域来实现。首先我们需要一个容器来定位。利用Canvas的原理我们知道进度条的“圆点”位置可以通过三角函数计算出来。当用户触摸环形区域时我们计算触摸点相对于圆心的角度再换算成百分比。template view classinteractive-container !-- 进度条组件 -- u-circle-progress refcircleProgress :percentinteractivePercent :duration0 active-color#2979ff :width300 :border-width20 /u-circle-progress !-- 覆盖在上层的透明触摸层用绝对定位盖住进度条 -- view classtouch-overlay touchstartonTouchStart touchmoveonTouchMove touchendonTouchEnd /view /view /template script export default { data() { return { interactivePercent: 50, isInteracting: false }; }, methods: { getTouchAngle(event) { // 获取触摸点信息 const touch event.touches[0]; // 这里需要获取组件实际在屏幕上的位置和半径可能需要使用uni.createSelectorQuery() // 为简化示例假设圆心在(150,150)半径r140 (宽度300减去边框) const centerX 150; const centerY 150; const r 140; const clientX touch.clientX; const clientY touch.clientY; // 计算触摸点相对于圆心的坐标 const deltaX clientX - centerX; const deltaY centerY - clientY; // 注意Y轴方向页面坐标系与数学坐标系相反 // 计算角度弧度Math.atan2返回的是从X轴正方向开始的角度 let angle Math.atan2(deltaY, deltaX); // 转换为0到2π的范围如果为负则加2π if (angle 0) { angle 2 * Math.PI; } // 将弧度转换为百分比0-100 // 我们从顶部-π/2开始作为0%顺时针增长 // 先调整角度起点让 -π/2 对应 0 angle Math.PI / 2; if (angle 2 * Math.PI) { angle - 2 * Math.PI; } let percent (angle / (2 * Math.PI)) * 100; return percent; }, onTouchStart(event) { this.isInteracting true; this.interactivePercent this.getTouchAngle(event); }, onTouchMove(event) { if (!this.isInteracting) return; event.preventDefault(); // 防止页面滚动 this.interactivePercent this.getTouchAngle(event); // 这里可以触发一个自定义事件比如progress-changing通知父组件进度正在被手动修改 this.$emit(progress-changing, this.interactivePercent); }, onTouchEnd() { this.isInteracting false; // 触摸结束确认进度修改 this.$emit(progress-change, this.interactivePercent); } } }; /script style scoped .interactive-container { position: relative; width: 300rpx; height: 300rpx; } .touch-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border-radius: 50%; /* background-color: rgba(255,0,0,0.1); */ /* 调试时可以打开看区域 */ } /style这个例子提供了核心思路。在实际项目中你需要使用uni.createSelectorQuery()来动态获取CircleProgress组件真实的圆心位置和半径这样才能精准计算。实现后用户就能通过拖拽环形区域来直观控制进度了交互感直接拉满。3.2 与动态数据实时联动进度条不应该是一个孤立的显示元件而应该与后台数据、其他组件状态紧密联动。例如在一个显示多个系统健康指标的仪表盘中CPU、内存、磁盘三个圆形进度条需要实时更新。我们可以利用Vue的响应式特性结合定时器或WebSocket让进度条“活”起来。这里模拟一个从服务器轮询数据的场景template view classdashboard view classprogress-item v-foritem in systemMetrics :keyitem.name u-circle-progress :percentitem.value :active-colorgetColorByPercent(item.value) :width180 view classmetric-info text classmetric-name{{ item.name }}/text text classmetric-value{{ item.value }}%/text /view /u-circle-progress /view /view /template script export default { data() { return { systemMetrics: [ { name: CPU, value: 45 }, { name: 内存, value: 78 }, { name: 磁盘, value: 32 } ] }; }, mounted() { // 模拟每3秒从服务器获取一次数据 this.metricsTimer setInterval(() { this.fetchSystemMetrics(); }, 3000); this.fetchSystemMetrics(); // 初始化加载 }, beforeDestroy() { clearInterval(this.metricsTimer); }, methods: { async fetchSystemMetrics() { // 这里应该是真实的API调用我们模拟一下 try { // const res await uni.request({ url: /api/metrics }); // 模拟数据变化 const mockData this.systemMetrics.map(item ({ ...item, value: Math.min(100, Math.max(0, item.value (Math.random() * 10 - 5))) // 在原有值附近随机波动 })); // 使用Vue.set或直接赋值确保响应式更新 this.systemMetrics mockData; } catch (error) { console.error(获取指标失败:, error); } }, getColorByPercent(percent) { if (percent 50) return #19be6b; // 绿色健康 if (percent 80) return #ff9900; // 橙色警告 return #fa3534; // 红色危险 } } }; /script style scoped .dashboard { display: flex; justify-content: space-around; flex-wrap: wrap; } .progress-item { margin: 20rpx; text-align: center; } .metric-info { display: flex; flex-direction: column; align-items: center; } .metric-name { font-size: 24rpx; color: #606266; } .metric-value { font-size: 32rpx; font-weight: bold; margin-top: 8rpx; } /style这个例子中进度条不再是静态的它们成了系统状态的“脉搏”随着数据跳动。颜色也根据数值动态变化提供了更直观的视觉警示。这种实时联动让数据展示变得生动而有力。4. 复杂场景与性能优化当动画和交互变得复杂或者需要同时渲染多个进度环时性能问题就会浮现。尤其是在低端手机上卡顿会严重影响体验。这里分享几个我实战中总结的优化点。4.1 多进度环的联动与性能平衡前面提到的仪表盘如果同时要渲染十几个环每个环都有自己的动画压力就上来了。Canvas渲染虽然比DOM性能好但过量绘制依然会带来压力。优化策略一差异化更新。不是所有进度环都需要以最高频率如60fps更新。对于变化缓慢的数据如磁盘使用率一天才变几次可以设置更长的更新间隔或者只在数值真正发生变化时才重绘。我们可以给每个进度环数据增加一个needsUpdate的标记。优化策略二使用requestAnimationFrame统一更新。如果多个环需要同步平滑动画不要为每个环单独设置setInterval。应该用一个中心的动画循环来管理所有动画状态。// 在父组件中管理一个动画循环 data() { return { animatedMetrics: [], // 存放带目标值和当前值的对象 animationId: null }; }, methods: { updateMetrics(newMetrics) { // 将新数据与旧数据合并为目标值 this.animatedMetrics newMetrics.map(newItem { const oldItem this.animatedMetrics.find(item item.name newItem.name); return { name: newItem.name, targetValue: newItem.value, currentValue: oldItem ? oldItem.currentValue : newItem.value }; }); // 如果动画循环没启动则启动它 if (!this.animationId) { this.animateAllProgress(); } }, animateAllProgress() { const animate () { let allFinished true; const damping 0.1; // 缓动系数值越小动画越平滑但越慢 this.animatedMetrics.forEach(item { const diff item.targetValue - item.currentValue; if (Math.abs(diff) 0.1) { // 设置一个最小阈值 allFinished false; // 使用缓动公式逼近目标值 item.currentValue diff * damping; } else { item.currentValue item.targetValue; } }); // 触发视图更新在Vue中直接赋值给绑定到子组件的数据即可 // 这里假设 this.metricsForDisplay 是绑定到子组件的数据 this.metricsForDisplay this.animatedMetrics.map(item ({ name: item.name, value: Math.round(item.currentValue) // 四舍五入取整显示 })); if (!allFinished) { this.animationId requestAnimationFrame(animate); } else { this.animationId null; // 所有动画完成停止循环 } }; this.animationId requestAnimationFrame(animate); } }这样无论有多少个进度环需要动画我们都只使用一个浏览器原生动画循环所有计算集中进行效率更高动画也更同步、更平滑。4.2 自定义Canvas绘制以突破组件限制uview的CircleProgress组件提供了不错的默认样式但如果你需要更极致的定制比如渐变色进度条、图片纹理填充、动态变化的背景图案或者是在进度条上绘制刻度、标记点那么直接使用原生Canvas API在组件基础上进行“二次绘制”或者完全自己封装一个可能是更终极的方案。思路是获取Canvas上下文进行自定义绘制。虽然uview的组件没有直接暴露内部的Canvas上下文但我们可以通过一个小技巧在组件的slot里放入一个canvas标签并绝对定位到与进度环相同的位置和大小然后在上面绘制我们想要的自定义内容。template view classcustom-progress-wrapper !-- uview的进度环作为底层 -- u-circle-progress :percentpercent :widthcanvasSize :border-widthborderWidth inactive-colortransparent !-- 将底色设为透明以便显示我们自定义的底层 -- active-color#ffffff !-- 激活色设为白色覆盖在我们自定义的渐变层上 -- :durationduration /u-circle-progress !-- 自定义的Canvas层绘制渐变背景 -- canvas :idcanvasBg-${uid} :canvas-idcanvasBg-${uid} classcustom-canvas :style{ width: canvasSize rpx, height: canvasSize rpx } /canvas /view /template script export default { data() { return { uid: Math.random().toString(36).substr(2), // 生成唯一ID防止多实例冲突 canvasSize: 300, borderWidth: 20, percent: 75, duration: 1000 }; }, mounted() { // 等待组件渲染完成后绘制Canvas this.$nextTick(() { setTimeout(() this.drawCustomBackground(), 100); // 加一点延迟确保Canvas节点已准备好 }); }, methods: { drawCustomBackground() { // 获取Canvas上下文 const ctx uni.createCanvasContext(canvasBg-${this.uid}, this); const center this.canvasSize / 2; const radius (this.canvasSize - this.borderWidth) / 2; // 1. 创建环形渐变 const gradient ctx.createCircularGradient(center, center, radius); gradient.addColorStop(0, #8a2be2); // 紫色 gradient.addColorStop(1, #1e90ff); // 蓝色 // 2. 绘制一个完整的、带渐变的圆环作为背景 ctx.setLineWidth(this.borderWidth); ctx.setStrokeStyle(gradient); ctx.arc(center, center, radius, 0, 2 * Math.PI); ctx.stroke(); // 3. 绘制刻度线可选 ctx.setLineWidth(2); ctx.setStrokeStyle(#ffffff); for (let i 0; i 12; i) { const angle (i * Math.PI) / 6; // 每30度一个刻度 const startX center (radius - 10) * Math.cos(angle); const startY center (radius - 10) * Math.sin(angle); const endX center radius * Math.cos(angle); const endY center radius * Math.sin(angle); ctx.beginPath(); ctx.moveTo(startX, startY); ctx.lineTo(endX, endY); ctx.stroke(); } ctx.draw(); } } }; /script style scoped .custom-progress-wrapper { position: relative; display: inline-block; } .custom-canvas { position: absolute; top: 0; left: 0; pointer-events: none; /* 确保Canvas不拦截触摸事件 */ } /style这个方案相当于我们把uview的进度条当作一个“蒙版”它白色的进度部分会覆盖在我们自定义的彩色渐变背景上从而呈现出渐变色的进度效果。同时我们还在背景上绘制了刻度线。这种方法给了我们几乎无限的定制能力但复杂度也更高需要处理好图层顺序和绘制时机。5. 实战案例构建一个智能健身进度环最后我们把这些技巧综合起来做一个贴近实际需求的案例一个智能健身App的今日训练进度环。这个环需要显示今日目标完成百分比。点击环可以快速记录一次训练。完成目标时100%有庆祝动画环放大缩小、颜色闪烁。环内显示消耗的卡路里和训练时间。template view classfitness-container view classprogress-wrapper taphandleTap !-- 主进度环 -- u-circle-progress refmainProgress :percentcurrentPercent :active-colorprogressColor :inactive-color#f0f0f0 :width320 :border-width24 :duration300 :typenull view classinner-content text classcalories{{ calories }} kcal/text text classtime{{ minutes }} 分钟/text text classhint v-if!isGoalReached点击圆环记录训练/text text classcongrats v-else 目标达成/text /view /u-circle-progress !-- 庆祝动画的粒子效果层简化版用多个小圆点模拟 -- view classparticles v-ifshowParticles view classparticle v-for(p, index) in particles :keyindex :style{ left: p.x px, top: p.y px, width: p.size px, height: p.size px, backgroundColor: p.color, opacity: p.opacity } /view /view /view button classadd-button tapaddWorkout 添加一次训练/button /view /template script export default { data() { return { currentPercent: 30, // 初始进度 calories: 180, minutes: 25, isGoalReached: false, progressColor: #19be6b, showParticles: false, particles: [] }; }, watch: { // 监听进度变化达到100%时触发庆祝效果 currentPercent(newVal) { if (newVal 100 !this.isGoalReached) { this.isGoalReached true; this.celebrateGoal(); } else if (newVal 100) { this.isGoalReached false; this.showParticles false; } // 根据进度动态改变颜色 if (newVal 50) { this.progressColor #ff9900; } else if (newVal 80) { this.progressColor #19be6b; } else { this.progressColor #2979ff; } } }, methods: { handleTap() { // 点击圆环区域也触发添加训练 this.addWorkout(); }, addWorkout() { // 模拟添加一次训练增加随机卡路里和时间 const newCalories this.calories Math.floor(Math.random() * 50) 20; const newMinutes this.minutes Math.floor(Math.random() * 10) 5; // 计算新进度假设目标300卡路里或60分钟 const calorieGoal 300; const timeGoal 60; const calorieProgress Math.min((newCalories / calorieGoal) * 100, 100); const timeProgress Math.min((newMinutes / timeGoal) * 100, 100); // 取两者中较高的作为总进度 const newPercent Math.max(calorieProgress, timeProgress); // 使用缓动动画更新进度 this.animateToNewPercent(newPercent); this.calories newCalories; this.minutes newMinutes; }, animateToNewPercent(targetPercent) { // 简单的线性动画实际可用前面介绍的缓动函数 const startPercent this.currentPercent; const duration 500; const startTime Date.now(); const animate () { const now Date.now(); const elapsed now - startTime; let progress elapsed / duration; if (progress 1) progress 1; this.currentPercent startPercent (targetPercent - startPercent) * progress; if (progress 1) { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); }, celebrateGoal() { // 1. 进度环放大缩小动画通过CSS动画 const progressEl this.$refs.mainProgress; // 这里可以通过操作DOM样式实现但uview组件可能不易直接获取DOM。更实际的做法是包装一层view做动画。 // 我们通过改变外层容器的transform来实现 this.showParticles true; this.generateParticles(); // 3秒后隐藏粒子 setTimeout(() { this.showParticles false; this.particles []; }, 3000); }, generateParticles() { const particleCount 30; const newParticles []; const centerX 160; // 圆心坐标相对于320宽度 const centerY 160; const radius 140; // 圆环半径 for (let i 0; i particleCount; i) { const angle Math.random() * 2 * Math.PI; const distance radius Math.random() * 40; // 从圆环边缘向外 const speed 1 Math.random() * 2; newParticles.push({ x: centerX Math.cos(angle) * distance, y: centerY Math.sin(angle) * distance, size: 4 Math.random() * 8, color: hsl(${Math.random() * 360}, 100%, 60%), opacity: 0.8 Math.random() * 0.2, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 1.0 // 粒子生命值 }); } this.particles newParticles; this.animateParticles(); }, animateParticles() { const animate () { let allDead true; this.particles.forEach(p { p.life - 0.02; // 生命值递减 if (p.life 0) { allDead false; p.x p.vx; p.y p.vy; p.vy 0.05; // 模拟重力 p.opacity p.life; } }); if (!allDead this.showParticles) { // 需要触发视图更新这里简单重新赋值数组 this.particles [...this.particles]; requestAnimationFrame(animate); } }; requestAnimationFrame(animate); } } }; /script style scoped .fitness-container { display: flex; flex-direction: column; align-items: center; padding: 40rpx; } .progress-wrapper { position: relative; margin-bottom: 40rpx; } .inner-content { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; } .calories { font-size: 48rpx; font-weight: bold; color: #333; } .time { font-size: 28rpx; color: #666; margin-top: 10rpx; } .hint, .congrats { font-size: 24rpx; color: #999; margin-top: 20rpx; } .congrats { color: #ff9900; font-weight: bold; } .particles { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; } .particle { position: absolute; border-radius: 50%; } .add-button { background-color: #2979ff; color: white; border: none; border-radius: 50rpx; padding: 20rpx 60rpx; font-size: 32rpx; } /style这个案例融合了动态数据绑定、交互反馈点击、条件渲染庆祝文字、状态驱动的样式变化颜色、以及简单的粒子动画。它不再是一个冰冷的进度显示器而是一个有反馈、有情感、鼓励用户完成目标的互动式组件。在实际开发中粒子动画部分可能会因为频繁的DOM操作影响性能如果遇到性能问题可以考虑用Canvas来绘制粒子效果和性能都会更好。但上面的方案在小数量粒子下对于大部分场景已经足够且更易于理解和实现。