feat: 添加歌曲智能排序功能(默认启用)

- 实现智能排序:数字 -> 字母 -> 其他符号
- 添加 smartSort() 方法支持自然数值排序
- 默认启用排序功能,用户无需设置
- 支持多语言字符(中文、日文、英文等)
- 添加完整的测试工具和文档

新增文件:
- test_sort.html (可视化测试页面)
- verify_sort.py (Python验证脚本)
- verify_sort.js (Node.js验证脚本)
- SORT_FEATURE.md (功能说明)
- SORT_USAGE.md (使用指南)
- QUICKSTART_SORT.md (快速开始)
- IMPLEMENTATION_SUMMARY.md (实现总结)
- CHANGELOG_SORT.md (更新日志)
- UPDATE_SUMMARY.md (更新说明)

修改文件:
- public/src/js/songselect.js (添加智能排序逻辑)
- README.md (更新功能介绍)
This commit is contained in:
2025-11-15 15:59:08 +08:00
parent 4ba37435da
commit 25c26b2b2e
11 changed files with 1989 additions and 8 deletions

View File

@@ -141,10 +141,11 @@ class SongSelect{
return a.id > b.id ? 1 : -1
}
})
const titlesort = localStorage.getItem("titlesort") ?? "false";
if (titlesort === "true") {
this.songs.sort((a, b) => a.title.localeCompare(b.title));
}
// 默认启用智能排序,除非用户明确禁用
const titlesort = localStorage.getItem("titlesort") ?? "true";
if (titlesort === "true") {
this.songs.sort((a, b) => this.smartSort(a.title, b.title));
}
if(assets.songs.length){
this.songs.push({
title: strings.back,
@@ -598,6 +599,50 @@ class SongSelect{
}
}
smartSort(titleA, titleB){
// 智能排序函数:按数字、字母、其他符号的顺序排序
// 返回值:-1 表示 titleA 在前1 表示 titleB 在前0 表示相等
// 辅助函数:判断字符类型
const getCharType = (char) => {
if (!char) return 3; // 空字符串排在最后
const code = char.charCodeAt(0);
// 数字 (0-9)
if (code >= 48 && code <= 57) return 0;
// 英文字母 (A-Z, a-z)
if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122)) return 1;
// 其他所有字符(符号、中文、日文等)
return 2;
};
// 辅助函数:提取首字符并确定类型
const getFirstCharInfo = (title) => {
if (!title || title.length === 0) return { type: 3, char: '', title: '' };
const firstChar = title.charAt(0);
const type = getCharType(firstChar);
return { type, char: firstChar, title };
};
const infoA = getFirstCharInfo(titleA);
const infoB = getFirstCharInfo(titleB);
// 首先按字符类型排序:数字 < 字母 < 其他符号
if (infoA.type !== infoB.type) {
return infoA.type - infoB.type;
}
// 同类型字符,使用 localeCompare 进行自然排序
// 这样可以正确处理数字序列(如 1, 2, 10 而不是 1, 10, 2
// 也能处理各种语言的字符
return titleA.localeCompare(titleB, undefined, {
numeric: true, // 数字按数值排序
sensitivity: 'base' // 忽略大小写和重音符号
});
}
mouseDown(event){
if(event.target === this.selectable || event.target.parentNode === this.selectable){
this.selectable.focus()
@@ -2383,7 +2428,8 @@ class SongSelect{
y: frameTop + 640,
w: 273,
h: 66,
id: "1p" + name + "\n" + rank,
id: "1p" + name + "
" + rank,
}, ctx => {
this.draw.nameplate({
ctx: ctx,
@@ -3166,7 +3212,8 @@ class SongSelect{
chartParsed = true
if(song.type === "tja"){
promise = readFile(blob, false, "utf-8").then(dataRaw => {
var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : []
var data = dataRaw ? dataRaw.replace(/\0/g, "").split("
") : []
var tja = new ParseTja(data, "oni", 0, 0, true)
for(var diff in tja.metadata){
var meta = tja.metadata[diff]
@@ -3177,7 +3224,8 @@ class SongSelect{
})
}else if(song.type === "osu"){
promise = readFile(blob).then(dataRaw => {
var data = dataRaw ? dataRaw.replace(/\0/g, "").split("\n") : []
var data = dataRaw ? dataRaw.replace(/\0/g, "").split("
") : []
var osu = new ParseOsu(data, "oni", 0, 0, true)
if(osu.generalInfo.AudioFilename){
musicFilename = osu.generalInfo.AudioFilename
@@ -3257,7 +3305,11 @@ class SongSelect{
toDelete() {
// ここに削除処理を書く
if (!confirm("本当に削除しますか?\nこの曲に問題がある場合や\n公序良俗に反する場合にのみ実行したほうがいいと思います\n本当に曲が削除されます\n成功しても反映まで1分ほどかかる場合があります")) {
if (!confirm("本当に削除しますか?
この曲に問題がある場合や
公序良俗に反する場合にのみ実行したほうがいいと思います
本当に曲が削除されます
成功しても反映まで1分ほどかかる場合があります")) {
return;
}
fetch("/api/delete", {