PHP项目打包为桌面应用,使用PHP Desktop和inno setup实现。
目录
[PHP Desktop](#PHP Desktop)
[Inno setup](#Inno setup)
[使用PHP Desktop](#使用PHP Desktop)
[解压PHP Desktop](#解压PHP Desktop)
使用工具
PHP Desktop
PHP Desktop是一个将 PHP 运行环境 + Chromium Embedded Framework (CEF) 打包的工具,能让 PHP 脚本脱离传统 Web 服务器(如 Apache/Nginx),直接以桌面应用形式运行。核心优势是快速将 PHP Web 项目转为跨平台桌面程序(Windows/Linux/macOS)。

根据环境进行下载,我用的是windows。
Inno setup
Inno setup用的是汉化版,原版打包程序时没有中文语言包。
在浏览器中搜索 "inno setup 中文版下载"。
我用的是这个,下载后安装一下。

Thinkphp
下载的thinkphp版本要求6以上,因为php_desktop中的php版本为8。
版本低的话可能加载会有问题。
直接使用命令安装:
bash
composer create-project topthink/think tp
开始使用
音乐播放器
使用thinkphp开发一个简单的音乐播放器项目,使用的是mysql数据库,
功能简单介绍:页面是一个音乐播放器,从数据库加载一下音乐记录,然后调用本地的音乐封面和音乐文件进行显示和播放。可以切换上一首、下一首、开始播放、暂停播放、设置音量大小等功能。
数据库
创建一个music.com数据库,字符集采用utf8mb4。
创建数据表
sql
CREATE TABLE `music` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '歌曲名称',
`singer` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '歌手名称',
`path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '音乐文件路径',
`cover` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '封面图片路径(允许为空,因部分音乐可能无封面)',
`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1正常 0禁用',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='音乐表';
导入数据
sql
INSERT INTO `music.com`.`music` (`id`, `title`, `singer`, `path`, `cover`, `sort`, `status`, `create_time`, `update_time`) VALUES (1, '雨一直下', '张宇', '/music/yuyizhixia.mp3', '/cover/yuyizhixia.png', 1, 1, '2025-12-06 17:25:13', '2025-12-12 10:38:41');
INSERT INTO `music.com`.`music` (`id`, `title`, `singer`, `path`, `cover`, `sort`, `status`, `create_time`, `update_time`) VALUES (2, '趁早', '张宇', '/music/chenzao.mp3', '/cover/chenzao.png', 2, 1, '2025-12-06 17:25:13', '2025-12-12 10:38:43');
INSERT INTO `music.com`.`music` (`id`, `title`, `singer`, `path`, `cover`, `sort`, `status`, `create_time`, `update_time`) VALUES (3, '一言难尽', '张宇', '/music/yiyannanjin.mp3', '/cover/yiyannanjin.png', 3, 1, '2025-12-06 17:25:13', '2025-12-12 10:38:45');
数据库配置
创建.env文件,设置项目配置,如下:
php
APP_DEBUG = true
[APP]
DEFAULT_TIMEZONE = Asia/Shanghai
[DATABASE]
TYPE = mysql
HOSTNAME = 127.0.0.1
DATABASE = music.com
USERNAME = root
PASSWORD = root
HOSTPORT = 3306
CHARSET = utf8
DEBUG = true
[LANG]
default_lang = zh-cn
创建模型层
在app中创建model目录,在model目录下创建Music.php文件,内容如下:
php
<?php
namespace app\model;
use think\Model;
class Music extends Model
{
protected $autoWriteTimestamp = true;
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
protected $dateFormat = 'Y-m-d H:i:s';
}
创建控制器
在app/controller中创建Music.php,
播放器页面显示,播放器列表,播放器信息、同步音乐库、上传音乐功能实现,
内容如下:
php
<?php
namespace app\controller;
use think\db\exception\DbException;
use think\exception\ValidateException;
use think\facade\Request;
use think\facade\View;
class Music extends Api
{
/**
* 首页(播放器页面)
*/
public function index()
{
return View::fetch('player');
}
/**
* 获取音乐列表
*/
public function getList()
{
$list = \app\model\Music::where('status', 1)
->order('sort', 'asc')
->field('id, title, singer, path, cover')
->select()->toArray();
return $this->success('success', $list);
}
/**
* 获取单首音乐信息
*/
public function getInfo()
{
$id = Request::param('id', 0, 'intval');
if (!$id) {
return $this->error(400, '参数错误');
}
$music = \app\model\Music::where('id', $id)
->where('status', 1)
->field('id, title, singer, path, cover')
->find();
if (!$music) {
return $this->error(404, '音乐不存在');
}
return $this->success('success', $music->toArray());
}
/**
* 同步音乐库(核心接口)
* @return \think\response\Json
*/
public function syncMusic()
{
try {
// 定义音乐目录(public/music 绝对路径)
$musicDir = public_path('music');
// 扫描目录中的音频文件
$dirMusicList = scan_music_files($musicDir);
$dirFilePaths = array_column($dirMusicList, 'path');
// 获取数据库中的音乐记录
$dbMusicList = \app\model\Music::where('status', 1)
->field('id, path')
->select()->toArray();
$dbFilePaths = array_column($dbMusicList, 'path');
// 对比数据,计算需要新增/删除的记录
$addPaths = array_diff($dirFilePaths, $dbFilePaths); // 目录有、数据库无 → 新增
$delPaths = array_diff($dbFilePaths, $dirFilePaths); // 数据库有、目录无 → 删除
$addCount = 0;
$delCount = 0;
// 执行删除操作
if (!empty($delPaths)) {
$delCount = \app\model\Music::whereIn('path', $delPaths)->delete();
}
// 执行新增操作
if (!empty($addPaths)) {
$addData = [];
foreach ($dirMusicList as $music) {
if (in_array($music['path'], $addPaths)) {
$addData[] = [
'title' => $music['title'],
'singer' => $music['singer'],
'path' => $music['path'],
'cover' => $music['cover'],
'sort' => 0,
'status' => 1,
'create_time' => date('Y-m-d H:i:s'),
'update_time' => date('Y-m-d H:i:s'),
];
}
}
if (!empty($addData)) {
\app\model\Music::insertAll($addData);
$addCount = count($addData);
}
}
// 返回同步结果
$msg = "同步成功!";
if ($addCount) {
$msg .= ",新增 {$addCount} 首";
}
if ($delCount) {
$msg .= ",删除 {$delCount} 首";
}
return $this->success($msg, [
'add_count' => $addCount,
'del_count' => $delCount,
]);
} catch (DbException|\Exception $e) {
return $this->error(500,'同步失败:' . $e->getMessage());
}
}
/**
* 上传本地音乐文件
* @return \think\response\Json
*/
public function uploadMusic()
{
// 强制返回 JSON 格式,关闭 TP6 的 HTML 错误页
header('Content-Type: application/json; charset=utf-8');
try {
// 获取上传文件
$file = Request::file('music_file');
if (!$file) {
return $this->error(400,'请选择要上传的音乐文件');
}
// 2. 手动验证文件(替代 validate() 方法,核心修复!)
$allowedExts = ['mp3', 'wav', 'flac', 'ogg', 'm4a']; // 允许的扩展名
$maxSize = 20 * 1024 * 1024; // 20MB(字节)
// 获取文件基础信息(TP6 UploadedFile 支持的方法)
$fileExt = strtolower($file->getOriginalExtension()); // 原始扩展名(小写)
$fileSize = $file->getSize(); // 文件大小(字节)
$fileName = $file->getOriginalName(); // 原始文件名
// 验证扩展名
if (!in_array($fileExt, $allowedExts)) {
return $this->error(400, "文件格式不支持!仅允许:" . implode(',', $allowedExts));
}
// 验证文件大小
if ($fileSize > $maxSize) {
return $this->error(400, "文件过大!最大支持 20MB,当前文件大小:" . round($fileSize/1024/1024, 2) . "MB");
}
// 确保上传目录存在(避免移动文件失败)
$uploadDir = app()->getRootPath() . 'public/music';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true); // 递归创建目录
}
// 移动文件到 public/music 目录
// 保留原始文件名(可能覆盖同名文件)
$savePath = $file->move($uploadDir, $fileName);
// 组装返回数据(前端可访问的路径)
$filePath = '/music/' . $savePath->getBasename(); // 如 /music/张宇-雨一直下.mp3
// 返回上传成功结果
return $this->success('文件上传成功', [
'file_name' => $fileName,
'file_path' => $filePath
]);
} catch (ValidateException|\Exception $e) {
return $this->error(500,'上传失败:' . $e->getMessage());
}
}
}
创建视图
在view下创建music目录,在music目录下创建player.html视图文件,
样式和js代码、html内容都在一起,内容比较多。
内容如下:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>简单音乐播放器 - ThinkPHP 6.1</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", sans-serif;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.player-box {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 30px;
margin-bottom: 30px;
display: flex;
align-items: center;
gap: 30px;
}
.cover-img {
width: 200px;
height: 200px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
}
.cover-img img {
width: 100%;
height: 100%;
object-fit: cover;
}
.player-info {
flex: 1;
}
.music-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
}
.music-singer {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 6px;
background: #eee;
border-radius: 3px;
margin-bottom: 20px;
cursor: pointer;
}
.progress {
height: 100%;
background: #1890ff;
border-radius: 3px;
width: 0%;
}
.control-buttons {
display: flex;
gap: 10px; /* 缩小间距避免拥挤 */
align-items: center;
}
/* 同步/列表图标按钮样式 */
#sync-icon, #list-icon {
width: 40px;
height: 40px;
font-size: 18px;
cursor: pointer;
}
/* 右侧悬浮列表面板样式 */
.music-list-panel {
position: fixed;
right: 0;
top: 0;
width: 300px;
height: 100vh; /* 占满视口高度 */
background: #fff;
box-shadow: -2px 0 10px rgba(0,0,0,0.1);
padding: 20px;
z-index: 999; /* 确保在最上层 */
display: none; /* 默认隐藏 */
overflow-y: auto; /* 列表过长时滚动 */
}
/* 列表项样式(适配面板) */
.music-item {
padding: 12px 15px;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 15px;
}
.music-item:hover {
background: #f0f7ff;
}
.music-item.active {
background: #e6f7ff;
border-left: 4px solid #1890ff;
}
.btn {
width: 50px;
height: 50px;
border-radius: 50%;
background: #1890ff;
color: #fff;
border: none;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.btn:hover {
background: #096dd9;
transform: scale(1.05);
}
.btn-play {
width: 60px;
height: 60px;
font-size: 24px;
}
.music-list {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
}
.list-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.music-item {
padding: 12px 15px;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 15px;
}
.music-item:hover {
background: #f0f7ff;
}
.music-item.active {
background: #e6f7ff;
border-left: 4px solid #1890ff;
}
.item-cover {
width: 40px;
height: 40px;
border-radius: 4px;
overflow: hidden;
}
.item-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.item-info {
flex: 1;
}
.item-title {
font-weight: 500;
margin-bottom: 3px;
}
.item-singer {
font-size: 12px;
color: #666;
}
/* 音量控制样式 */
.volume-control {
display: flex;
align-items: center;
gap: 10px;
margin-left: 15px; /* 缩小左侧间距 */
}
.volume-icon {
font-size: 18px;
color: #666;
cursor: pointer;
transition: color 0.3s;
}
.volume-icon:hover {
color: #1890ff;
}
.volume-slider {
width: 80px;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: #eee;
border-radius: 2px;
outline: none;
cursor: pointer;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #1890ff;
transition: all 0.3s;
}
.volume-slider::-webkit-slider-thumb:hover {
background: #096dd9;
transform: scale(1.2);
}
.volume-percent {
font-size: 12px;
color: #666;
width: 30px;
text-align: center;
}
/* 提示消息样式 */
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 6px;
color: #fff;
font-size: 14px;
z-index: 9999;
opacity: 0;
transition: opacity 0.3s;
}
.toast.success {
background: #52c41a;
}
.toast.error {
background: #f5222d;
}
.toast.show {
opacity: 1;
}
/* 上传组件样式 */
.upload-box {
background: #f9f9f9;
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
cursor: pointer;
transition: all 0.3s;
}
.upload-box:hover {
border-color: #1890ff;
background: #f0f7ff;
}
.upload-box.active {
border-color: #52c41a;
background: #f6ffed;
}
.upload-icon {
font-size: 40px;
color: #ccc;
margin-bottom: 10px;
}
.upload-text {
font-size: 16px;
color: #666;
margin-bottom: 5px;
}
.upload-tip {
font-size: 12px;
color: #999;
}
#upload-file {
display: none; /* 隐藏原生文件选择框 */
}
.upload-progress {
height: 6px;
background: #eee;
border-radius: 3px;
margin-top: 15px;
overflow: hidden;
display: none;
}
.upload-progress-bar {
height: 100%;
background: #1890ff;
width: 0%;
transition: width 0.3s;
}
/* 调整同步按钮位置,和上传组件对齐 */
.operation-bar {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 15px;
}
/* 同步图标禁用状态样式 */
#sync-icon:disabled {
cursor: not-allowed; /* 鼠标变为禁止样式 */
opacity: 0.6; /* 透明度降低,提示不可点击 */
background: #ccc; /* 背景变灰 */
}
/* 同步中加载图标样式(可选) */
#sync-icon.loading {
animation: spin 1s linear infinite; /* 旋转动画 */
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<!-- 播放器主体 -->
<div class="player-box">
<div class="cover-img">
<img id="current-cover" src="/cover/default.png" alt="封面">
</div>
<div class="player-info">
<div class="music-title" id="current-title">请选择歌曲</div>
<div class="music-singer" id="current-singer">--</div>
<div class="progress-bar" id="progress-bar">
<div class="progress" id="progress"></div>
</div>
<div class="control-buttons">
<button class="btn" id="prev-btn">◀</button>
<button class="btn btn-play" id="play-btn">▶</button>
<button class="btn" id="next-btn">▶</button>
<!-- 新增:同步图标(替换原同步按钮) -->
<button class="btn" id="sync-icon" title="同步音乐库">🔄</button>
<!-- 新增:列表图标(控制列表显示) -->
<button class="btn" id="list-icon" title="显示音乐列表">📜</button>
<!-- 音量控制模块(调整间距避免拥挤) -->
<div class="volume-control">
<span class="volume-icon" id="volume-icon">🔊</span>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="1">
<span class="volume-percent" id="volume-percent">100%</span>
</div>
</div>
</div>
</div>
<!-- 上传组件(保留) -->
<div class="upload-box" id="upload-box">
<input type="file" id="upload-file" accept=".mp3,.wav,.flac,.ogg,.m4a" multiple>
<div class="upload-icon">📁</div>
<div class="upload-text">点击或拖拽文件到此处上传</div>
<div class="upload-tip">支持格式:MP3、WAV、FLAC、OGG、M4A(单文件最大20MB)</div>
<div class="upload-progress" id="upload-progress">
<div class="upload-progress-bar" id="upload-progress-bar"></div>
</div>
</div>
<!-- 右侧悬浮音乐列表面板(默认隐藏) -->
<div class="music-list-panel" id="music-list-panel">
<div class="list-title">音乐列表</div>
<div id="music-list-container"></div>
</div>
<!-- 提示消息容器 -->
<div class="toast" id="toast"></div>
</div>
<!-- 音频元素(隐藏) -->
<audio id="audio-player" preload="auto">
<source id="audio-source" src="" type="audio/mpeg">
您的浏览器不支持音频播放
</audio>
<script>
// 全局变量
let musicList = []; // 音乐列表
let currentIndex = -1; // 当前播放索引
let isPlaying = false; // 播放状态
let currentVolume = 1; // 当前音量(0-1,默认最大音量)
let isMuted = false; // 是否静音
// DOM元素
const audioPlayer = document.getElementById('audio-player');
const audioSource = document.getElementById('audio-source');
const playBtn = document.getElementById('play-btn');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const progressBar = document.getElementById('progress-bar');
const progress = document.getElementById('progress');
const currentTitle = document.getElementById('current-title');
const currentSinger = document.getElementById('current-singer');
const currentCover = document.getElementById('current-cover');
const musicListContainer = document.getElementById('music-list-container');
// 同步相关dom
const toast = document.getElementById('toast');
// 音量相关dom
const volumeIcon = document.getElementById('volume-icon');
const volumeSlider = document.getElementById('volume-slider');
const volumePercent = document.getElementById('volume-percent');
// 上传相关元素
const uploadBox = document.getElementById('upload-box');
const uploadFile = document.getElementById('upload-file');
const uploadProgress = document.getElementById('upload-progress');
const uploadProgressBar = document.getElementById('upload-progress-bar');
// 新增DOM元素(列表/同步图标、悬浮面板)
const syncIcon = document.getElementById('sync-icon');
const listIcon = document.getElementById('list-icon');
const musicListPanel = document.getElementById('music-list-panel');
// 初始化:加载音乐列表
window.onload = async function() {
await loadMusicList();
bindEvents();
renderMusicList(); // 预渲染列表(点击图标时直接显示)
};
// 加载音乐列表
async function loadMusicList() {
try {
const response = await fetch('index.php/music/getList');
const res = await response.json();
if (res.code === 200) {
musicList = res.data;
renderMusicList();
} else {
alert('加载音乐列表失败:' + res.msg);
}
} catch (error) {
console.error('加载音乐列表出错:', error);
alert('加载音乐列表出错,请刷新重试');
}
}
// 渲染音乐列表
function renderMusicList() {
musicListContainer.innerHTML = '';
musicList.forEach((music, index) => {
const item = document.createElement('div');
item.className = `music-item ${index === currentIndex ? 'active' : ''}`;
item.dataset.index = index;
item.innerHTML = `
<div class="item-cover">
<img src="${music.cover || '/cover/default.png'}" alt="${music.title}">
</div>
<div class="item-info">
<div class="item-title">${music.title}</div>
<div class="item-singer">${music.singer}</div>
</div>
`;
musicListContainer.appendChild(item);
});
}
// 绑定事件
function bindEvents() {
// 播放/暂停按钮
playBtn.addEventListener('click', togglePlay);
// 上一首
prevBtn.addEventListener('click', playPrev);
// 下一首
nextBtn.addEventListener('click', playNext);
// 进度条点击
progressBar.addEventListener('click', seek);
// 音频时间更新
audioPlayer.addEventListener('timeupdate', updateProgress);
// 音频播放结束
audioPlayer.addEventListener('ended', playNext);
// 播放列表
// 1. 同步图标点击事件(替换原同步按钮)
syncIcon.addEventListener('click', syncMusicLibrary);
// 2. 列表图标点击:显示右侧悬浮列表
listIcon.addEventListener('click', () => {
musicListPanel.style.display = 'block';
renderMusicList(); // 确保列表是最新数据
});
// 3. 列表面板鼠标离开:隐藏列表
musicListPanel.addEventListener('mouseleave', () => {
musicListPanel.style.display = 'none';
});
// 4. 列表项点击播放(原有逻辑保留,适配新面板)
musicListContainer.addEventListener('click', function(e) {
const item = e.target.closest('.music-item');
if (item) {
const index = parseInt(item.dataset.index);
playMusic(index);
// 鼠标离开面板后自动隐藏,无需手动关闭
}
});
// 新增:音量滑块事件
volumeSlider.addEventListener('input', adjustVolume);
// 新增:静音图标点击事件
volumeIcon.addEventListener('click', toggleMute);
// 新增:音频音量变化事件(同步滑块和图标)
audioPlayer.addEventListener('volumechange', syncVolumeUI);
// 新增:上传框点击触发文件选择
uploadBox.addEventListener('click', () => {
uploadFile.click();
});
// 新增:文件选择后触发上传
uploadFile.addEventListener('change', (e) => {
const files = e.target.files;
if (files.length > 0) {
uploadFiles(files);
}
});
// 新增:拖拽上传(可选,增强体验)
uploadBox.addEventListener('dragover', (e) => {
e.preventDefault();
uploadBox.classList.add('active');
});
uploadBox.addEventListener('dragleave', () => {
uploadBox.classList.remove('active');
});
uploadBox.addEventListener('drop', (e) => {
e.preventDefault();
uploadBox.classList.remove('active');
const files = e.dataTransfer.files;
if (files.length > 0) {
uploadFiles(files);
}
});
}
// 播放/暂停切换
function togglePlay() {
if (currentIndex === -1 && musicList.length > 0) {
// 还没选择歌曲,默认播放第一首
playMusic(0);
return;
}
if (isPlaying) {
audioPlayer.pause();
playBtn.innerHTML = '▶';
} else {
audioPlayer.play();
playBtn.innerHTML = '❚❚';
}
isPlaying = !isPlaying;
}
// 播放指定索引的音乐
function playMusic(index) {
if (index < 0 || index >= musicList.length) return;
currentIndex = index;
const music = musicList[index];
// 更新播放器信息
currentTitle.textContent = music.title;
currentSinger.textContent = music.singer;
currentCover.src = music.cover || '/cover/default.png';
audioSource.src = music.path;
audioPlayer.load();
audioPlayer.play();
// 新增:同步音量设置(切换歌曲时保持当前音量和静音状态)
audioPlayer.volume = currentVolume;
audioPlayer.muted = isMuted;
syncVolumeUI(); // 同步 UI
// 更新状态
isPlaying = true;
playBtn.innerHTML = '❚❚';
// 更新列表选中状态
renderMusicList();
}
// 播放上一首
function playPrev() {
if (musicList.length === 0) return;
let index = currentIndex - 1;
if (index < 0) index = musicList.length - 1;
playMusic(index);
}
// 播放下一首
function playNext() {
if (musicList.length === 0) return;
let index = currentIndex + 1;
if (index >= musicList.length) index = 0;
playMusic(index);
}
// 更新进度条
function updateProgress() {
const duration = audioPlayer.duration;
const currentTime = audioPlayer.currentTime;
const percent = (currentTime / duration) * 100;
progress.style.width = `${percent}%`;
}
// 进度条跳转
function seek(e) {
const rect = progressBar.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const duration = audioPlayer.duration;
audioPlayer.currentTime = pos * duration;
progress.style.width = `${pos * 100}%`;
}
/**
* 调整音量(滑块拖动时触发)
*/
function adjustVolume() {
currentVolume = parseFloat(volumeSlider.value);
audioPlayer.volume = currentVolume;
audioPlayer.muted = false; // 调整音量时自动取消静音
isMuted = false;
syncVolumeUI(); // 同步 UI 显示
}
/**
* 切换静音/取消静音
*/
function toggleMute() {
isMuted = !isMuted;
audioPlayer.muted = isMuted;
syncVolumeUI(); // 同步 UI 显示
}
/**
* 同步音量 UI 显示(图标、滑块、百分比)
*/
function syncVolumeUI() {
// 同步滑块值(如果是手动静音,滑块保持原音量)
if (!isMuted) {
volumeSlider.value = audioPlayer.volume;
currentVolume = audioPlayer.volume;
}
// 计算音量百分比
const percent = Math.round(currentVolume * 100);
volumePercent.textContent = `${percent}%`;
// 切换音量图标
if (isMuted || percent === 0) {
volumeIcon.textContent = '🔇'; // 静音图标
} else if (percent < 30) {
volumeIcon.textContent = '🔈'; // 低音量图标
} else {
volumeIcon.textContent = '🔊'; // 正常音量图标
}
}
/**
* 显示提示消息
* @param {string} msg 消息内容
* @param {boolean} isSuccess 是否成功
*/
function showToast(msg, isSuccess = true) {
toast.textContent = msg;
toast.className = `toast ${isSuccess ? 'success' : 'error'} show`;
// 3秒后隐藏提示
setTimeout(() => {
toast.className = 'toast';
}, 3000);
}
/**
* 同步音乐库核心逻辑
*/
async function syncMusicLibrary() {
// 1. 同步开始:禁用同步图标,修改样式/图标
if (syncIcon) {
syncIcon.disabled = true; // 禁用按钮,无法点击
syncIcon.classList.add('loading'); // 添加旋转动画
syncIcon.textContent = '⟳'; // 替换为加载中图标(可选)
}
try {
// 原有同步逻辑(调用接口、提示消息等)保持不变
const response = await fetch('/index.php?s=music/sync');
const res = await response.json();
if (res.code === 200) {
showToast(res.msg);
await loadMusicList();
renderMusicList();
// 处理播放状态(原有逻辑)
if (currentIndex >= 0 && !musicList[currentIndex]) {
currentIndex = -1;
isPlaying = false;
playBtn.innerHTML = '▶';
currentTitle.textContent = '请选择歌曲';
currentSinger.textContent = '--';
currentCover.src = '/cover/default.jpg';
audioSource.src = '';
audioPlayer.load();
}
} else {
showToast(res.msg, false);
}
} catch (error) {
showToast('同步失败:网络错误或接口不可用', false);
console.error('同步音乐库出错:', error);
} finally {
// 2. 同步结束:恢复按钮状态(无论成功/失败都执行)
if (syncIcon) {
syncIcon.disabled = false; // 启用按钮
syncIcon.classList.remove('loading'); // 移除旋转动画
syncIcon.textContent = '🔄'; // 还原原有图标
}
}
}
/**
* 上传文件核心逻辑
* @param {FileList} files 选中的文件列表
*/
async function uploadFiles(files) {
// 过滤非音频文件
const allowedExts = ['.mp3', '.wav', '.flac', '.ogg', '.m4a'];
const audioFiles = Array.from(files).filter(file => {
const ext = '.' + file.name.split('.').pop().toLowerCase();
return allowedExts.includes(ext);
});
if (audioFiles.length === 0) {
showToast('请选择支持的音频格式文件', false);
return;
}
// 显示上传进度条
uploadProgress.style.display = 'block';
uploadProgressBar.style.width = '0%';
let successCount = 0;
let failCount = 0;
// 批量上传文件(逐个上传)
for (let i = 0; i < audioFiles.length; i++) {
const file = audioFiles[i];
try {
// 构建 FormData
const formData = new FormData();
formData.append('music_file', file);
// 发送上传请求(兼容模式路径,适配 php_desktop)
const response = await fetch('/index.php?s=music/upload', {
method: 'POST',
headers: {
'Accept': 'application/json; charset=utf-8',
},
body: formData,
// 监听上传进度
onUploadProgress: (e) => {
if (e.lengthComputable) {
// 计算总进度(当前文件进度 / 文件总数 + 已完成文件数 / 文件总数)
const fileProgress = e.loaded / e.total;
const totalProgress = (i + fileProgress) / audioFiles.length;
uploadProgressBar.style.width = `${totalProgress * 100}%`;
}
}
});
const res = await response.json();
if (res.code === 200) {
successCount++;
} else {
failCount++;
console.error(`文件 ${file.name} 上传失败:`, res.msg);
}
} catch (error) {
failCount++;
console.error(`文件 ${file.name} 上传失败:`, error);
}
}
// 上传完成:更新进度条 + 提示结果 + 自动同步音乐库
uploadProgressBar.style.width = '100%';
setTimeout(() => {
uploadProgress.style.display = 'none';
uploadProgressBar.style.width = '0%';
}, 500);
// 显示上传结果提示
if (successCount > 0 && failCount === 0) {
showToast(`上传成功!共上传 ${successCount} 首音乐`);
} else if (successCount > 0 && failCount > 0) {
showToast(`部分上传成功:成功 ${successCount} 首,失败 ${failCount} 首`, false);
} else {
showToast(`上传失败!共 ${failCount} 首文件上传失败`, false);
}
// 自动同步音乐库(更新数据库和列表)
if (successCount > 0) {
await syncMusicLibrary();
}
// 清空文件选择框(允许重复选择同一文件)
uploadFile.value = '';
}
</script>
</body>
</html>
设置路由
在route/app.php中设置功能路由,如下:
php
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
use think\facade\Route;
// 播放器首页
Route::get('/', 'Music/index');
Route::get('/player', 'Music/index');
// 接口路由
Route::get('/music/getList', 'Music/getList');
Route::get('/music/getInfo', 'Music/getInfo');
// 音乐库同步接口
Route::get('/music/sync', 'Music/syncMusic');
// 音乐上传接口
Route::post('/music/upload', 'Music/uploadMusic');
准备项目文件
需要准备音乐封面、音频文件、项目icon文件。
创建cover和music目录,存储封面和音乐文件,favicon.ico为项目icon文件。

使用PH P Desktop
解压PHP Desktop
把下载的php desktop压缩包在本地解压,内容如下:

目录和文件介绍
locales: 内置 Chrome/CEF(Chromium Embedded Framework)内核的本地化(多语言)资源目录,负责支撑内核层面的语言适配,和 PHP/ThinkPHP/FastAdmin 的业务级语言配置完全无关。
php:是php运行程序和扩展程序
www:可以放置php项目文件,或者php项目入口文件。
settings.json php desktop项目配置文件,在这里修改配置
运行项目
直接双击使用phpdesktop-chrome.exe运行项目,会启动php环境运行demo项目,
在www目录下有一些php demo文件。
效果如下:

加载项目
放入thinkphp文件
把thinkphp项目放入php desktop项目中,然后修改配置,并正常运行起项目。
操作步骤如下:
把www目录删除,然后把thinkphp中tp下的所有文件和目录复制放到www同级中。
如下:

修改配置
把www目录改为public,并设置项目信息:项目入口文件、项目icon文件、项目名称等。
如下:

运行项目
双击phpdesktop-chrome.exe运行项目,效果如下:

打包项目
修改配置
打包前修改项目配置,关闭调试如下:

关闭thinkphp debug
创建脚本文件
安装Inno setup汉化程序后,创建脚本文件。
步骤如下:

下一步 填写应用程序信息

设置应用程序文件夹

添加项目可执行文件,把整个项目通过添加文件夹加入。



之后一直下一步,然后设定安装icon



选择不编辑
然后修改脚本,增加icon设置

开始编译
选择构建->编译
保存脚本
开始编译

编译完成后,在phpdesktop中的Output目录下生成exe文件,如下:

最终效果
桌面快捷方式
双击后正常安装,安装完成后如下:

首页
播放音乐并可点击或者拖拽文件上传

显示播放列表
点击列表图标后 从右侧显示播放列表。

总结
使用PHP Desktop和inno setup确实可以把php项目打包为桌面应用,安装、使用和卸载与其他桌面应用并无差异,但是首先安装应用过大,而且无法自定义软件图标,软件源码也不能加密。