微信小程序---暮之沧蓝音乐小程序

前言

本文介绍了一个基于Express搭建的静态文件服务器和小程序音乐播放器的实现方案。

主要内容包括:

  1. 使用Express、serve-static等模块搭建支持文件上传和浏览的静态服务器;

  2. 开发微信小程序音乐播放器,包含推荐页、播放器和播放列表三个标签页;

  3. 实现音乐播放控制功能,包括播放/暂停、进度条拖动、自动切歌等;

  4. 设计UI界面,包含轮播图、音乐推荐列表、播放控制面板等组件。

系统通过3000端口提供静态文件服务,小程序通过HTTP请求获取音频文件并进行播放管理。

结果展示

前提准备

1.1 搭建一个简单的静态文件服务器,并支持文件上传功能


1.2 创建index.js文件(注意:创建的文件夹一定是英文,中文cmd不能识别)

1.3 编辑index.js文件

javascript 复制代码
// 搭建一个简单的静态文件服务器,并支持文件上传功能
// 静态文件服务:通过 serve-static 和 serve-index 提供 ./htdocs 目录下的文件浏览和下载。
// 文件上传:通过 multiparty 解析上传的文件,并保存到 ./htdocs/upfile 目录。
// 服务器监听:在 3000 端口启动 HTTP 服务器。
var express = require('express');
var serveIndex = require('serve-index');
var serveStatic = require('serve-static');
var multiparty = require('multiparty');
var path = require('path'); // 用于路径处理
var fs = require('fs');     // 用于检查目录

var LOCAL_BIND_PORT = 3000;
var app = express();

// 确保上传目录存在
const uploadDir = path.join(__dirname, 'htdocs', 'upfile');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true });
}

// 文件上传接口
app.post('/upload', function (req, res) {
  var form = new multiparty.Form();
  form.encoding = 'utf-8';
  form.uploadDir = uploadDir;
  form.maxFilesSize = 4 * 1024 * 1024; // 4MB

  form.parse(req, function (err, fields, files) {
    if (err) {
      console.log('parse error: ' + err);
    } else {
      console.log('parse files: ' + JSON.stringify(files));
    }
    res.writeHead(200, { 'content-type': 'text/plain;charset=utf-8' });
    res.write('received upload');
    res.end();
  });
});

// 静态文件服务
var serve = serveStatic('./htdocs');
app.use('/', serveIndex('./htdocs', { 'icons': true }));
app.use('/', serve); // ✅ 使用中间件自动处理所有静态请求

// 启动服务器
console.log(`✅ 静态文件服务器已启动:http://localhost:${LOCAL_BIND_PORT}`);
console.log(`📁 访问目录:./htdocs`);
console.log(`📥 上传目录:./htdocs/upfile`);
console.log(`🔧 按 Ctrl + C 停止服务器`);
app.listen(LOCAL_BIND_PORT);

1.4 在此目录(music_bg)路径输入cmd,运行以下命令

bash 复制代码
npm init -y                 # 自动创建package,json配置文件

npm install express --save  # 安装Express框架,用于快速搭建HTTP服务器

npm install nodemon -g      # 安装nodemon监控文件修改

1.5 运行index.js文件

bash 复制代码
node index.js

1.6 打开浏览器,运行http://127.0.0.1:3000

可以找些音频文件放进upfile文件夹

后期需要将如"http://127.0.0.1:3000/upfile/%5B%E5%85%8D%E8%B4%B9%E4%BC%B4%E5%A5%8F%5DAmbient.mp3

填入代码里

代码展示

整体结构

2.1 index.json

javascript 复制代码
{
  "navigationBarBackgroundColor": "#fff",
  "navigationBarTitleText": "暮之沧蓝音乐",
  "navigationBarTextStyle":"black"
}

2.2 index.wxml

html 复制代码
<!-- 轮播图 -->
<view class="tab">
  <!-- bindtap="changeItem",单击tab区域切换到对应的标签页 -->
  <view class="tab-item {{ tab==0?'active:':''}}" bindtap="changeItem" data-item="0">音乐推荐</view>
  <view class="tab-item {{ tab==1?'active:':''}}" bindtap="changeItem" data-item="1">播放器</view>
  <view class="tab-item {{ tab==2?'active:':''}}" bindtap="changeItem" data-item="2">播放列表</view>
</view>
<!-- 主体内容 -->
<view class="content">
  <swiper current="{{ item }}" bindchange="changeTab">
    <swiper-item>
      <include src="info.wxml" />
    </swiper-item>
    <swiper-item>
      <include src="play.wxml" />
    </swiper-item>
    <swiper-item>
      <include src="playlist.wxml" />
    </swiper-item>
  </swiper>
</view>
<!-- 底部播放器 -->
<view class="player">
  <image class="player-cover" src="{{ play.coverImgUrl}}" />
  <view class="player-info">
    <view class="player-info-title">{{ play.title }}</view>
    <view class="player-info-singer">{{ play.singer }}</view>
  </view>
  <view class="player-controls">
    <!-- 切换到播放列表 -->
    <image src="/images/B.png" bindtap="changePage" data-page="2"/>
    <!-- 播放 -->
    <image wx:if="{{ state=='paused'}}" src="/images/ZT.png" bindtap="play"/>
    <image wx:else src="/images/BF.png" bindtap="pause"/>
    <!-- 下一曲 -->
    <image src="/images/XYQ.png" bindtap="next"/>
  </view>
</view>

2.3 index.wxss

css 复制代码
page {
  display: flex;
  flex-direction: column;
  background: rgb(207, 236, 247);
  color: rgb(70, 68, 68);
  height: 100%;
}

.tab {
  display: flex;
}

.tab-item {
  flex: 1;
  font-size: 10pt;
  text-align: center;
  line-height: 72rpx;
  border-bottom: 6rpx solid white;
}

.content {
  flex: 1;
}

.content > swiper {
  height: 100%;
}

.player {
  background:rgb(213, 238, 247);
  border-top: 1rpx solid black;
  height: 112rpx;
}

.tab-item.active {
  color: red;
  border-block-color: red;
}

.content-info {
  height: 100%;
}
/* 隐藏滚动条 */
::-webkit-scrollbar {
  width: 0;
  height: 0;
  color: transparent;
}

.content-info-slide {
  height: 310rpx;
  margin-bottom: 20rpx;
}

.content-info-slide image{
  width: 100%;
  height: 100%;
}

.content-info-portal {
  display: flex;
  margin-bottom: 15rpx;
}

.content-info-portal > view {
  flex: 1;
  font-size: 10pt;
  text-align: center;
}

.content-info-portal image {
  width: 90rpx;
  height: 90rpx;
  display: block;
  margin: 20rpx auto;
}

.content-info-list {
  font-size: 10pt;
  margin-bottom: 20rpx;
}

.content-info-list > .list-title {
  font-size: 15pt;
  margin: 50rpx 35rpx;
  color: brown;
}

.content-info-list > .list-inner {
  display: flex;
  flex-wrap: wrap;
  margin: 0 20rpx;
}

.content-info-list > .list-inner > .list-item {
  flex: 1;
}

.content-info-list > .list-inner > .list-item > image {
  display: block;
  width: 200rpx;
  height: 200rpx;
  margin: 0 auto;
  border-radius: 10rpx;
  border: 1rpx solid #555;
}

.content-info-list > .list-inner > .list-item > view {
  width: 200rpx;
  margin: 10rpx auto;
  font-size: 10pt;
}

/* 播放器样式 */
.player {
  display: flex;
  align-items: center;
  background: rgb(207, 236, 247);
  border-top: 1rpx solid black;
  height: 115rpx;
}

.player-cover {
  width: 80rpx;
  height: 80rpx;
  margin-left: 15rpx;
  border-radius: 8rpx;
  border: 1rpx solid black;
}

.player-info {
  flex: 1;
  font-size: 10pt;
  line-height: 50rpx;
  margin-left: 20rpx;
  padding-bottom: 10rpx;
}

.player-info-singer {
  color: gray;
}

.player-controls image {
  width: 50rpx;
  height: 50rpx;
  margin-right: 30rpx;
}

/* 播放器 */
.content-play {
  display: flex;
  justify-content: space-around;
  flex-direction: column;
  height: 100%;
  text-align: center;
}

.content-play-info > view {
  color: gray;
  font-size: 12pt;
}

.content-play-cover image {
  animation: rotateImage 10s linear infinite;
  width: 400rpx;
  height: 400rpx;
  border-radius: 50%;
  border: 1rpx solid gray;
}

@keyframes rotateImage {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.content-play-progress {
  display: flex;
  align-items: center;
  margin: 0 35rpx;
  font-size: 9pt;
  text-align: center;
}

.content-play-progress > view {
  flex: 1;
}

/* 播放列表 */
.playlist-item {
  display: flex;
  align-items: center;
  border-bottom: 1rpx solid black;
  height: 115rpx;
}

.playlist-cover {
  width: 80rpx;
  height: 80rpx;
  margin-left: 15rpx;
  border-radius: 8rpx;
  border: 1rpx solid black;
}

.playlist-info {
  flex: 1;
  font-size: 10pt;
  line-height: 40rpx;
  margin-left: 20rpx;
  padding-bottom: 5rpx;
}

.playlist-info-singer {
  color: gray;
}

.playlist-controls {
  font-size: 10pt;
  margin-right: 20rpx;
  color: red;
}

2.4 info.wxml

html 复制代码
<scroll-view class="content-info" scroll-y>
	<!-- 轮播图 -->
	<view style="background: white; height: 1300rpx;">
		<swiper class="content-info-slide" indicator-color="rgba(255,255,255,.5)" indicator-active-color="#fff" indicator-dots circular autoplay>
			<swiper-item>
				<image src="/images/banner.png"/>
			</swiper-item>
			<swiper-item>
				<image src="/images/banner2.png"/>
			</swiper-item>
			<swiper-item>
				<image src="/images/banner3.png"/>
			</swiper-item>
		</swiper>	
		<!-- 功能按钮 -->
		<view class="content-info-portal">
			<view>
				<image src="/images/F1.png"/>
				<text>私人漫游</text>
			</view>
			<view>
				<image src="/images/F2.png"/>
				<text>每日推荐</text>
			</view>
			<view>
				<image src="/images/F3.png"/>
				<text>新歌榜单</text>
			</view>
		</view>
		<!-- 为你推荐 -->
		<view class="content-info-list">
			<view class="list-title">为你推荐</view>
				<view class="list-inner">
					<view class="list-item">
						<image src="/images/T1.png"/>
						<view>City Of Stars</view>
					</view>
					<view class="list-item">
						<image src="/images/T2.png"/>
						<view>那时雨</view>
					</view>
					<view class="list-item">
						<image src="/images/T3.png"/>
						<view>Show Me Love</view>
					</view>
					<view class="list-item">
						<image src="/images/T4.png"/>
						<view>AM</view>
					</view>
					<view class="list-item">
						<image src="/images/T5.png"/>
						<view>习惯失恋</view>
					</view>
					<view class="list-item">
						<image src="/images/T6.png"/>
						<view>幻想是痛的延续</view>
					</view>
				</view>
		</view>
	</view>

	<!-- <view>已到达最底部</view> -->
</scroll-view>

2.5 play.wxml

html 复制代码
<!-- 播放器页面 -->
<view class="content-play">
	<!-- 显示音乐信息 -->
	<view class="content-play-info">
		<text>{{ play.title }}</text>
		<view>--{{ play.singer }}--</view>
	</view>
	<!-- 显示专辑封面 -->
	<view class="content-play-cover">
		<image src="{{ play.coverImgUrl }}" style="animation-play-state: {{ state }} " />
	</view>
	<!-- 显示播放进度和时间 -->
	<view class="content-play-progress">
		<text>{{ play.currentTime }}</text>
		<view>
			<slider bindchange="sliderChange" activeColor="#d33a31" block-size="12" backgroundColor="#dadada" value="{{ play.percent }}" />
		</view>	
		<text>{{ play.duration }}</text>
	</view>
</view>

2.6 playlist.wxml

html 复制代码
<!-- 播放列表 -->
<scroll-view class="content-playlist" scroll-y>
	<view class="playlist-item" wx:for="{{ playlist }}" wx:key="id" bindtap="change" data-index="{{ index }}">
		<image class="playlist-cover" src="{{ item.converImgUrl }}"/>
		<view class="playlist-info">
			<view class="playlist-info-title">{{ item.title }}</view>
			<view class="playlist-info-singer">{{ item.singer }}</view>
		</view>
		<view class="playlist-controls">
			<text wx:if="{{ index==playIndex }}">正在播放</text>
		</view>
	</view>

</scroll-view>

2.7 index.js

javascript 复制代码
// index.js

Page({
  data: {
		// 切换标签页的值
		item: 0,
		// 标签页索引
		tab: 0,
		// 播放列表数据
		playlist: [
			{
			id: 1,
			title: "City Of Stars",
			singer: "王OK",
			src: "{此处填入前面服务器生成的网址}",
			converImgUrl: "/images/T1.png"
			},
			{
				id: 2,
				title: "那时雨",
				singer: "徐良",
				src: "{此处填入前面服务器生成的网址}",
				converImgUrl: "/images/T2.png"
				},
			{
				id: 3,
				title: "Show Me Love",
				singer: "WizTheMC",
				src: "{此处填入前面服务器生成的网址}",
				converImgUrl: "/images/T3.png"
				},
			{
				id: 4,
				title: "AM",
				singer: "T-Chenxi",
				src: "{此处填入前面服务器生成的网址}",
				converImgUrl: "/images/T4.png"
				},
			{
				id: 5,
				title: "习惯失恋",
				singer: "容祖儿",
				src: "{此处填入前面服务器生成的网址}",
				converImgUrl: "/images/T5.png"
				},
			{
				id: 6,
				title: "幻想是痛的延续",
				singer: "匿名",
				src: "{此处填入前面服务器生成的网址}",
				converImgUrl: "/images/T6.png"
				},
		],
		// 播放状态,running表示正在播放
		state: "paused",
		// 当前播放的曲目在播放列表数组中的索引值
		playIndex: 0,
		play: {
			// 播放时长
			currentTime: "00:00",
			duration: "00:00",
			// 播放进度
			percent: 0,
			title: "",
			singer: "",
			converImgUrl: "/images/T1.png"
		},
	},
	// 切换到对应的标签页
	changeItem: function(e) {
		this.setData({
			item: e.target.dataset.item
		})
	},
	// 更改当前标签页的索引
	changeTab: function(e){
		this.setData({
			tab:e.detail.current
		})
	},

	// 实现音乐播放功能
	audioCtx: null,
	onReady: function(){
		// 控制播放器页面的进度条的进度与时间显示
		this.audioCtx = wx.createInnerAudioContext()
		var that = this
		// 播放失败检测
		this.audioCtx.onError(function(){
			console.log("播放失败:" + that.audioCtx.src)
		})
		// 播放完成自动切换下一曲
		this.audioCtx.onEnded(function(){
			that.next()
		})
		// 自动更新播放进度
		this.audioCtx.onPlay(function(){})
		// 获取音乐状态信息
		this.audioCtx.onTimeUpdate(function(){
			that.setData({
				"play.duration": formatTime(that.audioCtx.duration),
				"play.currentTime": formatTime(that.audioCtx.currentTime),
				"play.percent": that.audioCtx.currentTime / that.audioCtx.duration * 100
			})
		})
		// 默认选择第一首歌
		this.setMusic(0)
		// 格式化时间
		function formatTime(time) {
			var minute = Math.floor(time / 60) % 60;
			var second = Math.floor(time) % 60;
			return (minute < 10 ? '0' + minute: minute) + ':' + (second < 10 ? '0' + second: second)
			}
		},
		
	// 获取当前滚动条的进度
	sliderChange: function(e) {
		var second = e.detail.value * this.audioCtx.duration / 100
		this.audioCtx.seek(second)
	},
	// 切换当前播放的歌曲
	setMusic: function(index){
		var music = this.data.playlist[index]
		this.audioCtx.src = music.src
		this.setData({
			playIndex: index,
			"play.title": music.title,
			"play.singer": music.singer,
			"play.coverImgUrl": music.converImgUrl,
			"play.currentTime": "00:00",
			"play.duration": "00:00",
			"play.percent": 0
		})
	},

	// 处理播放与暂停
	play: function() {
		this.audioCtx.play()
		this.setData({
			state: "running"
		})
	},
	pause: function() {
		this.audioCtx.pause()
		this.setData({
			state: "paused"
		})
	},
	// 下一曲按钮
  next: function() {
    var index = this.data.playIndex >= this.data.playlist.length - 1 ? 0 : this.data.playIndex + 1
		this.setMusic(index)
		// 状态为暂停,不要立即播放
    if (this.data.state === 'running') {
      this.play()
    }
	},
	// 点击列表歌曲并播放
	change: function(e) {
		this.setMusic(e.currentTarget.dataset.index)
		this.play()
	}
	
})
相关推荐
游戏开发爱好者87 小时前
App 上架平台全解析,iOS 应用发布流程、苹果 App Store 审核步骤
android·ios·小程序·https·uni-app·iphone·webview
2501_916007477 小时前
iOS 上架 App 费用详解 苹果应用发布成本、App Store 上架收费标准、开发者账号与审核实战经验
android·ios·小程序·https·uni-app·iphone·webview
文心快码BaiduComate7 小时前
我用Zulu写了一款塔防游戏给弟弟当生日礼物
人工智能·微信小程序·程序员
文心快码BaiduComate5 天前
轻松实践:用Python实现“名字大作战”游戏,表白Zulu!
前端·后端·微信小程序
文心快码BaiduComate6 天前
用Comate Zulu开发一款微信小程序
前端·后端·微信小程序
Emma歌小白8 天前
如何首次运行小程序后端
微信小程序
赣州云智科技的技术铺子8 天前
【一步步开发AI运动APP】十二、自定义扩展新运动项目1
微信小程序·小程序·云开发·智能小程序
2501_915918418 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张8 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview