13 Commits

12 changed files with 1166 additions and 788 deletions

View File

@@ -864,6 +864,10 @@
"role": "manager" "role": "manager"
} }
], ],
"musicConfig": {
"enabled": true,
"filePath": "/assets/music/1765778401201.mp3"
},
"displayConfig": { "displayConfig": {
"showBonusModule": true, "showBonusModule": true,
"individual": { "individual": {
@@ -951,35 +955,8 @@
"date": "2026-02-08", "date": "2026-02-08",
"time": "00:00:00" "time": "00:00:00"
}, },
"drumConfig": { "music": {
"sound": { "enabled": true,
"volume": 1, "filePath": "/assets/music/1765778401201.mp3"
"frequency1": 150,
"frequency2": 100,
"attackTime": 0.01,
"decayTime": 0.3,
"type1": "sine",
"type2": "triangle",
"enabled": false
},
"animation": {
"beatInterval": 200,
"beatScale": 1.3,
"beatTranslateY": -15,
"beatRotate": 5,
"idlePulseDuration": 2,
"beatDuration": 100,
"enabled": true
},
"pattern": {
"strongBeats": [
1,
4
],
"totalBeats": 4,
"accentMultiplier": 1.5,
"accentFrequencyOffset": 10,
"accentAnimation": 50
}
} }
} }

379
package-lock.json generated
View File

@@ -9,13 +9,14 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.1.0", "express": "^5.2.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"nodemon": "^3.1.11",
"sass": "^1.94.0", "sass": "^1.94.0",
"vite": "^7.2.2" "vite": "^7.2.2"
} }
@@ -1282,30 +1283,92 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/anymatch/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/append-field": { "node_modules/append-field": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.1.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"debug": "^4.4.0", "debug": "^4.4.3",
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.7.0",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.14.0", "qs": "^6.14.0",
"raw-body": "^3.0.0", "raw-body": "^3.0.1",
"type-is": "^2.0.0" "type-is": "^2.0.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
} }
}, },
"node_modules/braces": { "node_modules/braces": {
@@ -1314,7 +1377,6 @@
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.1.1"
}, },
@@ -1393,6 +1455,13 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-stream": { "node_modules/concat-stream": {
"version": "1.6.2", "version": "1.6.2",
"resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz",
@@ -1647,18 +1716,19 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "5.1.0", "version": "5.2.1",
"resolved": "https://registry.npmmirror.com/express/-/express-5.1.0.tgz", "resolved": "https://registry.npmmirror.com/express/-/express-5.2.1.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.1",
"content-disposition": "^1.0.0", "content-disposition": "^1.0.0",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"cookie": "^0.7.1", "cookie": "^0.7.1",
"cookie-signature": "^1.2.1", "cookie-signature": "^1.2.1",
"debug": "^4.4.0", "debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0", "encodeurl": "^2.0.0",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"etag": "^1.8.1", "etag": "^1.8.1",
@@ -1712,7 +1782,6 @@
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@@ -1816,6 +1885,19 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
@@ -1828,6 +1910,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -1853,42 +1945,48 @@
} }
}, },
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"depd": "2.0.0", "depd": "~2.0.0",
"inherits": "2.0.4", "inherits": "~2.0.4",
"setprototypeof": "1.2.0", "setprototypeof": "~1.2.0",
"statuses": "2.0.1", "statuses": "~2.0.2",
"toidentifier": "1.0.1" "toidentifier": "~1.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} },
}, "funding": {
"node_modules/http-errors/node_modules/statuses": { "type": "opencollective",
"version": "2.0.1", "url": "https://opencollective.com/express"
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
} }
}, },
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.7.1",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.1.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0" "safer-buffer": ">= 2.1.2 < 3.0.0"
}, },
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true,
"license": "ISC"
},
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz", "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz",
@@ -1911,13 +2009,25 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -1928,7 +2038,6 @@
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
}, },
@@ -1942,7 +2051,6 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@@ -2048,6 +2156,19 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
@@ -2172,6 +2293,96 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/nodemon": {
"version": "3.1.11",
"resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.11.tgz",
"integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
},
"bin": {
"nodemon": "bin/nodemon.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nodemon"
}
},
"node_modules/nodemon/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/nodemon/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/nodemon/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
@@ -2299,6 +2510,13 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmmirror.com/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true,
"license": "MIT"
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
@@ -2324,36 +2542,20 @@
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.1.tgz", "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "~3.1.2",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"iconv-lite": "0.7.0", "iconv-lite": "~0.7.0",
"unpipe": "1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -2494,6 +2696,19 @@
"@parcel/watcher": "^2.4.1" "@parcel/watcher": "^2.4.1"
} }
}, },
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": { "node_modules/send": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/send/-/send-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/send/-/send-1.2.0.tgz",
@@ -2609,6 +2824,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2650,6 +2878,19 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -2673,7 +2914,6 @@
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@@ -2690,6 +2930,16 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/touch": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/touch/-/touch-3.1.1.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
"dev": true,
"license": "ISC",
"bin": {
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/type-is": { "node_modules/type-is": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz",
@@ -2710,6 +2960,13 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true,
"license": "MIT"
},
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",

View File

@@ -12,13 +12,14 @@
}, },
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^5.1.0", "express": "^5.2.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"nodemon": "^3.1.11",
"sass": "^1.94.0", "sass": "^1.94.0",
"vite": "^7.2.2" "vite": "^7.2.2"
} }

Binary file not shown.

202
server.js
View File

@@ -13,14 +13,15 @@ const app = express();
const PORT = 3000; const PORT = 3000;
const CONFIG_FILE_PATH = path.join(__dirname, 'data', 'config.json'); const CONFIG_FILE_PATH = path.join(__dirname, 'data', 'config.json');
// 创建上传目录 // ===================== 原有图片上传配置(保留) =====================
// 创建图片上传目录
const uploadDir = path.join(__dirname, 'uploads'); const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) { if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true }); fs.mkdirSync(uploadDir, { recursive: true });
} }
// 配置multer // 图片上传multer配置
const storage = multer.diskStorage({ const imageStorage = multer.diskStorage({
destination: (req, file, cb) => { destination: (req, file, cb) => {
cb(null, uploadDir); cb(null, uploadDir);
}, },
@@ -32,8 +33,8 @@ const storage = multer.diskStorage({
} }
}); });
const upload = multer({ const imageUpload = multer({
storage, storage: imageStorage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB限制 limits: { fileSize: 5 * 1024 * 1024 }, // 5MB限制
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/; const allowedTypes = /jpeg|jpg|png|gif/;
@@ -47,15 +48,55 @@ const upload = multer({
} }
}); });
// 中间件 // ===================== 新增:音乐上传配置 =====================
// 创建音乐上传目录(对应前端访问路径)
const musicUploadDir = path.join(__dirname, 'public', 'assets', 'music');
// 确保目录存在(不存在则创建)
if (!fs.existsSync(musicUploadDir)) {
fs.mkdirSync(musicUploadDir, { recursive: true });
}
// 音乐上传multer配置
const musicStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, musicUploadDir); // 保存到public/assets/music
},
filename: (req, file, cb) => {
// 保留原文件名+时间戳,避免重复
const timestamp = Date.now();
const ext = path.extname(file.originalname);
const filename = `${timestamp}${ext}`;
cb(null, filename);
}
});
const musicUpload = multer({
storage: musicStorage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB音乐文件限制
fileFilter: (req, file, cb) => {
// 仅允许MP3格式
const allowedTypes = /mp3/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype) || file.mimetype === 'audio/mpeg';
if (extname && mimetype) {
return cb(null, true);
} else {
cb(new Error('只允许上传MP3格式的音频文件'));
}
}
});
// ===================== 中间件(保留+优化) =====================
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
// 静态文件服务Vue应用上传的图片) // 静态文件服务Vue应用上传的图片、音乐文件
app.use(express.static(path.join(__dirname, 'dist'))); app.use(express.static(path.join(__dirname, 'dist')));
app.use('/uploads', express.static(uploadDir)); app.use('/uploads', express.static(uploadDir));
app.use('/assets/music', express.static(musicUploadDir)); // 新增:音乐文件静态访问
// API: 获取配置数据 // ===================== 原有API保留 =====================
// API: 获取整体配置数据
app.get('/api/config', (req, res) => { app.get('/api/config', (req, res) => {
try { try {
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8'); const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
@@ -66,7 +107,7 @@ app.get('/api/config', (req, res) => {
} }
}); });
// API: 保存配置数据 // API: 保存整体配置数据
app.post('/api/config', (req, res) => { app.post('/api/config', (req, res) => {
try { try {
fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(req.body, null, 2), 'utf8'); fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(req.body, null, 2), 'utf8');
@@ -77,8 +118,140 @@ app.post('/api/config', (req, res) => {
} }
}); });
// ===================== 新增音乐专属API =====================
// API: 获取音乐配置单独返回musicConfig
app.get('/api/musicConfig', (req, res) => {
try {
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
const config = JSON.parse(configData);
// 兜底如果没有musicConfig返回默认值
res.json(config.musicConfig || {
enabled: false,
filePath: '/assets/music/background.mp3'
});
} catch (error) {
console.error('读取音乐配置失败:', error);
res.status(500).json({
enabled: false,
filePath: '/assets/music/background.mp3'
});
}
});
// ===================== 新增获取已上传音乐列表API =====================
app.get('/api/music/list', (req, res) => {
try {
// 音乐文件存储目录(和之前配置的一致)
const musicDir = path.join(__dirname, 'public', 'assets', 'music');
// 先判断目录是否存在,避免报错
if (!fs.existsSync(musicDir)) {
return res.json({ success: true, data: [] });
}
// 读取目录下的所有MP3文件
const files = fs.readdirSync(musicDir).filter(file => file.endsWith('.mp3'));
// 返回文件名+访问路径
const musicList = files.map(file => ({
filename: file,
filePath: `/assets/music/${file}`
}));
res.json({ success: true, data: musicList });
} catch (error) {
console.error('读取音乐列表失败:', error);
res.status(500).json({ success: false, error: '读取音乐列表失败' });
}
});
// ===================== 新增结束 =====================
// API: 更新音乐配置仅更新musicConfig节点不影响其他配置
app.post('/api/musicConfig', (req, res) => {
try {
// 1. 读取原有配置
const configData = fs.readFileSync(CONFIG_FILE_PATH, 'utf8');
const config = JSON.parse(configData);
// 2. 校验参数
const { enabled, filePath } = req.body;
if (typeof enabled !== 'boolean' || !filePath) {
return res.status(400).json({
success: false,
error: '参数错误enabled必须为布尔值filePath不能为空'
});
}
// 3. 更新musicConfig节点保留其他配置不变
config.musicConfig = { enabled, filePath };
// 4. 写入配置文件
fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf8');
res.json({
success: true,
data: config.musicConfig
});
} catch (error) {
console.error('更新音乐配置失败:', error);
res.status(500).json({
success: false,
error: '更新音乐配置失败'
});
}
});
// API: 上传音乐文件
app.post('/api/upload/music', musicUpload.single('musicFile'), (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: '没有选择要上传的音乐文件'
});
}
// 返回前端可访问的音乐路径对应public/assets/music
const relativePath = `/assets/music/${req.file.filename}`;
res.json({
success: true,
filePath: relativePath, // 音乐访问路径
filename: req.file.filename,
originalName: req.file.originalname
});
} catch (error) {
console.error('音乐文件上传失败:', error);
res.status(500).json({
success: false,
error: error.message || '音乐文件上传失败'
});
}
});
// ===================== 兼容前端旧路径 /upload-music =====================
app.post('/upload-music', (req, res) => {
musicUpload.single('musicFile')(req, res, (err) => {
if (err) {
return res.status(500).json({
success: false,
error: err.message || '音乐文件上传失败'
});
}
if (!req.file) {
return res.status(400).json({
success: false,
error: '没有选择要上传的音乐文件'
});
}
const relativePath = `/assets/music/${req.file.filename}`;
res.json({
success: true,
filePath: relativePath,
filename: req.file.filename,
originalName: req.file.originalname
});
});
});
// ===================== 原有图片上传/删除API保留 =====================
// API: 上传图片 // API: 上传图片
app.post('/api/upload', upload.single('image'), (req, res) => { app.post('/api/upload', imageUpload.single('image'), (req, res) => {
try { try {
if (!req.file) { if (!req.file) {
return res.status(400).json({ error: '没有文件上传' }); return res.status(400).json({ error: '没有文件上传' });
@@ -115,16 +288,23 @@ app.delete('/api/upload/:filename', (req, res) => {
} }
}); });
// ===================== 前端路由兼容(保留) =====================
// 处理Vue Router历史模式 - 使用正则表达式代替通配符 // 处理Vue Router历史模式 - 使用正则表达式代替通配符
app.get(/^((?!\/api).)*$/, (req, res) => { app.get(/^((?!\/api).)*$/, (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html')); res.sendFile(path.join(__dirname, 'dist', 'index.html'));
}); });
// ===================== 服务器启动(保留+优化) =====================
// 启动服务器并监听错误 // 启动服务器并监听错误
const server = app.listen(PORT, '0.0.0.0', () => { const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`服务器运行在 http://localhost:${PORT}`); console.log(`服务器运行在 http://localhost:${PORT}`);
console.log('服务器已成功启动,可以访问 http://localhost:3000'); console.log('服务器已成功启动,可以访问 http://localhost:3000');
console.log('API端点: GET/POST /api/config'); console.log('API端点:');
console.log(' - 整体配置: GET/POST /api/config');
console.log(' - 音乐配置: GET/POST /api/musicConfig');
console.log(' - 图片上传: POST /api/upload');
console.log(' - 音乐上传: POST /api/upload/music');
console.log(' - 图片删除: DELETE /api/upload/:filename');
}); });
// 监听服务器错误 // 监听服务器错误

View File

@@ -12,8 +12,6 @@ import {
saveDisplayConfig as saveDisplayConfigToConfig, saveDisplayConfig as saveDisplayConfigToConfig,
getBattleEndTime, getBattleEndTime,
saveBattleEndTime as saveBattleEndTimeToConfig, saveBattleEndTime as saveBattleEndTimeToConfig,
getDrumConfig,
saveDrumConfig as saveDrumConfigToConfig,
getBonusRules, getBonusRules,
saveBonusRules as saveBonusRulesToConfig saveBonusRules as saveBonusRulesToConfig
} from '../services/configService'; } from '../services/configService';
@@ -29,7 +27,7 @@ export let bonusRules = [
export let systemUsers = []; export let systemUsers = [];
export let displayConfig = null; export let displayConfig = null;
export let battleEndTime = { date: new Date().toISOString().split('T')[0], time: '00:00:00' }; export let battleEndTime = { date: new Date().toISOString().split('T')[0], time: '00:00:00' };
export let drumConfig = {};
// 保存结束时间 // 保存结束时间
export const saveBattleEndTime = async (endTime) => { export const saveBattleEndTime = async (endTime) => {
@@ -59,30 +57,7 @@ export const saveDisplayConfig = async (config) => {
return await saveDisplayConfigToConfig(config); return await saveDisplayConfigToConfig(config);
}; };
// 保存战鼓配置
export const saveDrumConfig = async (config) => {
console.log('保存战鼓配置:', config);
// 深度合并配置确保嵌套对象如sound、animation、pattern的属性不会丢失
drumConfig = {
...drumConfig,
...config,
sound: {
...drumConfig.sound,
...config.sound
},
animation: {
...drumConfig.animation,
...config.animation
},
pattern: {
...drumConfig.pattern,
...config.pattern
}
};
return await saveDrumConfigToConfig(drumConfig);
};
// 保存奖金规则 // 保存奖金规则
export const saveBonusRules = async (rules) => { export const saveBonusRules = async (rules) => {
@@ -118,7 +93,7 @@ export const refreshData = async () => {
systemUsers = await getSystemUsers(); systemUsers = await getSystemUsers();
displayConfig = await getDisplayConfig(); displayConfig = await getDisplayConfig();
battleEndTime = await getBattleEndTime(); battleEndTime = await getBattleEndTime();
drumConfig = await getDrumConfig();
return true; return true;
} catch (error) { } catch (error) {
console.error('刷新数据失败:', error); console.error('刷新数据失败:', error);

View File

@@ -1,7 +1,10 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import BattleRanking from '../views/BattleRanking.vue'; import BattleRanking from '../views/BattleRanking.vue'; // 首页组件
import AdminPanel from '../views/AdminPanel.vue'; import AdminPanel from '../views/AdminPanel.vue'; // 管理员面板组件
import { musicPlayer } from '@/utils/musicPlayer'; // 音乐播放器实例
import { getMusicConfig } from '@/services/configService'; // 音乐配置读取服务
// 路由配置
const routes = [ const routes = [
{ {
path: '/', path: '/',
@@ -16,23 +19,56 @@ const routes = [
meta: { title: '管理员面板' } meta: { title: '管理员面板' }
}, },
{ {
// 捕获所有未匹配的路由,重定向到首页 // 404路由未匹配路径重定向到首页
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
redirect: '/' redirect: '/'
} }
]; ];
// 创建路由实例
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes routes
}); });
// 全局前置守卫,设置页面标题 // 路由守卫:页面切换时控制音乐状态
router.beforeEach((to, from, next) => { router.beforeEach(async (to, from, next) => {
// 设置文档标题 // 1. 进入管理员页面:强制暂停+静音
if (to.path.startsWith('/admin')) {
musicPlayer.pause(); // 暂停音乐
musicPlayer.setMuted(true); // 强制静音(管理员页面始终无声音)
next();
return;
}
// 2. 进入首页:按配置播放/暂停
if (to.path === '/') {
try {
const musicConfig = await getMusicConfig(); // 读取音乐配置
if (musicConfig.enabled) {
// 初始化音乐路径+开关状态
musicPlayer.initMusicConfig(musicConfig.filePath, musicConfig.enabled);
musicPlayer.setMuted(false); // 首页取消静音
musicPlayer.play(); // 播放音乐
} else {
musicPlayer.pause(); // 开关关闭则暂停
}
} catch (error) {
console.error('首页音乐配置读取失败:', error);
musicPlayer.pause();
}
}
// 3. 进入其他页面:暂停音乐
if (to.path !== '/' && !to.path.startsWith('/admin')) {
musicPlayer.pause();
}
// 4. 设置页面标题(可选增强)
if (to.meta.title) { if (to.meta.title) {
document.title = to.meta.title; document.title = to.meta.title;
} }
next(); next();
}); });

View File

@@ -1,5 +1,9 @@
// 配置文件API路径 // 配置文件API路径
const CONFIG_API_URL = '/api/config'; // 修复后绝对路径直接请求后端3000端口
const CONFIG_API_URL = 'http://localhost:3000/api/config';
// 新增音乐相关API地址和后端接口对应
const MUSIC_API_URL = 'http://localhost:3000/api/musicConfig';
const MUSIC_UPLOAD_API_URL = 'http://localhost:3000/api/upload/music';
/** /**
* 读取配置文件 * 读取配置文件
@@ -102,25 +106,16 @@ const getDefaultConfig = () => ({
} }
} }
}, },
// ========== 音乐配置默认值和displayConfig同级 ==========
music: {
enabled: false,
filePath: '/assets/music/background.mp3'
},
// ==========================================================
battleEndTime: { battleEndTime: {
date: new Date().toISOString().split('T')[0], date: new Date().toISOString().split('T')[0],
time: '00:00:00' time: '00:00:00'
}, },
drumConfig: {
showDrum: false, // 控制战鼓的显示,默认不显示
sound: {
volume: 1.0,
enabled: false, // 控制声音播放,默认不播放
soundSrc: '' // 战鼓声音来源文件路径
},
animation: {
enabled: false
},
pattern: {
strongBeats: [1],
totalBeats: 4
}
},
backgroundConfig: { backgroundConfig: {
useBackgroundImage: true, useBackgroundImage: true,
backgroundImage: '/battle-background.jpg', // 默认战旗背景图片 backgroundImage: '/battle-background.jpg', // 默认战旗背景图片
@@ -332,26 +327,6 @@ export const saveBattleEndTime = async (endTime) => {
return await writeConfig(config); return await writeConfig(config);
}; };
/**
* 获取战鼓配置
* @returns {Object} 战鼓配置
*/
export const getDrumConfig = async () => {
const config = await readConfig();
return config.drumConfig || getDefaultConfig().drumConfig;
};
/**
* 保存战鼓配置
* @param {Object} drumConfig 战鼓配置
* @returns {boolean} 是否保存成功
*/
export const saveDrumConfig = async (drumConfig) => {
const config = await readConfig();
config.drumConfig = drumConfig;
return await writeConfig(config);
};
/** /**
* 获取背景配置 * 获取背景配置
* @returns {Object} 背景配置 * @returns {Object} 背景配置
@@ -370,4 +345,105 @@ export const saveBackgroundConfig = async (backgroundConfig) => {
const config = await readConfig(); const config = await readConfig();
config.backgroundConfig = backgroundConfig; config.backgroundConfig = backgroundConfig;
return await writeConfig(config); return await writeConfig(config);
};
/**
* 获取音乐配置(优先读后端接口,失败则兜底本地配置)
* @returns {Object} 音乐配置 { enabled, filePath }
*/
export const getMusicConfig = async () => {
try {
// 优先调用后端音乐配置接口
const response = await fetch(MUSIC_API_URL);
if (response.ok) {
const musicConfig = await response.json();
// 基础格式校验
if (typeof musicConfig.enabled !== 'boolean' || !musicConfig.filePath) {
throw new Error('后端音乐配置返回格式异常');
}
return musicConfig;
}
throw new Error(`获取音乐配置失败: ${response.status}`);
} catch (error) {
console.error('读取音乐配置接口失败,兜底读取本地配置:', error);
// 兜底逻辑读取本地config中的music字段
const localConfig = await readConfig();
return localConfig.music || { enabled: false, filePath: '/assets/music/background.mp3' };
}
};
/**
* 上传音乐文件(带返回值格式校验)
* @param {File} file 音乐文件MP3
* @returns {Object} { success: boolean, filePath?: string, error?: string }
*/
export const uploadMusicFile = async (file) => {
try {
// 前置校验:文件类型
if (!file.type.includes('audio/mpeg') && !file.name.endsWith('.mp3')) {
throw new Error('仅支持MP3格式的音乐文件');
}
// 构建FormData适配后端multer.single('musicFile')
const formData = new FormData();
formData.append('musicFile', file);
const response = await fetch(MUSIC_UPLOAD_API_URL, {
method: 'POST',
body: formData // FormData格式无需设置Content-Type浏览器自动处理
});
if (response.ok) {
const result = await response.json();
// 严格校验后端返回格式
if (typeof result.success !== 'boolean') {
throw new Error('音乐上传接口返回格式异常缺失success字段');
}
if (result.success && !result.filePath) {
throw new Error('音乐上传成功但未返回文件路径');
}
return result;
}
throw new Error(`音乐上传失败: ${response.status}`);
} catch (error) {
console.error('上传音乐文件失败:', error);
return { success: false, error: error.message };
}
};
/**
* 更新音乐配置(同步更新后端+本地配置)
* @param {boolean} enabled 是否开启播放
* @param {string} filePath 音乐文件路径
* @returns {boolean} 是否更新成功
*/
export const updateMusicConfig = async (enabled, filePath) => {
try {
// 1. 调用后端接口更新音乐配置
const response = await fetch(MUSIC_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled, filePath })
});
if (response.ok) {
const result = await response.json();
if (result.success) {
// 2. 同步更新本地config中的music字段
const localConfig = await readConfig();
localConfig.music = { enabled, filePath };
await writeConfig(localConfig);
console.log('音乐配置已同步更新到本地');
}
return result.success;
}
throw new Error(`保存音乐配置失败: ${response.status}`);
} catch (error) {
console.error('更新音乐配置失败:', error);
return false;
}
}; };

137
src/utils/musicPlayer.js Normal file
View File

@@ -0,0 +1,137 @@
// src/utils/musicPlayer.js
class MusicPlayer {
constructor() {
this.audio = null;
this.isPlaying = false;
this.defaultPath = "/assets/music/background.mp3";
this.enabled = false;
}
/**
* 适配组件调用的 init 方法(核心:复用 initMusicConfig 逻辑)
* @param {string} path 音乐文件路径(组件中传入的 musicPath
*/
init(path) {
// 组件调用 init 时,复用已有的 initMusicConfig开关状态先传 this.enabled后续组件会通过配置更新
this.initMusicConfig(path, this.enabled);
}
/**
* 原有初始化音乐配置方法(保留,适配动态配置)
* @param {string} filePath 音乐路径
* @param {boolean} enabled 播放开关
*/
initMusicConfig(filePath, enabled) {
this.enabled = enabled;
let validPath = this.defaultPath;
if (filePath && filePath.endsWith('.mp3')) {
validPath = filePath;
} else if (filePath) {
console.warn(`音乐路径无效非MP3格式${filePath},使用兜底路径`);
}
if (this.audio) {
this.audio.pause();
this.audio = null;
}
this.audio = new Audio(validPath);
this.audio.loop = true;
}
/**
* 播放音乐(保留原有逻辑,适配开关)
*/
play() {
if (!this.enabled) {
console.log("首页播放开关未开启,跳过音乐播放");
return;
}
if (!this.audio) {
this.initMusicConfig(this.defaultPath, false);
console.warn("未初始化音乐配置,使用兜底路径且关闭播放开关");
return;
}
if (!this.isPlaying) {
this.audio.play()
.then(() => {
this.isPlaying = true;
console.log("音乐播放成功,当前路径:", this.getCurrentPath());
})
.catch(err => {
console.error("音乐播放失败(浏览器自动播放限制/路径错误):", err);
this.isPlaying = false;
});
}
}
/**
* 暂停音乐(保留原有逻辑)
*/
pause() {
if (this.audio && this.isPlaying) {
this.audio.pause();
this.isPlaying = false;
console.log("音乐已暂停");
}
}
/**
* 新增:设置静音/取消静音(适配管理员/首页场景)
* @param {boolean} muted 是否静音
*/
setMuted(muted) {
if (this.audio) {
this.audio.muted = muted;
console.log(muted ? "音乐已静音" : "音乐已取消静音");
}
}
/**
* 新增stop 方法(组件 onUnmounted 调用,暂停+重置进度)
*/
stop() {
if (this.audio) {
this.audio.pause();
this.audio.currentTime = 0; // 重置播放进度到开头
this.isPlaying = false;
console.log("音乐已停止(重置进度)");
}
}
/**
* 新增destroy 方法(组件 onUnmounted 调用,销毁实例释放内存)
*/
destroy() {
this.stop(); // 先停止播放
if (this.audio) {
this.audio = null; // 清空音频实例,释放内存
console.log("音乐实例已销毁,释放内存");
}
}
/**
* 获取当前音乐路径(保留原有逻辑)
* @returns {string} 相对路径
*/
getCurrentPath() {
if (!this.audio) return this.defaultPath;
return this.audio.src.split(window.location.origin)[1] || this.defaultPath;
}
/**
* 更新音乐路径(保留原有逻辑)
* @param {string} newPath 新路径
*/
updateMusicPath(newPath) {
if (!newPath || !newPath.endsWith('.mp3')) {
console.error("更新的音乐路径无效非MP3格式", newPath);
return;
}
this.initMusicConfig(newPath, this.enabled);
console.log("音乐路径已更新为:", newPath);
if (this.enabled && this.isPlaying) {
this.pause();
this.play();
}
}
}
// 导出全局唯一实例
export const musicPlayer = new MusicPlayer();

View File

@@ -62,6 +62,81 @@
</div> </div>
</div> </div>
<!-- 背景音乐配置 -->
<div v-if="currentTab === 'music'" class="music-config-content">
<h2 class="game-subtitle">🎵 背景音乐配置</h2>
<div class="config-section">
<h3 class="text-gold">🎶 背景音乐设置</h3>
<div class="logo-upload-section">
<!-- 第1行背景音乐上传复用Logo上传样式 -->
<div class="config-item">
<label class="checkbox-label">
<span>背景音乐文件</span>
<input type="file" accept=".mp3" @change="handleMusicFileChange" class="logo-input">
</label>
</div>
<!-- 上传按钮 + 状态提示对齐Logo上传控件 -->
<div class="upload-controls" style="margin: 10px 0;">
<button
@click="handleMusicUpload"
:disabled="!selectedMusicFile"
class="btn-clear"
style="margin-right: 10px; background: #667eea;"
>
🎶 上传并应用
</button>
<span v-if="uploadMsg" class="upload-hint" :style="uploadMsg.includes('失败') ? 'color: red;' : 'color: #667eea;'">
{{ uploadMsg }}
</span>
</div>
<!-- 新增已上传音乐列表 -->
<div class="music-list-section" style="margin: 20px 0;">
<h4 class="text-gold">🎵 已上传音乐列表</h4>
<div class="music-list" v-if="musicList.length > 0">
<div
class="music-item"
v-for="music in musicList"
:key="music.filePath"
:class="{ active: music.filePath === currentMusicPath }"
@click="switchToMusic(music.filePath)"
>
{{ music.filename }}
<span v-if="music.filePath === currentMusicPath" style="color: #667eea; margin-left: 8px;"> 当前使用</span>
</div>
</div>
<p v-else class="upload-hint">暂无已上传的音乐文件</p>
</div>
<!-- 当前音乐路径回显对齐Logo大小配置 -->
<div class="config-item" style="margin: 10px 0;">
<label class="checkbox-label">
<span class="text-gold">当前音乐路径</span>
<input type="text" v-model="currentMusicPath" readonly class="text-input" placeholder="未配置音乐文件">
</label>
</div>
<!-- 第2行首页播放开关对齐Logo配置的复选框样式 -->
<div class="config-item" style="margin: 20px 0;">
<label class="checkbox-label">
<input type="checkbox" v-model="musicEnabled" @change="handleMusicSwitchChange">
<span class="text-gold">开启首页背景音乐播放</span>
</label>
</div>
<!-- 提示文本复用Logo上传的hint样式 -->
<p class="upload-hint">
仅支持MP3格式音频文件建议文件大小不超过10MB上传后立即生效
</p>
<p class="upload-hint">
开关关闭时首页将停止播放背景音乐管理员页面始终静音
</p>
</div>
</div>
</div>
<!-- 显示配置管理 --> <!-- 显示配置管理 -->
<div v-if="currentTab === 'config'" class="config-content"> <div v-if="currentTab === 'config'" class="config-content">
<h2 class="game-subtitle"> 显示配置管理</h2> <h2 class="game-subtitle"> 显示配置管理</h2>
@@ -655,174 +730,9 @@
</div> </div>
</div> </div>
<!-- 战鼓配置 -->
<div v-if="currentTab === 'drum'" class="drum-config-content">
<h2 class="game-subtitle">🥁 战鼓配置管理</h2>
<!-- 音效配置 -->
<div class="config-section">
<h3 class="text-gold">🔊 音效配置</h3>
<div class="config-options">
<div class="config-item">
<label class="checkbox-label">
<input type="checkbox" v-model="localDrumConfig.sound.enabled">
<span>启用音效</span>
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>音量 (0-1)</span>
<input type="number" v-model.number="localDrumConfig.sound.volume" min="0" max="1" step="0.1"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>起音时间 (s)</span>
<input type="number" v-model.number="localDrumConfig.sound.attackTime" min="0.001" max="0.5"
step="0.01" class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>衰减时间 (s)</span>
<input type="number" v-model.number="localDrumConfig.sound.decayTime" min="0.05" max="1" step="0.05"
class="width-input">
</label>
</div>
<h4 style="margin-top: 15px; color: #666;">🎵 第一个音调</h4>
<div class="config-item">
<label class="checkbox-label">
<span>音调类型</span>
<select v-model="localDrumConfig.sound.type1" class="select-input">
<option value="sine">正弦波</option>
<option value="square">方波</option>
<option value="triangle">三角波</option>
<option value="sawtooth">锯齿波</option>
</select>
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>频率 (Hz)</span>
<input type="number" v-model.number="localDrumConfig.sound.frequency1" min="50" max="500"
class="width-input">
</label>
</div>
<h4 style="margin-top: 15px; color: #666;">🎵 第二个音调</h4>
<div class="config-item">
<label class="checkbox-label">
<span>音调类型</span>
<select v-model="localDrumConfig.sound.type2" class="select-input">
<option value="sine">正弦波</option>
<option value="square">方波</option>
<option value="triangle">三角波</option>
<option value="sawtooth">锯齿波</option>
</select>
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>频率 (Hz)</span>
<input type="number" v-model.number="localDrumConfig.sound.frequency2" min="50" max="500"
class="width-input">
</label>
</div>
</div>
</div>
<!-- 动画配置 -->
<div class="config-section">
<h3>🎬 动画配置</h3>
<div class="config-options">
<div class="config-item">
<label class="checkbox-label">
<input type="checkbox" v-model="localDrumConfig.animation.enabled">
<span>启用动画</span>
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>节拍间隔 (ms)</span>
<input type="number" v-model.number="localDrumConfig.animation.beatInterval" min="50" max="1000"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>跳动缩放比例</span>
<input type="number" v-model.number="localDrumConfig.animation.beatScale" min="1.0" max="2.0"
step="0.1" class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>跳动上下位移 (px)</span>
<input type="number" v-model.number="localDrumConfig.animation.beatTranslateY" min="-50" max="50"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>跳动旋转角度 (deg)</span>
<input type="number" v-model.number="localDrumConfig.animation.beatRotate" min="0" max="20"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>单次跳动持续时间 (ms)</span>
<input type="number" v-model.number="localDrumConfig.animation.beatDuration" min="50" max="500"
class="width-input">
</label>
</div>
</div>
</div>
<!-- 节拍模式配置 -->
<div class="config-section">
<h3>🎵 节拍模式配置</h3>
<div class="config-options">
<div class="config-item">
<label class="checkbox-label">
<span>每小节总拍数</span>
<input type="number" v-model.number="localDrumConfig.pattern.totalBeats" min="1" max="8"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>强拍位置 (1-4)</span>
<input type="text" v-model="localDrumConfig.pattern.strongBeatsStr" placeholder="如: 1,4"
class="text-input" @input="updateStrongBeats">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>强拍音量倍数</span>
<input type="number" v-model.number="localDrumConfig.pattern.accentMultiplier" min="1" max="3"
step="0.1" class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>强拍频率偏移 (%)</span>
<input type="number" v-model.number="localDrumConfig.pattern.accentFrequencyOffset" min="-50" max="50"
class="width-input">
</label>
</div>
<div class="config-item">
<label class="checkbox-label">
<span>强拍动画增强 (%)</span>
<input type="number" v-model.number="localDrumConfig.pattern.accentAnimation" min="0" max="100"
class="width-input">
</label>
</div>
</div>
</div>
</div>
</div> </div>
<!-- 保存按钮 --> <!-- 保存按钮 -->
<div class="save-section"> <div class="save-section">
<button @click="saveData" class="save-btn">💾 保存所有数据</button> <button @click="saveData" class="save-btn">💾 保存所有数据</button>
@@ -954,14 +864,19 @@ import {
saveDisplayConfig, saveDisplayConfig,
battleEndTime, battleEndTime,
saveBattleEndTime, saveBattleEndTime,
drumConfig,
saveDrumConfig,
refreshData, refreshData,
initializeData initializeData
} from '../data/mockData.js'; } from '../data/mockData.js';
// 新增音乐相关依赖引入
import { uploadMusicFile, updateMusicConfig, getMusicConfig } from '../services/configService.js';
import { musicPlayer } from '@/utils/musicPlayer';
const router = useRouter(); const router = useRouter();
// 新增音乐配置变量
const selectedMusicFile = ref(null); // 选中的MP3文件
const uploadMsg = ref(''); // 上传提示信息
const musicEnabled = ref(false); // 首页播放开关状态
const currentMusicPath = ref(''); // 当前音乐路径
const musicList = ref([]); // 新增:已上传音乐列表
// 返回首页 // 返回首页
const goToHome = () => { const goToHome = () => {
router.push('/'); router.push('/');
@@ -984,7 +899,7 @@ const tabs = [
{ key: 'config', label: '显示配置' }, { key: 'config', label: '显示配置' },
{ key: 'champion', label: '冠军Logo配置' }, { key: 'champion', label: '冠军Logo配置' },
{ key: 'endTime', label: '结束时间设置' }, { key: 'endTime', label: '结束时间设置' },
{ key: 'drum', label: '战鼓配置' } { key: 'music', label: '背景音乐设置' }
]; ];
// 冠军Logo配置 // 冠军Logo配置
@@ -994,40 +909,171 @@ const championLogos = ref({
teamChampionSize: 60, // 默认60px teamChampionSize: 60, // 默认60px
individualChampionSize: 60 // 默认60px individualChampionSize: 60 // 默认60px
}); });
// ========== 增强版:音乐相关核心方法 ==========
// 1. 选择音乐文件(校验格式+友好提示)
const handleMusicFileChange = (e) => {
const file = e.target.files[0];
if (file) {
// 严格校验MP3格式兼容不同浏览器的MIME类型
const isMp3 = file.type === 'audio/mpeg' || file.type === 'audio/mp3' || file.name.endsWith('.mp3');
if (!isMp3) {
uploadMsg.value = '❌ 仅支持MP3格式音频文件';
selectedMusicFile.value = null;
return;
}
// 校验文件大小10MB限制
if (file.size > 10 * 1024 * 1024) {
uploadMsg.value = '❌ 文件大小超过10MB请选择更小的文件';
selectedMusicFile.value = null;
return;
}
selectedMusicFile.value = file;
uploadMsg.value = `✅ 已选择文件:${file.name}`;
} else {
selectedMusicFile.value = null;
uploadMsg.value = '';
}
};
// 新增:获取已上传音乐列表(调用后端/api/music/list接口
const fetchMusicList = async () => {
try {
const res = await fetch('http://localhost:3000/api/music/list');
const data = await res.json();
if (data.success) {
musicList.value = data.data;
} else {
uploadMsg.value = `⚠️ 获取音乐列表失败:${data.error}`;
}
} catch (err) {
uploadMsg.value = `⚠️ 获取音乐列表异常:${err.message}`;
}
};
// 新增:点击列表项切换音乐
const switchToMusic = async (filePath) => {
try {
uploadMsg.value = '⏳ 正在切换音乐...';
// 调用后端接口更新config.json的音乐路径
await updateMusicConfig(musicEnabled.value, filePath);
// 回显新路径到页面
currentMusicPath.value = filePath;
// 实时切换播放器音乐(和原有上传逻辑保持一致)
musicPlayer.updateMusicPath(filePath);
// 若开关开启,立即播放新音乐
if (musicEnabled.value) {
musicPlayer.play();
}
uploadMsg.value = '✅ 音乐切换成功!';
} catch (err) {
uploadMsg.value = `❌ 切换音乐失败:${err.message}`;
}
};
// 修改原handleMusicUpload方法在上传成功后添加fetchMusicList()
const handleMusicUpload = async () => {
if (!selectedMusicFile.value) {
uploadMsg.value = '❌ 请先选择MP3文件';
return;
}
uploadMsg.value = '⏳ 正在上传音乐文件...';
try {
const formData = new FormData();
formData.append('musicFile', selectedMusicFile.value);
const response = await fetch('http://localhost:3000/upload-music', {
method: 'POST',
body: formData
});
const uploadResult = await response.json();
if (uploadResult.success) {
await updateMusicConfig(musicEnabled.value, uploadResult.filePath);
currentMusicPath.value = uploadResult.filePath;
musicPlayer.updateMusicPath(uploadResult.filePath);
if (musicEnabled.value) {
musicPlayer.play();
}
// 新增:上传成功后刷新列表
await fetchMusicList();
uploadMsg.value = '✅ 音乐上传成功!已自动应用到首页';
selectedMusicFile.value = null;
} else {
uploadMsg.value = `❌ 上传失败:${uploadResult.error}`;
}
} catch (err) {
uploadMsg.value = `❌ 上传异常:${err.message}`;
}
};
// 3. 切换播放开关更新config.json+实时控制)
const handleMusicSwitchChange = async () => {
try {
// 更新config.json的enabled状态
const updateResult = await updateMusicConfig(musicEnabled.value, currentMusicPath.value);
if (updateResult) {
uploadMsg.value = `✅ 开关已${musicEnabled.value ? '开启' : '关闭'}`;
// 实时控制播放器
if (musicEnabled.value) {
musicPlayer.play();
} else {
musicPlayer.pause();
}
} else {
// 失败时回滚开关状态
musicEnabled.value = !musicEnabled.value;
uploadMsg.value = '❌ 开关更新失败!';
}
} catch (err) {
musicEnabled.value = !musicEnabled.value;
uploadMsg.value = `❌ 开关更新异常:${err.message}`;
}
};
// 4. 初始化音乐配置(页面加载时回显状态)
const initMusicConfig = async () => {
try {
const config = await getMusicConfig();
musicEnabled.value = config.enabled ?? false; // 兼容默认值
currentMusicPath.value = config.filePath || '/assets/music/background.mp3'; // 默认路径
await fetchMusicList();// 新增:初始化时加载已上传列表
} catch (err) {
uploadMsg.value = `⚠️ 音乐配置初始化失败:${err.message}`;
// 初始化默认值
musicEnabled.value = false;
currentMusicPath.value = '/assets/music/background.mp3';
}
};
// ========== 音乐方法结束 ==========
// 组件挂载时初始化冠军Logo配置 // 组件挂载时初始化冠军Logo配置
onMounted(async () => { onMounted(async () => {
try { try {
await initializeData(); await initializeData();
// 重新加载本地数据副本 // 重新加载本地数据副本合并第二个onMounted的逻辑
localIndividualRankings.value = [...individualRankings]; localIndividualRankings.value = [...individualRankings];
localTeamRankings.value = [...teamRankings]; localTeamRankings.value = [...teamRankings];
localBonusRules.value = [...bonusRules]; localBonusRules.value = [...bonusRules];
localDisplayConfig.value = { ...displayConfig }; localDisplayConfig.value = { ...displayConfig };
localBattleEndTime.value = { ...battleEndTime };
// 确保皇冠位置配置存在 // 确保皇冠位置配置存在
if (!localDisplayConfig.value.crownPosition) { if (!localDisplayConfig.value.crownPosition) {
localDisplayConfig.value.crownPosition = { top: '-100px' }; localDisplayConfig.value.crownPosition = { top: '-100px' };
} else if (!localDisplayConfig.value.crownPosition.top) { } else if (!localDisplayConfig.value.crownPosition.top) {
localDisplayConfig.value.crownPosition.top = '-100px'; localDisplayConfig.value.crownPosition.top = '-100px';
} }
localBattleEndTime.value = { ...battleEndTime };
localDrumConfig.value = { ...drumConfig };
// 初始化冠军Logo配置 // 初始化冠军Logo配置
if (displayConfig.championLogos) { if (displayConfig.championLogos) {
championLogos.value = { ...displayConfig.championLogos }; championLogos.value = { ...displayConfig.championLogos };
} }
// 重新处理强拍位 // 新增:初始化音乐配
if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) { await initMusicConfig();
localDrumConfig.value.pattern.strongBeatsStr =
localDrumConfig.value.pattern.strongBeats.join(',') || '1,4';
}
} catch (error) { } catch (error) {
console.error('初始化数据失败:', error); console.error('初始化数据失败:', error);
} }
}); });
// 处理冠军Logo上传 // 处理冠军Logo上传
const handleChampionLogoUpload = async (event, type) => { const handleChampionLogoUpload = async (event, type) => {
const file = event.target.files[0]; const file = event.target.files[0];
@@ -1138,13 +1184,6 @@ const handleRefreshData = () => {
localBonusRules.value = [...bonusRules]; localBonusRules.value = [...bonusRules];
localDisplayConfig.value = { ...displayConfig }; localDisplayConfig.value = { ...displayConfig };
localBattleEndTime.value = { ...battleEndTime }; localBattleEndTime.value = { ...battleEndTime };
localDrumConfig.value = { ...drumConfig };
// 重新处理强拍位置
if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) {
localDrumConfig.value.pattern.strongBeatsStr =
localDrumConfig.value.pattern.strongBeats.join(',') || '1,4';
}
if (success) { if (success) {
alert('数据刷新成功!'); alert('数据刷新成功!');
@@ -1164,57 +1203,7 @@ const localTeamRankings = ref([...teamRankings]);
const localBonusRules = ref([...bonusRules]); const localBonusRules = ref([...bonusRules]);
const localDisplayConfig = ref({ ...displayConfig }); const localDisplayConfig = ref({ ...displayConfig });
const localBattleEndTime = ref({ ...battleEndTime }); const localBattleEndTime = ref({ ...battleEndTime });
// 初始化本地战鼓配置副本
const localDrumConfig = ref({ ...drumConfig });
// 添加强拍位置的字符串表示,用于输入框
if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) {
localDrumConfig.value.pattern.strongBeatsStr = localDrumConfig.value.pattern.strongBeats.join(',') || '1,4';
} else {
localDrumConfig.value.pattern = localDrumConfig.value.pattern || {};
localDrumConfig.value.pattern.strongBeats = [1, 4];
localDrumConfig.value.pattern.strongBeatsStr = '1,4';
}
// 组件挂载时初始化数据
onMounted(async () => {
try {
await initializeData();
// 重新加载本地数据副本
localIndividualRankings.value = [...individualRankings];
localTeamRankings.value = [...teamRankings];
localBonusRules.value = [...bonusRules];
localDisplayConfig.value = { ...displayConfig };
localBattleEndTime.value = { ...battleEndTime };
localDrumConfig.value = { ...drumConfig };
// 重新处理强拍位置
if (localDrumConfig.value.pattern && localDrumConfig.value.pattern.strongBeats) {
localDrumConfig.value.pattern.strongBeatsStr =
localDrumConfig.value.pattern.strongBeats.join(',') || '1,4';
}
} catch (error) {
console.error('初始化数据失败:', error);
}
});
// 更新强拍位置数组
const updateStrongBeats = () => {
try {
const beatsStr = localDrumConfig.value.pattern.strongBeatsStr;
if (!beatsStr) {
localDrumConfig.value.pattern.strongBeats = [];
return;
}
const beats = beatsStr.split(',')
.map(beat => parseInt(beat.trim()))
.filter(beat => !isNaN(beat) && beat > 0 && beat <= 8);
localDrumConfig.value.pattern.strongBeats = beats;
} catch (error) {
console.error('更新强拍位置失败:', error);
localDrumConfig.value.pattern.strongBeats = [1, 4];
localDrumConfig.value.pattern.strongBeatsStr = '1,4';
}
};
// 对话框状态 // 对话框状态
const showAddIndividual = ref(false); const showAddIndividual = ref(false);
@@ -1326,12 +1315,6 @@ const saveData = async () => {
localIndividualRankings.value.sort((a, b) => b.score - a.score); localIndividualRankings.value.sort((a, b) => b.score - a.score);
localTeamRankings.value.sort((a, b) => b.totalScore - a.totalScore); localTeamRankings.value.sort((a, b) => b.totalScore - a.totalScore);
// 保存战鼓配置前,确保强拍位置数组是最新的
updateStrongBeats();
// 移除临时的字符串表示,避免保存到配置中
const configToSave = { ...localDrumConfig.value };
delete configToSave.pattern.strongBeatsStr;
// 导入必要的配置服务函数 // 导入必要的配置服务函数
const { readConfig, writeConfig } = await import('../services/configService'); const { readConfig, writeConfig } = await import('../services/configService');
@@ -1346,7 +1329,6 @@ const saveData = async () => {
// 保存冠军Logo配置 // 保存冠军Logo配置
currentConfig.displayConfig.championLogos = championLogos.value; currentConfig.displayConfig.championLogos = championLogos.value;
currentConfig.battleEndTime = localBattleEndTime.value; currentConfig.battleEndTime = localBattleEndTime.value;
currentConfig.drumConfig = configToSave;
// 一次性保存所有配置 // 一次性保存所有配置
const result = await writeConfig(currentConfig); const result = await writeConfig(currentConfig);
@@ -2204,4 +2186,93 @@ const deleteBonusRule = (index) => {
margin: 20px; margin: 20px;
} }
} }
/* 响应式设计 */
@media (max-width: 768px) {
.top-nav {
flex-direction: column;
gap: 15px;
}
.table-header,
.table-row {
font-size: 0.8rem;
}
.action-col {
flex-direction: column;
}
.modal {
min-width: auto;
margin: 20px;
}
}
// ========== 新增:音乐配置样式适配 ==========
.text-input[readonly] {
background-color: #f8f9fa;
color: #667eea;
cursor: not-allowed;
}
.btn-clear[disabled] {
background-color: #ccc !important;
cursor: not-allowed;
}
.logo-input[type="file"] {
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
// ========== 音乐样式结束 ==========
/* 背景音乐配置页面整体样式(补充) */
.music-config-content {
padding: 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 15px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
/* 音乐配置标题样式(补充) */
.music-config-content .game-subtitle {
color: #667eea;
margin-bottom: 25px;
font-size: 1.5rem;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
/* 新增:已上传音乐列表样式 */
.music-list-section {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.music-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
}
.music-item {
padding: 10px 15px;
background: white;
border-radius: 4px;
border: 1px solid #eee;
cursor: pointer;
transition: all 0.2s;
}
.music-item:hover {
background: #f0f7ff;
border-color: #667eea;
}
.music-item.active {
background: #e6f0ff;
border-color: #667eea;
font-weight: 500;
}
</style> </style>

View File

@@ -7,20 +7,7 @@
</div> </div>
</section> </section>
<!-- 第二部分战鼓动画浮动并支持拖放 -->
<section v-if="localDisplayConfig.showDrum" class="drums-section card-game" @mousedown="startDrag"
@click="handleDrumClick" :style="{ left: drumsPosition.x + 'px', top: drumsPosition.y + 'px' }">
<div class="drums-container">
<!-- 战鼓动画在上面 -->
<div class="drums-animation">
<div class="drum glow-border" :class="{ beating: isBeating }">🥁</div>
<div class="drum" :class="{ beating: isBeating }">🥁</div>
<div class="trophy" style="font-size: 2.5rem; filter: drop-shadow(0 0 10px var(--gold-primary));">🏆</div>
<div class="drum" :class="{ beating: isBeating }">🥁</div>
<div class="drum" :class="{ beating: isBeating }">🥁</div>
</div>
</div>
</section>
<!-- 任务设置模块 --> <!-- 任务设置模块 -->
<section class="task-settings-section card-game"> <section class="task-settings-section card-game">
@@ -216,6 +203,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed, reactive, proxyRefs } from 'vue'; import { ref, onBeforeMount, onMounted, onUnmounted, watch, computed, reactive, proxyRefs } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { import {
@@ -224,10 +212,13 @@ import {
bonusRules, bonusRules,
displayConfig, displayConfig,
battleEndTime, battleEndTime,
drumConfig,
initializeData initializeData
} from '../data/mockData.js'; } from '../data/mockData.js';
import { readConfig } from '../services/configService.js'; import { readConfig, getMusicConfig } from '../services/configService.js';
import { musicPlayer } from '@/utils/musicPlayer';
// 新增:选项卡激活状态(控制音乐面板显示/隐藏)
const activeTab = ref('');
// 创建默认显示配置的函数 // 创建默认显示配置的函数
function createDefaultDisplayConfig() { function createDefaultDisplayConfig() {
@@ -692,17 +683,7 @@ const hours = ref(0);
const minutes = ref(0); const minutes = ref(0);
const seconds = ref(0); const seconds = ref(0);
// 战鼓动画状态
const isBeating = ref(false);
let beatInterval = null;
let countdownInterval = null; let countdownInterval = null;
// 音频上下文和战鼓音效
let audioContext = null;
const isPlayingSound = ref(false);
// 战鼓位置状态
const drumsPosition = ref({ x: 20, y: 20 });
// 倒计时位置状态已移除,直接在模板中使用固定位置 // 倒计时位置状态已移除,直接在模板中使用固定位置
// 奖金设置模块位置状态 - 使用reactive存储实际定位值 // 奖金设置模块位置状态 - 使用reactive存储实际定位值
const bonusPosition = reactive({ x: 'auto', y: 'auto' }); const bonusPosition = reactive({ x: 'auto', y: 'auto' });
@@ -726,13 +707,7 @@ function throttle(func, limit) {
}; };
} }
// 开始拖动战鼓
const startDrag = (e) => {
isDragging = true;
dragOffset.x = e.clientX - drumsPosition.value.x;
dragOffset.y = e.clientY - drumsPosition.value.y;
e.preventDefault();
};
// 开始拖动奖金模块(鼠标事件) // 开始拖动奖金模块(鼠标事件)
const startBonusDrag = (e) => { const startBonusDrag = (e) => {
@@ -792,10 +767,6 @@ const endTouch = (e) => {
// 优化的拖动函数 - 使用节流减少更新频率(鼠标事件) // 优化的拖动函数 - 使用节流减少更新频率(鼠标事件)
const drag = throttle((e) => { const drag = throttle((e) => {
if (isDragging) {
drumsPosition.value.x = e.clientX - dragOffset.x;
drumsPosition.value.y = e.clientY - dragOffset.y;
}
if (isBonusDragging) { if (isBonusDragging) {
// 计算新的位置 // 计算新的位置
const newX = e.clientX - bonusDragOffset.x; const newX = e.clientX - bonusDragOffset.x;
@@ -881,237 +852,11 @@ const calculateCountdown = () => {
} }
}; };
// 初始化音频上下文
const initAudioContext = () => {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
};
// 音频缓存用于存储加载的MP3文件
const audioBufferCache = ref({});
// 加载MP3文件到音频缓冲区
const loadAudioFile = async (filePath) => {
try {
// 检查是否已缓存
if (audioBufferCache.value[filePath]) {
return audioBufferCache.value[filePath];
}
// 加载音频文件
const response = await fetch(filePath);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// 缓存音频缓冲区
audioBufferCache.value[filePath] = audioBuffer;
return audioBuffer;
} catch (error) {
console.error('加载音频文件失败:', error);
return null;
}
};
// 播放战鼓音效
const playDrumSound = async (isStrongBeat = false) => {
// 检查是否启用声音播放
if (!audioContext || isPlayingSound.value || drumConfig?.sound?.enabled === false) return;
isPlayingSound.value = true;
try {
// 使用配置的音效参数
const soundConfig = drumConfig?.sound || {};
const patternConfig = drumConfig?.pattern || {};
// 检查是否配置了MP3文件路径
if (soundConfig.soundSrc && soundConfig.soundSrc.trim() !== '') {
// 使用MP3文件播放
const audioBuffer = await loadAudioFile(soundConfig.soundSrc);
if (audioBuffer) {
// 创建音频源节点
const source = audioContext.createBufferSource();
const gainNode = audioContext.createGain();
// 连接节点
source.buffer = audioBuffer;
source.connect(gainNode);
gainNode.connect(audioContext.destination);
// 设置音量,支持强拍音量增强
const baseVolume = soundConfig.volume || 1.0;
const accentMultiplier = patternConfig.accentMultiplier || 1.2;
const volume = isStrongBeat ? baseVolume * accentMultiplier : baseVolume;
gainNode.gain.value = volume;
// 播放声音
source.start(0);
// 设置完成后重置播放状态
setTimeout(() => {
isPlayingSound.value = false;
}, audioBuffer.duration * 1000 + 100);
return;
}
}
// 如果没有配置MP3文件或加载失败回退到合成音效
// 创建振荡器节点
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
// 连接节点
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// 设置战鼓音效参数,支持强拍
const baseVolume = soundConfig.volume || 1.0;
// 使用pattern配置中的强拍音量倍数
const accentMultiplier = patternConfig.accentMultiplier || 1.2;
const volume = isStrongBeat ? baseVolume * accentMultiplier : baseVolume;
// 使用soundConfig中的type1
oscillator.type = soundConfig.type1 || 'sine';
// 基础频率,支持强拍频率偏移
const baseFrequency = soundConfig.frequency1 || 150;
const frequencyOffset = isStrongBeat ? (patternConfig.accentFrequencyOffset || 0) / 100 : 0;
const actualFrequency = baseFrequency * (1 + frequencyOffset);
oscillator.frequency.setValueAtTime(actualFrequency, audioContext.currentTime);
// 频率渐变
oscillator.frequency.exponentialRampToValueAtTime(actualFrequency * 0.5, audioContext.currentTime + 0.1);
// 设置音量包络
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + (soundConfig.attackTime || 0.01));
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + (soundConfig.decayTime || 0.3));
// 播放声音
oscillator.start();
oscillator.stop(audioContext.currentTime + (soundConfig.decayTime || 0.3));
// 双音调效果 - 始终使用
const oscillator2 = audioContext.createOscillator();
const gainNode2 = audioContext.createGain();
oscillator2.connect(gainNode2);
gainNode2.connect(audioContext.destination);
oscillator2.type = soundConfig.type2 || 'triangle';
oscillator2.frequency.setValueAtTime(soundConfig.frequency2 || 100, audioContext.currentTime);
gainNode2.gain.setValueAtTime(0, audioContext.currentTime);
gainNode2.gain.linearRampToValueAtTime(volume * 0.8, audioContext.currentTime + (soundConfig.attackTime || 0.01));
gainNode2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + (soundConfig.decayTime || 0.3) + 0.2);
oscillator2.start();
oscillator2.stop(audioContext.currentTime + (soundConfig.decayTime || 0.3) + 0.2);
// 设置完成后重置播放状态
setTimeout(() => {
isPlayingSound.value = false;
}, (soundConfig.decayTime || 0.3) * 1000 + 150);
} catch (error) {
console.error('播放战鼓音效出错:', error);
isPlayingSound.value = false;
}
};
// 修改相关函数以支持异步
const handleDrumClick = async () => {
// 如果音频上下文未初始化,初始化它
if (!audioContext) {
initAudioContext();
}
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
audioContext.resume();
}
// 触发战鼓动画和音效,使用配置的点击效果
const animationConfig = drumConfig?.animation || {};
isBeating.value = true;
await playDrumSound(true); // 点击总是强拍
setTimeout(() => {
isBeating.value = false;
}, animationConfig.clickBeatDuration || 250);
};
// 战鼓动画效果
const startDrumAnimation = () => {
// 检查是否显示战鼓
if (drumConfig?.showDrum === false) return;
// 使用配置的动画和节拍参数
const animationConfig = drumConfig?.animation || {};
const patternConfig = drumConfig?.pattern || {};
// 检查是否启用动画
if (animationConfig.enabled === false) return;
let beatCount = 0;
// 使用配置的节拍间隔
const interval = animationConfig.beatInterval || 200;
beatInterval = setInterval(() => {
beatCount++;
// 使用配置的节拍模式和总拍数
const totalBeats = patternConfig.totalBeats || 4;
const currentBeat = ((beatCount - 1) % totalBeats) + 1;
// 根据节拍模式确定是否是强拍
const strongBeats = patternConfig.strongBeats || [1, 4];
const isStrongBeat = strongBeats.includes(currentBeat);
// 执行动画和音效
isBeating.value = true;
// 根据是否是强拍播放音效
playDrumSound(isStrongBeat);
// 设置CSS变量支持强拍动画增强
const drums = document.querySelectorAll('.drum');
drums.forEach(drum => {
// 使用配置的动画参数
drum.style.setProperty('--drum-scale', isStrongBeat ?
(animationConfig.beatScale || 1.3) * (1 + (patternConfig.accentAnimation || 0) / 100) :
(animationConfig.beatScale || 1.3));
drum.style.setProperty('--drum-translate-y', isStrongBeat ?
`${(animationConfig.beatTranslateY || -15) * (1 + (patternConfig.accentAnimation || 0) / 100)}px` :
`${animationConfig.beatTranslateY || -15}px`);
drum.style.setProperty('--drum-rotate', `${animationConfig.beatRotate || 5}deg`);
drum.style.setProperty('--drum-brightness', isStrongBeat ? '1.4' : '1.3');
drum.style.setProperty('--drum-saturation', isStrongBeat ? '1.3' : '1.2');
});
// 根据节拍类型设置持续时间
const beatDuration = isStrongBeat
? (animationConfig.beatDuration || 150)
: (animationConfig.beatDuration || 100);
setTimeout(() => {
isBeating.value = false;
}, beatDuration);
}, interval);
};
// 已移至文件中异步版本的handleDrumClick函数
// 跳转到管理员页面 // 跳转到管理员页面
const goToAdmin = () => { const goToAdmin = () => {
router.push('/admin'); router.push('/admin');
}; };
// 监听窗口点击事件,用于用户交互后初始化音频上下文
document.addEventListener('click', initAudioContext, { once: true });
document.addEventListener('touchstart', initAudioContext, { once: true });
const handleResize = () => { const handleResize = () => {
// 计算并设置排名明细区域的最小高度,使其底部与视口对齐 // 计算并设置排名明细区域的最小高度,使其底部与视口对齐
const rankingsSection = document.querySelector('.rankings-section'); const rankingsSection = document.querySelector('.rankings-section');
@@ -1129,7 +874,13 @@ onMounted(async () => {
try { try {
// 异步初始化数据 // 异步初始化数据
await initializeData(); await initializeData();
// 新增:加入音乐控制逻辑
const musicConfig = await getMusicConfig();
if (musicConfig.enabled) {
musicPlayer.play();
} else {
musicPlayer.pause();
}
// 更新本地显示配置确保columnAlignments属性存在 // 更新本地显示配置确保columnAlignments属性存在
if (displayConfig) { if (displayConfig) {
const configCopy = JSON.parse(JSON.stringify(displayConfig)); const configCopy = JSON.parse(JSON.stringify(displayConfig));
@@ -1156,7 +907,6 @@ onMounted(async () => {
calculateCountdown(); calculateCountdown();
countdownInterval = setInterval(calculateCountdown, 10); // 改为10ms更新一次以显示毫秒 countdownInterval = setInterval(calculateCountdown, 10); // 改为10ms更新一次以显示毫秒
startDrumAnimation();
// 监听窗口大小变化,确保排名明细与底部对齐 // 监听窗口大小变化,确保排名明细与底部对齐
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
handleResize(); // 初始调整 handleResize(); // 初始调整
@@ -1186,24 +936,30 @@ const handleDisplayConfigChange = () => {
// 在实际项目中可能需要通过WebSocket或轮询来更新配置 // 在实际项目中可能需要通过WebSocket或轮询来更新配置
onUnmounted(() => { onUnmounted(() => {
// 1. 强制暂停+静音(双重保险)
musicPlayer.pause();
musicPlayer.setMuted(true);
// 2. 销毁实例(防止内存泄漏)
if (isMusicInitiated.value) {
musicPlayer.stop();
musicPlayer.destroy();
isMusicInitiated.value = false; // 重置初始化状态
}
// 3. 重置响应式变量
isMusicEnabled.value = false;
musicPath.value = '';
document.removeEventListener('click', unlockMusicPlay);
document.removeEventListener('touchstart', unlockMusicPlay);
if (countdownInterval) clearInterval(countdownInterval); if (countdownInterval) clearInterval(countdownInterval);
if (beatInterval) clearInterval(beatInterval);
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
// 移除拖放相关的事件监听
document.removeEventListener('mousemove', drag); document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', endDrag); document.removeEventListener('mouseup', endDrag);
// 移除触摸事件监听
document.removeEventListener('touchmove', touchMove); document.removeEventListener('touchmove', touchMove);
document.removeEventListener('touchend', endTouch); document.removeEventListener('touchend', endTouch);
document.removeEventListener('touchcancel', endTouch); document.removeEventListener('touchcancel', endTouch);
// 清理音频资源
if (audioContext) {
audioContext.close();
audioContext = null;
}
}); });
</script> </script>
@@ -1725,97 +1481,7 @@ onUnmounted(() => {
} }
} }
/* 战鼓部分 - 浮动并支持拖放 */
.drums-section {
position: fixed;
left: 20px;
top: 20px;
padding: 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
cursor: move;
z-index: 1000;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transition: box-shadow 0.3s ease;
}
.drums-section:hover {
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.25);
}
.drums-section:active {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.drums-container {
display: flex;
justify-content: center;
}
.drums-animation {
display: flex;
align-items: center;
gap: 20px;
font-size: 3rem;
}
.drum {
transition: transform 0.1s ease, filter 0.1s ease;
animation: idlePulse 2s infinite alternate;
}
.drum.beating {
/* 使用CSS变量方便动态调整 */
--drum-scale: 1.3;
--drum-translate-y: -15px;
--drum-rotate: 5deg;
--drum-brightness: 1.3;
--drum-saturation: 1.2;
transform: scale(var(--drum-scale)) translateY(var(--drum-translate-y)) rotate(var(--drum-rotate));
filter: brightness(var(--drum-brightness)) saturate(var(--drum-saturation));
animation: drumBeat 0.1s ease-in-out;
}
/* 战鼓闲置时的轻微脉动动画 */
@keyframes idlePulse {
0% {
transform: scale(1);
}
100% {
transform: scale(1.05);
}
}
/* 增强跳动效果的关键帧动画 */
@keyframes drumBeat {
0% {
transform: scale(1);
}
50% {
transform: scale(var(--drum-scale, 1.3)) translateY(var(--drum-translate-y, -15px)) rotate(var(--drum-rotate, 5deg));
}
100% {
transform: scale(1);
}
}
.trophy {
animation: bounce 1s infinite alternate;
}
@keyframes bounce {
from {
transform: translateY(0);
}
to {
transform: translateY(-10px);
}
}
/* 按钮样式 */ /* 按钮样式 */
.btn-game-secondary { .btn-game-secondary {
@@ -2089,6 +1755,7 @@ onUnmounted(() => {
gap: 15px; gap: 15px;
} }
/* 移除空的total-score-content规则集以避免SCSS警告 */
.total-score-item { .total-score-item {
@@ -2319,11 +1986,7 @@ onUnmounted(() => {
height: auto; height: auto;
} }
/* 战鼓部分调整 */
.drums-section {
transform: scale(0.8);
/* 缩小战鼓元素 */
}
/* 2. 倒计时模块调整 - 移至冠军战区上方,缩小时间显示为一行 */ /* 2. 倒计时模块调整 - 移至冠军战区上方,缩小时间显示为一行 */
.timer-float { .timer-float {

View File

@@ -1,7 +1,12 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import path from 'path' // 新增引入path模块
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
resolve: { // 新增:配置路径别名
alias: {
'@': path.resolve(__dirname, 'src') // 让@代表项目根目录下的src文件夹
}
}
}) })