UniApp实现漂亮的音乐歌词滚动播放效果

在现代的音乐播放应用中,歌词的展示和滚动播放已经成为了一个非常常见的功能。今天,我们将通过UniApp来实现一个漂亮的歌词滚动播放功能。我们将使用UniApp提供的组件和API来完成这个任务。

页面结构

在页面的模板部分,我们需要创建一个音频播放器和歌词展示区域。使用<scroll-view>组件来实现歌词的滚动效果。

html 复制代码
<template>
  <view class="audio-container">
    <!-- 音频播放器 -->
    <view class="audio-player">
      <audio :src="audioSrc" @timeupdate="updateTime" @ended="audioEnded"></audio>
      <view class="controls">
        <button @click="playAudio">播放</button>
        <button @click="pauseAudio">暂停</button>
      </view>
      <view class="time">
        {{ currentTime }} / {{ duration }}
      </view>
    </view>

    <!-- 歌词展示区域 -->
    <scroll-view class="lyrics" scroll-y :scroll-top="scrollTop">
      <view v-for="(line, index) in lyrics" :key="index" :class="{ active: currentLineIndex === index }">
        {{ line.text }}
      </view>
    </scroll-view>
  </view>
</template>

脚本逻辑

在脚本部分,我们需要处理音频的播放、暂停、时间更新等事件,并根据当前播放时间更新歌词的显示和滚动位置。

javascript 复制代码
<script>
export default {
  data() {
    return {
      audioSrc: 'https://example.com/audio.mp3', // 音频文件地址
      lyrics: [
        { time: 0, text: '第一行歌词' },
        { time: 5000, text: '第二行歌词' },
        { time: 10000, text: '第三行歌词' },
        // 更多歌词行...
      ],
      currentTime: '00:00', // 当前播放时间
      duration: '00:00', // 音频总时长
      currentLineIndex: 0, // 当前高亮的歌词行索引
      scrollTop: 0, // 歌词滚动位置
    };
  },
  methods: {
    playAudio() {
      const audio = document.querySelector('audio');
      audio.play();
    },
    pauseAudio() {
      const audio = document.querySelector('audio');
      audio.pause();
    },
    updateTime(event) {
      const audio = event.target;
      this.currentTime = this.formatTime(audio.currentTime);
      this.duration = this.formatTime(audio.duration);
      this.updateLyrics(audio.currentTime * 1000); // 转换为毫秒
    },
    audioEnded() {
      this.currentTime = '00:00';
      this.currentLineIndex = 0;
      this.scrollTop = 0;
    },
    updateLyrics(currentTime) {
      for (let i = 0; i < this.lyrics.length; i++) {
        if (currentTime >= this.lyrics[i].time) {
          this.currentLineIndex = i;
        } else {
          break;
        }
      }
      this.scrollLyrics();
    },
    scrollLyrics() {
      const lineHeight = 30; // 每行歌词的高度
      this.scrollTop = this.currentLineIndex * lineHeight;
    },
    formatTime(time) {
      const minutes = Math.floor(time / 60);
      const seconds = Math.floor(time % 60);
      return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
    },
  },
};
</script>

样式设计

在样式部分,我们可以设计音频播放器和歌词展示区域的样式,使其看起来更加美观。

css 复制代码
<style scoped>
.audio-container {
  padding: 20px;
}

.audio-player {
  margin-bottom: 20px;
}

.controls {
  margin-bottom: 10px;
}

.time {
  font-size: 14px;
  color: #666;
}

.lyrics {
  height: 300px;
  overflow-y: auto;
  border: 1px solid #ccc;
  padding: 10px;
  font-size: 16px;
  line-height: 1.5;
  text-align: center;
}

.lyrics view {
  transition: color 0.3s ease;
}

.lyrics .active {
  color: #ff6600;
  font-weight: bold;
}
</style>

运行效果

通过以上步骤,你可以在UniApp中实现一个漂亮的音乐歌词滚动播放效果。运行项目后,你应该能够看到一个带有播放、暂停按钮的音频播放器,以及随着音乐播放自动滚动的歌词。

app体验地址

项目开源地址:imovie: 爱电影小程序uni-app

歌词解析

网络上拿到的歌词,可能是类似如下格式:

bash 复制代码
{"songStatus":1,"lyricVersion":3,"lyric":"[by:有个陷阱他们早就已经沦陷]\n\n[00:09.23]\n[00:20.20]花的心藏在蕊中\n[00:23.80]空把花期都错过\n[00:27.26]\n[00:29.52]你的心忘了季节\n[00:33.12]从不轻易让人懂\n[00:37.24]\n[00:38.81]为何不牵我的手\n[00:42.49]共听日月唱首歌\n[00:46.09]\n[00:47.46]黑夜又白昼\n[00:49.72]黑夜又白昼\n[00:51.73]人生为欢有几何\n[00:55.54]\n[00:57.00]春去春会来\n[01:01.72]花谢花会再开\n[01:06.08]只要你愿意\n[01:08.39]只要你愿意\n[01:10.41]让梦划向你心海\n[01:15.65]春去春会来\n[01:20.32]花谢花会再开\n[01:24.73]只要你愿意\n[01:27.06]只要你愿意\n[01:29.02]让梦划向你心海\n[01:33.43]\n[02:12.13]花瓣泪飘落风中\n[02:15.73]虽有悲意也从容\n[02:19.33]\n[02:21.41]你的泪晶莹剔透\n[02:25.04]心中一定还有梦\n[02:29.30]\n[02:30.73]为何不牵我的手\n[02:34.38]同看海天成一色\n[02:39.36]潮起又潮落\n[02:41.68]潮起又潮落\n[02:43.68]送走人间许多愁\n[02:48.28]\n[02:48.99]春去春会来\n[02:53.61]花谢花会再开\n[02:57.98]只要你愿意\n[03:00.35]只要你愿意\n[03:02.32]让梦划向你心海\n[03:07.58]春去春会来\n[03:12.32]花谢花会再开\n[03:16.58]只要你愿意\n[03:18.95]只要你愿意\n[03:21.01]让梦划向你心海\n[03:26.23]只要你愿意\n[03:28.29]只要你愿意\n[03:30.26]让梦划向你心海\n[03:35.06]\n","code":200}

需要对其解析,解析为类似以下的格式:

bash 复制代码
lyrics: [
        { time: 0, text: '第一行歌词' },
        { time: 5000, text: '第二行歌词' },
        { time: 10000, text: '第三行歌词' },
        // 更多歌词行...
      ],

解析方法:

javascript 复制代码
/**
 * 歌词解析
 * @param {lrcContent} string - 歌词内容
 * @returns {lyrics} 对象数组
 */
function parseLyric(lrcContent) {
    const lines = lrcContent.split('\n');
    const lyrics = [];

    lines.forEach(line => {
        const match = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\]/);
        if (match) {
            const minutes = parseInt(match[1]);
            const seconds = parseInt(match[2]);
            const milliseconds = parseInt(match[3]);

            const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds;

            // 提取歌词文本
            const text = line.replace(/\[\d{2}:\d{2}\.\d{2,3}\]/g, '').trim();
            lyrics.push({ time, text });
        }
    });

    return lyrics;
}

完成audio组件源码

html 复制代码
<template>
	<view class="audio_container">
		<view class="audio-title"
			style="width: 100%; text-align: left; font-size: 36rpx;font-weight: bold;padding: 0rpx 0rpx; position: relative;">
			<uni-notice-bar single :scrollable="titleScroll" :size="titleFontSize"
				:background-color="titleBackgroundColor" :color="titleColor" :speed="titleScrollSpeed" :text="title"
				class="uni-noticebar" style="padding: 0px; margin-bottom: 0px;">
			</uni-notice-bar>
			<uni-fav v-show="isCollectBtn" :checked="isFavorited" class="favBtn"  bgColor="#dddddd" bgColorChecked="#ffaa00" @click="handleCollec"
				style="color:#848484; position: absolute;top: 0rpx;right: 0px;"></uni-fav>
		</view>
		<view class="audio-subTitle"
			:style="'font-size: '+subTitleFontSize+';font-weight: bold;padding: 0rpx 0rpx 4rpx 0rpx;position: relative;'">
			<uni-notice-bar single :scrollable="titleScroll" :size="titleFontSize"
				:background-color="titleBackgroundColor" :color="subTitleColor" :speed="titleScrollSpeed"
				:text="localSubTitle" class="uni-noticebar">
			</uni-notice-bar>
			<uni-icons v-show="isShareBtn" @click="handleShare" type="redo" size="20"
				style="color:#848484;position: absolute;top: 0rpx;right: 0px;"></uni-icons>
		</view>
		<view>
			<slider :backgroundColor='backgroundColor' :activeColor='activeColor' @change="handleSliderChange"
				:value="sliderIndex" :max="maxSliderIndex" block-color="#343434" block-size="16" />
		</view>
		<view style="padding: 0rpx 15rpx 0rpx 15rpx ; display: block; ">
			<view style="float: left; font-size: 20rpx;color:#848484;">
				{{currentTimeText}}
			</view>
			<view style="float: right;font-size: 20rpx;color:#848484;">
				{{totalTimeText}}
			</view>
		</view>
		<view style="margin-top: 70rpx;">
			<uni-grid :column="5" :showBorder="false" :square="false">
				<uni-grid-item>
					<view class="uni-grid-icon">
						<image @tap="handleFastRewind" src="../../static/images/playlist.svg"
							style="width: 48rpx;height: 48rpx;top:6rpx;">
						</image>
					</view>
				</uni-grid-item>
				<uni-grid-item>
					<view class="uni-grid-icon">
						<image @tap="handleFastRewind" src="../../static/images/get-back.svg"
							style="width: 48rpx;height: 48rpx;top:6rpx;">
						</image>
					</view>
				</uni-grid-item>
				<uni-grid-item>
					<view class="uni-grid-icon">
						<image @tap="handleChangeAudioState" v-show="!isPlaying" src="../../static/images/play.svg"
							style="width: 48rpx;height: 48rpx;top:6rpx;">
						</image>
						<image @tap="handleChangeAudioState" v-show="isPlaying" src="../../static/images/pause.svg"
							style="width: 48rpx;height: 48rpx;top:6rpx;">
						</image>
					</view>
				</uni-grid-item>
				<uni-grid-item>
					<view class="uni-grid-icon">
						<image @tap="handleFastForward" src="../../static/images/fast-forward.svg"
							style="width: 48rpx;height: 48rpx;top:6rpx;">
						</image>
					</view>
				</uni-grid-item>
				<uni-grid-item>
					<view class="uni-grid-icon">
						<image @tap="handleLoopPlay" src="../../static/images/Loop.svg"
							style="width: 48rpx;height: 48rpx; top:6rpx; ">
						</image>
					</view>
				</uni-grid-item>
			</uni-grid>
		</view>
		<view v-show="isShowLrc">
		   <scroll-view class="lyrics" scroll-y :scroll-top="scrollTop" :current="currentLineIndex" ref="lyricsContainer" >
				<block v-for="(line, index) in lyrics" :key="index">
				  <view :class="{ 'active': currentLineIndex === index }">{{ line.text }}</view>
				</block>
			</scroll-view>
		</view>
	</view>
</template>
<script>
	export default {
		name: 'my-audio',
		//audioPlay开始播放
		//audioPause停止播放
		//audioEnd音频自然播放结束事件
		//audioCanplay音频进入可以播放状态,但不保证后面可以流畅播放
		//change播放状态改变 返回值false停止播放 true开始播放
		//audioError 播放器错误
		//audioCollec 音频收藏
		emits: ['audioPlay', 'audioPause', 'audioEnd', 'audioCanplay', 'change', 'audioError','audioCollec'],
		props: {
			//标题文字
			title: {
				type: String,
				default: '空'
			},
			//标题默认字体大小
			titleFontSize: {
				type: Number,
				default: 35
			},
			//标题文字颜色
			titleColor: {
				type: String,
				default: '#303030'
			},
			//标题背景色
			titleBackgroundColor: {
				type: String,
				default: 'white'
			},
			//标题是否滚动
			titleScroll: {
				type: Boolean,
				default: false
			},
			//标题滚动速度
			titleScrollSpeed: {
				type: Number,
				default: 100
			},

			subTitle: {
				type: String,
				default: '空'
			},
			subTitleColor: {
				type: String,
				default: '#6C7996'
			},
			subTitleFontSize: {
				type: String,
				default: "30rpx"
			},
			//是否自动播放
			autoplay: {
				type: Boolean,
				default: false
			},
			//滑块左侧已选择部分的线条颜色
			activeColor: {
				type: String,
				default: '#7C7C7C'
			},
			//滑块右侧背景条的颜色
			backgroundColor: {
				type: String,
				default: '#E5E5E5'
			},

			//音频地址
			src: {
				type: [String, Array],
				default: ''
			},

			//是否倒计时
			isCountDown: {
				type: Boolean,
				default: false
			},

			//音乐封面
			audioCover: {
				type: String,
				default: ''
			},
			//是否显示收藏按钮
			isCollectBtn: {
				type: Boolean,
				default: false
			},
			//状态是否是已收藏
			isFavorited: {
				type: Boolean,
				default: false
			},
			//是否显示分享按钮
			isShareBtn: {
				type: Boolean,
				default: false
			},
			
			//是否显示歌词
			isShowLrc: {
				type: Boolean,
				default: false
			},
			
			//歌词信息
			lyrics: {
				type: [Array],
				default: []
			},
		},
		data() {
			return {
				totalTimeText: '00:00', //视频总长度文字
				currentTimeText: '00:00:00', //视频已播放长度文字

				isPlaying: false, //播放状态

				sliderIndex: 0, //滑块当前值
				maxSliderIndex: 100, //滑块最大值

				IsReadyPlay: false, //是否已经准备好可以播放了

				isLoop: false, //是否循环播放

				speedValue: [0.5, 0.8, 1.0, 1.25, 1.5, 2.0],
				speedValueIndex: 2,
				playSpeed: '1.0', //播放倍速 可取值:0.5/0.8/1.0/1.25/1.5/2.0

				currentLineIndex: 0,
				localSubTitle:this.subTitle,
				shortLrc:'',
				scrollTop: 0, // 初始滚动位置
				stringObject: (data) => {
					return typeof(data)
				},
				innerAudioContext: uni.createInnerAudioContext()
			}
		},
		watch: {
		    subTitle(newVal) {
		      this.localSubTitle = newVal;
			  }
		},
		async mounted() {
			this.innerAudioContext.src = typeof(this.src) == 'string' ? this.src : this.src[0];
			if (this.autoplay) {
				if (!this.src) return console.error('src cannot be empty,The target value is string or array')

				// #ifdef H5
				var ua = window.navigator.userAgent.toLowerCase();
				if (ua.match(/MicroMessenger/i) == 'micromessenger') {
					const jweixin = require('../../utils/jweixin');

					jweixin.config({});
					jweixin.ready(() => {
						WeixinJSBridge.invoke('getNetworkType', {}, (e) => {
							this.innerAudioContext.play();

						})
					})
				}
				// #endif

				// #ifndef H5
				this.innerAudioContext.autoplay = true;
				// #endif
			}

			//音频播放事件
			this.innerAudioContext.onPlay(() => {
				this.isPlaying = true;
				this.$emit('audioPlay')

				this.$emit('change', {
					state: true
				});

				setTimeout(() => {
					this.maxSliderIndex = parseFloat(this.innerAudioContext.duration).toFixed(2);

				}, 100)
			});

			//音频暂停事件
			this.innerAudioContext.onPause(() => {
				this.$emit('audioPause');
				this.$emit('change', {
					state: false
				});
			});

			//音频自然播放结束事件
			this.innerAudioContext.onEnded(() => {
				this.isPlaying = !this.isPlaying;
				this.$emit('audioEnd');

				if (this.isLoop) {
					this.changePlayProgress(0);
					this.innerAudioContext.play();
				}
			});

			//音频进入可以播放状态,但不保证后面可以流畅播放
			this.innerAudioContext.onCanplay((event) => {

				this.IsReadyPlay = true;

				this.$emit('audioCanplay');
				
				let duration = this.innerAudioContext.duration;

				//console.log('总时长', duration)

				//将当前音频长度秒转换为00:00:00格式
				this.totalTimeText = this.getFormateTime(duration);
				this.maxSliderIndex = parseFloat(duration).toFixed(2);

				//console.log(this.getFormateTime(duration))
				
				//console.log('总时长1', this.totalTimeText)

				//防止视频无法正确获取时长
				setTimeout(() => {
					duration = this.innerAudioContext.duration;

					//将当前音频长度秒转换为00:00:00格式
					this.totalTimeText = this.getFormateTime(duration);
					this.maxSliderIndex = parseFloat(duration).toFixed(2);
					
					//console.log('总时长2', this.totalTimeText)
				}, 300)
				
			});

			//音频播放错误事件
			this.innerAudioContext.onTimeUpdate((res) => {
				this.sliderIndex = parseFloat(this.innerAudioContext.currentTime).toFixed(2);
				this.currentTimeText = this.getFormateTime(this.innerAudioContext.currentTime);
				//更新歌词
				const currentTime = this.innerAudioContext.currentTime * 1000; // 转换为毫秒
				this.updateLyrics(currentTime);
			});

			//音频播放错误事件
			this.innerAudioContext.onError((res) => {
				console.log(res.errMsg);
				console.log(res.errCode);
				this.$emit('change', {
					state: false
				});
				this.audioPause();

				this.$emit('audioError', res);
			});

		},
		methods: {
			//销毁innerAudioContext()实例
			audioDestroy() {
				console.log("audioDestroy")
				if (this.innerAudioContext) {
					if (this.isPlaying && !this.innerAudioContext.paused) {
						this.audioPause();
					}
					this.innerAudioContext.destroy();
					this.isPlaying = false;
				}
			},
			//点击变更播放状态
			handleChangeAudioState() {
				if(this.src ===''){
					uni.showToast({
									title: '无播放资源',
									icon: 'none',
									duration: 1000
								});
					return;
				}
				if (this.isPlaying && !this.innerAudioContext.paused) {
					this.audioPause();
				} else {
					this.audioPlay();
				}
			},
			//开始播放
			audioPlay() {
				this.$nextTick(() => {
					this.innerAudioContext.src = this.src;
					setTimeout(() => {
						this.innerAudioContext.play();
						this.isPlaying = true;
					}, 100); // 100毫秒
				});
				
			},
			//暂停播放
			audioPause() {
				this.innerAudioContext.pause();
				this.isPlaying = false;
			},
			//变更滑块位置
			handleSliderChange(e) {
				this.changePlayProgress(e.detail ? e.detail.value : e)
			},
			//更改播放倍速
			handleChageSpeed() {
				//获取播放倍速列表长度
				let speedCount = this.speedValue.length;
				//如果当前是最大倍速,从-1开始
				if (this.speedValueIndex == (speedCount - 1)) {
					this.speedValueIndex = -1;
				}
				//最新倍速序号
				this.speedValueIndex += 1;
				//获取最新倍速文字
				this.playSpeed = this.speedValue[this.speedValueIndex].toFixed(1);
				//暂停播放
				this.audioPause();
				//变更播放倍速
				this.innerAudioContext.playbackRate(this.speedValue[this.speedValueIndex]);
				//开始播放
				this.audioPlay();
			},
			//快退15秒
			handleFastRewind() {
				if (this.IsReadyPlay) {
					let value = parseInt(this.sliderIndex) - 15;
					this.changePlayProgress(value >= 0 ? value : 0);
				}
			},
			//快进15秒
			handleFastForward() {
				if (this.IsReadyPlay) {
					let value = parseInt(this.sliderIndex) + 15;
					this.changePlayProgress(value <= this.innerAudioContext.duration ? value : this.innerAudioContext
						.duration);
				}
			},
			//开启循环播放
			handleLoopPlay() {
				this.isLoop = !this.isLoop;
				if (this.isLoop) {
					uni.showToast({
						title: '已开启循环播放',
						duration: 1000
					});
				} else {
					uni.showToast({
						title: '取消循环播放',
						duration: 1000
					});
				}
			},
			//更改播放进度
			changePlayProgress(value) {
				this.innerAudioContext.seek(value);
				this.sliderIndex = value;
				this.currentTimeText = this.getFormateTime(value);
			},
			//秒转换为00:00:00
			getFormateTime(time) {
				let ms = time * 1000; // 1485000毫秒
				let date = new Date(ms);

				// 注意这里是使用的getUTCHours()方法,转换成UTC(协调世界时)时间的小时
				let hour = date.getUTCHours();
				// let hour = date.getHours(); 如果直接使用getHours()方法,则得到的时分秒格式会多出来8个小时(在国内开发基本都是使用的是东八区时间),getHours()方法会把当前的时区给加上。
				let minute = date.getMinutes();
				let second = date.getSeconds();

				let formatTime =
					`${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;

				return formatTime;
			},
			handleCollec() {
				this.$emit('audioCollec');
			},
			handleShare() {
				this.$emit('audioShare');
			},
			updateLyrics(currentTime) {
			    for (let i = 0; i < this.lyrics.length; i++) {
			        if (currentTime >= this.lyrics[i].time) {
			          this.currentLineIndex = i;
					  this.shortLrc = this.lyrics[i].text;
					  this.localSubTitle = this.subTitle + ' : '+this.shortLrc
			        } else {
			          break;
			        }
			    }
				this.scrollLyrics();
			},
			scrollLyrics() {
			    const lineHeight = 20; // 每行歌词的高度
			    this.scrollTop = this.currentLineIndex * lineHeight;
			},
		},
		onLoad() {
			console.log("onLoad")
		},
		onUnload() {
			console.log("onUnload")
			this.audioDestroy()
		},
		onHide() {
			console.log("onHide")
			this.audioDestroy()
		},
		beforeDestroy() {
			console.log("beforeDestroy")
			this.audioDestroy()
		}
	}
</script>

<style lang="scss" scoped>
	.audio_container {
		box-shadow: 0 0 10rpx #c3c3c3;
		padding: 30rpx 20rpx 30rpx 20rpx;

		.audio-title {
			font-size: 28rpx;
		}

		.uni-noticebar {
			padding: 0px;
			padding-right: 50rpx;
			margin-bottom: 0px;
			display: inline-block;
		}


		.audio-subTitle {
			width: 100%;
			text-align: left;
			font-size: 40rpx;
			color: blue;
		}

		.speed-text {
			position: absolute;
			top: 0rpx;
			left: 30rpx;
			right: 0;
			color: #475266;
			font-size: 16rpx;
			font-weight: 600;
		}

		.uni-grid-icon {
			text-align: center;
		}
		
		.lyrics {
		  margin-top: 20px;
		  height: 660rpx; /* 设置歌词容器的高度 */
		  // overflow: hidden; /* 隐藏溢出的歌词 */
		  overflow-y: auto; /* 允许垂直滚动 */
		  position: relative;
		  font-size: 32rpx;
		  line-height: 1.8;
		  text-align: center;
		}
		.lyrics view {
		  transition: color 1.2s ease; /* 添加平滑颜色变化效果 */
		}
		
		.lyrics .active {
		  color: #00aa00;
		  font-size: 45rpx;
		  font-weight: bold;
		}

	}
</style>

总结

通过使用UniApp的组件和API,我们可以轻松实现音乐歌词的滚动播放效果。关键在于监听音频的播放时间,并根据时间更新歌词的显示和滚动位置。

这里面有个悬而未决的问题,就是这个滚动显示,有时候会滚动到最上方或最下方,导致在视野区域看不到。以下的处理,虽然简单, 但也粗暴。原因就出在这里:

javascript 复制代码
scrollLyrics() {
		const lineHeight = 20; // 每行歌词的高度
		this.scrollTop = this.currentLineIndex * lineHeight;
},

如何让歌词能够根据进度居中显示?有知道的欢迎留言,感谢!

其他资源

vue实现歌词滚动_vue 实现一个歌词滚动效果-CSDN博客

相关推荐
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
阿伟来咯~6 小时前
一些 uniapp相关bug
uni-app·bug
瑶琴AI前端10 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
mosen86810 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
尚梦18 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
尚学教辅学习资料1 天前
基于SSM+uniapp的营养食谱系统+LW参考示例
java·uni-app·ssm·菜谱
Bessie2341 天前
微信小程序eval无法使用的替代方案
微信小程序·小程序·uni-app
qq22951165021 天前
小程序Android系统 校园二手物品交换平台APP
微信小程序·uni-app
qq22951165022 天前
微信小程序uniapp基于Android的流浪动物管理系统 70c3u
微信小程序·uni-app
qq22951165022 天前
微信小程序 uniapp+vue老年人身体监测系统 acyux
vue.js·微信小程序·uni-app