【AI创作】Phigros歌曲数据记录工具
作者:小编日期:2026-11-08浏览:12分类:网站源码
这个工具可以用树状图记录歌曲的各项数据(分数、评级、Acc、定数),用起来觉得不错(要手动录入曲目)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Phigros 歌曲记录器</title>
<style>
* { box-sizing: border-box; }
/* ===== 主题变量 ===== */
:root, [data-theme="light"] {
--bg: #ffffff;
--bg-card: #ffffff;
--bg-sub: #f7f7fb;
--bg-hover: #f0f0f6;
--bg-input: #ffffff;
--bg-input-dis: #f4f4f8;
--bg-modal: #ffffff;
--bg-overlay: rgba(30,30,40,0.35);
--text: #222233;
--text-strong: #000000;
--text-dim: #777;
--text-faint: #bbb;
--border: #e4e4ec;
--border-strong: #ddd; --accent: #b8860b;
--accent-bg: #fdf8ec;
--danger: #d03a3a;
--danger-bg: #fde8e8;
--shadow: 0 12px 40px rgba(0,0,0,0.15);
--rank-phi-bg: #f0c040; --rank-phi-fg: #222;
--rank-fc-bg: #2f6fd0; --rank-fc-fg: #fff;
--rank-wv-bg: #fff; --rank-wv-fg: #222; --rank-wv-border: #ccc;
--rank-s-bg: #f0a030; --rank-s-fg: #222;
--rank-a-bg: #4caf50; --rank-a-fg: #fff;
--rank-b-bg: #4a90d9; --rank-b-fg: #fff;
--rank-c-bg: #c08060; --rank-c-fg: #fff;
--rank-f-bg: #ccc; --rank-f-fg: #777;
--rank-none-bg: #f0f0f5; --rank-none-fg: #bbb;
--lv-bg: #f0f0f5; --lv-fg: #444; --lv-border: #ddd; --lv-empty: #bbb;
--diff-ez: #2e9e4f;
--diff-hd: #2f6fd0;
--diff-in: #d03a3a;
--diff-at: #7a7a85;
}
[data-theme="dark"] {
--bg: #0f0f14;
--bg-card: #1a1a26;
--bg-sub: #16161f; --bg-hover: #22223a;
--bg-input: #0f0f14;
--bg-input-dis: #1e1e2a;
--bg-modal: #1a1a26;
--bg-overlay: rgba(0,0,0,0.65);
--text: #e8e8f0;
--text-strong: #ffffff;
--text-dim: #8888aa;
--text-faint: #555;
--border: #333344;
--border-strong: #44445a;
--accent: #ffd966;
--accent-bg: #22222e;
--danger: #ff6b6b;
--danger-bg: #3a1a1a;
--shadow: 0 12px 40px rgba(0,0,0,0.6);
--rank-phi-bg: #ffd966; --rank-phi-fg: #000;
--rank-fc-bg: #4a6fd4; --rank-fc-fg: #fff;
--rank-wv-bg: #e8e8f0; --rank-wv-fg: #222; --rank-wv-border: #e8e8f0;
--rank-s-bg: #ffaa44; --rank-s-fg: #000;
--rank-a-bg: #66cc88; --rank-a-fg: #000;
--rank-b-bg: #6699cc; --rank-b-fg: #000;
--rank-c-bg: #cc8866; --rank-c-fg: #000;
--rank-f-bg: #444; --rank-f-fg: #aaa;
--rank-none-bg: #222; --rank-none-fg: #555; --lv-bg: #2a2a40; --lv-fg: #ffd966; --lv-border: #44445a; --lv-empty: #444;
--diff-ez: #2e9e4f;
--diff-hd: #2f6fd0;
--diff-in: #d03a3a;
--diff-at: #7a7a85;
}
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg); color: var(--text);
margin: 0 auto; padding: 20px; max-width: 1080px;
transition: background 0.2s, color 0.2s;
}
h1 { font-size: 1.3rem; margin: 0 0 4px; color: var(--accent); }
.sub { color: var(--text-dim); font-size: 0.8rem; margin-bottom: 18px; }
.toolbar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; align-items: center; }
.toolbar button {
background: var(--bg-sub); color: var(--text); border: 1px solid var(--border-strong);
border-radius: 8px; padding: 8px 14px; font-size: 0.85rem; cursor: pointer;
transition: background 0.15s;
}
.toolbar button:hover { background: var(--bg-hover); }
.theme-toggle {
margin-left: auto; display: flex; align-items: center; gap: 6px;
background: var(--bg-sub); color: var(--text); border: 1px solid var(--border-strong); border-radius: 999px; padding: 6px 14px; font-size: 0.82rem; cursor: pointer;
transition: background 0.15s;
}
.theme-toggle:hover { background: var(--bg-hover); }
.theme-toggle .icon { font-size: 1rem; }
/* 树状 */
.tree { user-select: none; }
.group { margin-bottom: 6px; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: var(--bg-card); }
.group-header {
display: flex; align-items: center; gap: 8px; padding: 10px 14px;
background: var(--bg-sub); cursor: pointer; font-weight: 700; font-size: 0.9rem; color: var(--text);
}
.group-header:hover { background: var(--bg-hover); }
.group-header .arrow { display: inline-block; transition: transform 0.2s; font-size: 0.7rem; color: var(--text-faint); }
.group-header.open .arrow { transform: rotate(90deg); }
.group-body { display: none; padding: 4px 0 8px 0; }
.group-body.open { display: block; }
.chapter { margin: 2px 0; }
.chapter-header {
display: flex; align-items: center; gap: 6px; padding: 7px 14px 7px 30px;
font-size: 0.82rem; color: var(--text-dim); cursor: pointer;
}
.chapter-header:hover { color: var(--text-strong); background: var(--bg-hover); } .chapter-header .arrow { display: inline-block; transition: transform 0.2s; font-size: 0.6rem; color: var(--text-faint); }
.chapter-header.open .arrow { transform: rotate(90deg); }
.chapter-body { display: none; }
.chapter-body.open { display: block; }
.song-item { margin: 1px 0; }
.song-row {
display: flex; align-items: center; gap: 10px; padding: 7px 8px 7px 48px;
font-size: 0.82rem; cursor: pointer; border-left: 3px solid transparent;
}
.song-row:hover { background: var(--bg-hover); }
.song-row.active { border-left-color: var(--accent); background: var(--accent-bg); }
.song-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-strong); font-weight: 500; }
.diff-btns { display: flex; gap: 6px; flex-shrink: 0; align-items: center; }
.diff-btn-wrap { display: flex; align-items: center; gap: 3px; }
.diff-btn {
min-width: 36px; height: 24px; border: none; border-radius: 4px;
font-size: 0.7rem; font-weight: 800; cursor: pointer;
transition: opacity 0.15s, transform 0.1s; padding: 0 5px; color: #fff;
}
.diff-btn:hover { transform: scale(1.06); }
.diff-btn.selected { outline: 2px solid var(--text-strong); outline-offset: 1px; }
.diff-btn.EZ { background: var(--diff-ez); }
.diff-btn.HD { background: var(--diff-hd); } .diff-btn.IN { background: var(--diff-in); }
.diff-btn.AT { background: var(--diff-at); }
.lv-tag {
display: inline-flex; align-items: center; justify-content: center;
height: 22px; min-width: 44px; padding: 0 7px;
border-radius: 6px; font-size: 0.72rem; font-weight: 800;
background: var(--lv-bg); color: var(--lv-fg); border: 1px solid var(--lv-border);
letter-spacing: 0.3px; white-space: nowrap;
}
.lv-tag.empty { background: transparent; color: var(--lv-empty); border: 1px dashed var(--lv-border); font-weight: 500; }
.song-del-btn {
flex-shrink: 0; width: 28px; height: 28px; border: none; border-radius: 6px;
background: transparent; color: var(--text-faint); font-size: 0.95rem; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, color 0.15s;
}
.song-del-btn:hover { background: var(--danger-bg); color: var(--danger); }
/* 遮罩 + 弹窗 */
.overlay {
position: fixed; inset: 0; background: var(--bg-overlay);
display: none; align-items: center; justify-content: center;
z-index: 100; padding: 20px; }
.overlay.open { display: flex; }
.modal {
background: var(--bg-modal); border: 1px solid var(--border); border-radius: 14px;
padding: 22px 26px; width: 100%; max-width: 500px;
box-shadow: var(--shadow); max-height: 90vh; overflow-y: auto;
color: var(--text);
}
.modal-title { font-size: 1rem; color: var(--accent); font-weight: 700; margin-bottom: 16px; }
.diff-switch { display: flex; gap: 8px; margin-bottom: 18px; }
.diff-switch button {
flex: 1; min-height: 48px; border: 2px solid var(--border); border-radius: 8px;
background: var(--bg-sub); color: var(--text-dim); cursor: pointer; transition: all 0.15s;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
padding: 4px 2px;
}
.diff-switch button:hover { border-color: var(--text-faint); }
.diff-switch button.active { background: var(--bg-card); }
.diff-switch button.active.EZ { border-color: var(--diff-ez); color: var(--diff-ez); }
.diff-switch button.active.HD { border-color: var(--diff-hd); color: var(--diff-hd); }
.diff-switch button.active.IN { border-color: var(--diff-in); color: var(--diff-in); }
.diff-switch button.active.AT { border-color: var(--diff-at); color: var(--diff-at); }
.diff-switch .sw-name { font-weight: 800; font-size: 0.85rem; }
.diff-switch .sw-lv { font-size: 0.72rem; font-weight: 700; color: var(--text-faint); } .diff-switch button.active.EZ .sw-lv { color: var(--diff-ez); }
.diff-switch button.active.HD .sw-lv { color: var(--diff-hd); }
.diff-switch button.active.IN .sw-lv { color: var(--diff-in); }
.diff-switch button.active.AT .sw-lv { color: var(--diff-at); }
.editor-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
.editor-row label { font-size: 0.82rem; color: var(--text-dim); min-width: 44px; }
.lv-input-wrap {
display: flex; align-items: center; gap: 0;
background: var(--bg-input); border: 1px solid var(--border-strong); border-radius: 8px;
overflow: hidden; height: 40px;
}
.lv-input-wrap:focus-within { border-color: var(--accent); }
.lv-input-prefix {
padding: 0 10px; height: 100%; display: flex; align-items: center;
background: var(--bg-sub); color: var(--accent); font-weight: 800; font-size: 0.9rem;
border-right: 1px solid var(--border);
}
.lv-input-wrap input {
width: 100px; height: 100%; background: transparent; border: none;
color: var(--text-strong); font-size: 1rem; font-weight: 700; padding: 0 12px; outline: none;
}
.editor-row input[type="number"] { width: 130px; height: 40px; background: var(--bg-input); border: 1px solid var(--border-strong);
border-radius: 8px; padding: 0 12px; color: var(--text-strong); font-size: 0.95rem;
}
.editor-row input[type="number"]:focus { outline: none; border-color: var(--accent); }
.editor-row input[type="number"]:disabled { opacity: 0.45; cursor: not-allowed; background: var(--bg-input-dis); }
.check-row { display: flex; gap: 20px; align-items: center; margin-bottom: 18px; }
.check-row label { display: flex; align-items: center; gap: 6px; font-size: 0.85rem; cursor: pointer; color: var(--text); }
.check-row input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; accent-color: var(--accent); }
.rank-display {
display: inline-block; min-width: 48px; text-align: center;
padding: 5px 12px; border-radius: 6px; font-weight: 800; font-size: 1.15rem;
}
.rank-phi { background: var(--rank-phi-bg); color: var(--rank-phi-fg); }
.rank-nu-fc { background: var(--rank-fc-bg); color: var(--rank-fc-fg); }
.rank-nu-white { background: var(--rank-wv-bg); color: var(--rank-wv-fg); border: 1.5px solid var(--rank-wv-border); }
.rank-s { background: var(--rank-s-bg); color: var(--rank-s-fg); }
.rank-a { background: var(--rank-a-bg); color: var(--rank-a-fg); }
.rank-b { background: var(--rank-b-bg); color: var(--rank-b-fg); }
.rank-c { background: var(--rank-c-bg); color: var(--rank-c-fg); }
.rank-false { background: var(--rank-f-bg); color: var(--rank-f-fg); }
.rank-none { background: var(--rank-none-bg); color: var(--rank-none-fg); }
.modal-btns { display: flex; gap: 10px; margin-top: 6px; flex-wrap: wrap; } .modal-btns button {
background: var(--bg-sub); color: var(--text); border: 1px solid var(--border-strong);
border-radius: 8px; padding: 9px 18px; cursor: pointer; font-size: 0.85rem;
}
.modal-btns button:hover { background: var(--bg-hover); }
.modal-btns button.danger { border-color: var(--danger); color: var(--danger); }
.modal-btns button.danger:hover { background: var(--danger-bg); }
.empty-song { padding: 8px 14px 8px 48px; font-size: 0.78rem; color: var(--text-faint); font-style: italic; }
.pick-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 16px; max-height: 50vh; overflow-y: auto; }
.pick-item {
padding: 9px 12px; border-radius: 8px; background: var(--bg-sub); border: 1px solid var(--border);
cursor: pointer; font-size: 0.85rem; color: var(--text);
}
.pick-item:hover { background: var(--bg-hover); color: var(--text-strong); }
.pick-item small { color: var(--text-faint); margin-left: 6px; }
.name-input {
width: 100%; background: var(--bg-input); border: 1px solid var(--border-strong); border-radius: 8px;
padding: 11px 14px; color: var(--text-strong); font-size: 0.95rem; margin-bottom: 16px;
}
.name-input:focus { outline: none; border-color: var(--accent); }
</style>
</head>
<body>
<h1>🎵 Phigros 歌曲记录器</h1>
<div class="sub">每首歌曲默认有EZ/HD/IN/AT,如果没有AT等难度,不要管它</div>
<div class="toolbar">
<button id="addSongBtn">+ 添加曲目</button>
<button id="exportBtn">导出数据</button>
<button id="importBtn">导入数据</button>
<input type="file" id="importFile" accept=".json" style="display:none;">
<button id="clearBtn" style="border-color:var(--danger);color:var(--danger);">清空全部</button>
<button class="theme-toggle" id="themeToggleBtn">
<span class="icon" id="themeIcon">🌙</span>
<span id="themeLabel">深色</span>
</button>
</div>
<div class="tree" id="tree"></div>
<!-- 遮罩:曲名输入 -->
<div class="overlay" id="nameOverlay">
<div class="modal">
<div class="modal-title">输入曲名</div>
<input type="text" class="name-input" id="nameInput" placeholder="输入曲名…" autocomplete="off">
<div class="modal-btns">
<button id="nameOkBtn">下一步:选择章节</button> <button id="nameCancelBtn">取消</button>
</div>
</div>
</div>
<!-- 遮罩:章节选择 -->
<div class="overlay" id="pickOverlay">
<div class="modal">
<div class="modal-title">选择所属章节</div>
<div class="pick-list" id="pickList"></div>
<div class="modal-btns">
<button id="pickCancelBtn">取消</button>
</div>
</div>
</div>
<!-- 遮罩:删除曲目确认 -->
<div class="overlay" id="delSongOverlay">
<div class="modal" style="max-width:400px;">
<div class="modal-title" style="color:var(--danger);">删除曲目</div>
<div style="font-size:0.88rem; color:var(--text); line-height:1.7; margin-bottom:18px;">
确定删除 <strong id="delSongName" style="color:var(--text-strong);"></strong> 吗?<br>
<span style="color:var(--text-dim); font-size:0.8rem;">该曲目所有难度的记录都会一并删除,不可恢复。</span>
</div>
<div class="modal-btns"> <button id="delSongCancelBtn">取消</button>
<button id="delSongConfirmBtn" class="danger">确认删除</button>
</div>
</div>
</div>
<!-- 遮罩:数据编辑 -->
<div class="overlay" id="editorOverlay">
<div class="modal">
<div class="modal-title" id="editorTitle">编辑数据</div>
<div class="diff-switch" id="diffSwitch"></div>
<div class="editor-row">
<label>等级</label>
<div class="lv-input-wrap">
<span class="lv-input-prefix">Lv.</span>
<input type="text" id="levelInput" placeholder="15" autocomplete="off">
</div>
</div>
<div class="editor-row">
<label>分数</label>
<input type="number" id="scoreInput" min="0" max="1000000" step="1" placeholder="0~1000000">
<label style="margin-left:4px;">Acc</label>
<input type="number" id="accInput" min="0" max="100" step="0.01" placeholder="0~100"> </div>
<div class="editor-row">
<span style="font-size:0.82rem; color:var(--text-dim);">评级</span>
<span class="rank-display rank-none" id="rankDisplay">—</span>
</div>
<div class="check-row">
<label><input type="checkbox" id="apCheck"> All Perfect</label>
<label><input type="checkbox" id="fcCheck"> Full Combo</label>
</div>
<div class="modal-btns">
<button id="saveBtn">保存</button>
<button id="deleteRecordBtn" class="danger">清除此难度记录</button>
</div>
</div>
</div>
<script>
(function(){
const STORAGE_KEY = 'phigros_recorder_data_v1';
const DATA_VERSION = '1.0';
const THEME_KEY = 'phigros_recorder_theme';
// ---------- 主题切换 ---------- const themeToggleBtn = document.getElementById('themeToggleBtn');
const themeIcon = document.getElementById('themeIcon');
const themeLabel = document.getElementById('themeLabel');
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
if (theme === 'dark') {
themeIcon.textContent = '☀️';
themeLabel.textContent = '浅色';
} else {
themeIcon.textContent = '🌙';
themeLabel.textContent = '深色';
}
try { localStorage.setItem(THEME_KEY, theme); } catch(e) {}
}
function initTheme() {
let saved = null;
try { saved = localStorage.getItem(THEME_KEY); } catch(e) {}
if (saved === 'dark' || saved === 'light') {
applyTheme(saved);
} else {
// 跟随系统
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(prefersDark ? 'dark' : 'light'); }
}
themeToggleBtn.addEventListener('click', function() {
const cur = document.documentElement.getAttribute('data-theme');
applyTheme(cur === 'dark' ? 'light' : 'dark');
});
initTheme();
// ---------- 章节骨架 ----------
const CHAPTERS = [
{ group: '主线章节', chapters: [
{ id: 'legacy', name: 'Legacy(原 Chapter 1–4)', songs: [] },
{ id: 'main5', name: 'Chapter 5', songs: [] },
{ id: 'main6', name: 'Chapter 6', songs: [] },
{ id: 'main7', name: 'Chapter 7', songs: [] },
{ id: 'main8', name: 'Chapter 8', songs: [] },
{ id: 'main9', name: 'Chapter 9', songs: [] },
]},
{ group: '支线章节', chapters: [
{ id: 'side1', name: 'Side Story 1 · 忘忧宫', songs: [] },
{ id: 'side2', name: 'Side Story 2 · 弭刻日', songs: [] },
{ id: 'side3', name: 'Side Story 3 · 盗乐行', songs: [] },
{ id: 'side4', name: 'Side Story 4 · 无相乡', songs: [] }, ]},
{ group: '额外章节', chapters: [
{ id: 'single', name: '单曲精选集', songs: [] },
{ id: 'more', name: '其他歌曲', songs: [] },
]},
];
let records = {};
let songIdCounter = 1;
let currentSongId = null;
let currentDiff = null;
let pendingSongName = null;
let pendingDeleteSongId = null;
function ensureIds() {
CHAPTERS.forEach(g => g.chapters.forEach(ch => {
ch.songs.forEach(s => { if (!s.id) s.id = 's' + (songIdCounter++); });
}));
}
function loadData() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) {
if (parsed.records) records = parsed.records;
if (parsed.chapters) {
parsed.chapters.forEach(g => {
const tg = CHAPTERS.find(x => x.group === g.group);
if (!tg) return;
g.chapters.forEach(sc => {
const tc = tg.chapters.find(x => x.id === sc.id);
if (tc && sc.songs) tc.songs = sc.songs;
});
});
}
if (parsed.songIdCounter) songIdCounter = parsed.songIdCounter;
}
}
} catch(e) { console.warn(e); }
ensureIds();
}
function saveData() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
version: DATA_VERSION, records, chapters: CHAPTERS, songIdCounter
}));
} catch(e) { alert('保存失败,存储空间可能已满'); } }
function getRank(score, fc, ap) {
if (ap) return { label: 'Phi', cls: 'rank-phi' };
const hasScore = !(score === null || score === undefined || score === '' || isNaN(score));
if (fc) {
if (hasScore && Number(score) >= 1000000) return { label: 'Phi', cls: 'rank-phi' };
return { label: 'V', cls: 'rank-nu-fc' };
}
if (!hasScore) return { label: '—', cls: 'rank-none' };
const s = Number(score);
if (s >= 1000000) return { label: 'Phi', cls: 'rank-phi' };
if (s >= 960000) return { label: 'V', cls: 'rank-nu-white' };
if (s >= 920000) return { label: 'S', cls: 'rank-s' };
if (s >= 880000) return { label: 'A', cls: 'rank-a' };
if (s >= 820000) return { label: 'B', cls: 'rank-b' };
if (s >= 700000) return { label: 'C', cls: 'rank-c' };
return { label: 'False', cls: 'rank-false' };
}
// ---------- 渲染树 ----------
const treeEl = document.getElementById('tree');
function renderTree() {
let html = ''; CHAPTERS.forEach((group, gi) => {
html += `<div class="group">
<div class="group-header open" data-gi="${gi}"><span class="arrow">▶</span>${group.group}</div>
<div class="group-body open" data-gi="${gi}">`;
group.chapters.forEach((ch, ci) => {
html += `<div class="chapter">
<div class="chapter-header" data-gi="${gi}" data-ci="${ci}">
<span class="arrow">▶</span>${ch.name}
<span style="color:var(--text-faint);font-size:0.7rem;margin-left:6px;">(${ch.songs.length})</span>
</div>
<div class="chapter-body">`;
if (ch.songs.length === 0) {
html += `<div class="empty-song">暂无曲目</div>`;
} else {
ch.songs.forEach(song => { html += renderSongRow(song); });
}
html += `</div></div>`;
});
html += `</div></div>`;
});
treeEl.innerHTML = html;
attachTreeEvents();
}
function renderSongRow(song) { const diffs = ['EZ','HD','IN','AT'];
let btnHtml = diffs.map(d => {
const rec = records[song.id + '|' + d];
const sel = (currentSongId === song.id && currentDiff === d) ? 'selected' : '';
const hasLv = rec && rec.level && rec.level.trim();
const lvHtml = hasLv
? `<span class="lv-tag" title="${d} 等级">Lv.${escapeHtml(rec.level.trim())}</span>`
: `<span class="lv-tag empty" title="未设置等级">—</span>`;
return `<span class="diff-btn-wrap">
<button class="diff-btn ${d} ${sel}" data-songid="${song.id}" data-diff="${d}" title="${d}${rec ? ' (已记录)' : ''}">${d}</button>
${lvHtml}
</span>`;
}).join('');
return `<div class="song-item">
<div class="song-row ${currentSongId === song.id ? 'active' : ''}" data-songid="${song.id}">
<span class="song-name">${escapeHtml(song.name)}</span>
<div class="diff-btns">${btnHtml}</div>
<button class="song-del-btn" data-songid="${song.id}" title="删除此曲目">🗑</button>
</div>
</div>`;
}
function escapeHtml(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function attachTreeEvents() {
treeEl.querySelectorAll('.group-header').forEach(el => {
el.addEventListener('click', function() {
this.classList.toggle('open');
this.nextElementSibling.classList.toggle('open');
});
});
treeEl.querySelectorAll('.chapter-header').forEach(el => {
el.addEventListener('click', function(e) {
e.stopPropagation();
this.classList.toggle('open');
this.nextElementSibling.classList.toggle('open');
});
});
treeEl.querySelectorAll('.diff-btn').forEach(el => {
el.addEventListener('click', function(e) {
e.stopPropagation();
openEditor(this.dataset.songid, this.dataset.diff);
});
});
treeEl.querySelectorAll('.song-del-btn').forEach(el => {
el.addEventListener('click', function(e) {
e.stopPropagation();
const songId = this.dataset.songid; const found = findSongById(songId);
if (!found) return;
pendingDeleteSongId = songId;
document.getElementById('delSongName').textContent = found.song.name;
document.getElementById('delSongOverlay').classList.add('open');
});
});
treeEl.querySelectorAll('.song-row').forEach(el => {
el.addEventListener('click', function(e) {
if (e.target.closest('.diff-btn-wrap') || e.target.closest('.song-del-btn')) return;
openEditor(this.dataset.songid, 'IN');
});
});
}
function findSongById(id) {
for (const g of CHAPTERS) for (const ch of g.chapters) {
const s = ch.songs.find(x => x.id === id);
if (s) return { song: s, chapter: ch, group: g };
}
return null;
}
// ---------- 删除曲目确认 ----------
const delSongOverlay = document.getElementById('delSongOverlay'); delSongOverlay.addEventListener('click', function(e) {
if (e.target === delSongOverlay) { pendingDeleteSongId = null; delSongOverlay.classList.remove('open'); }
});
document.getElementById('delSongCancelBtn').addEventListener('click', function() {
pendingDeleteSongId = null;
delSongOverlay.classList.remove('open');
});
document.getElementById('delSongConfirmBtn').addEventListener('click', function() {
if (!pendingDeleteSongId) return;
CHAPTERS.forEach(g => g.chapters.forEach(ch => {
ch.songs = ch.songs.filter(s => s.id !== pendingDeleteSongId);
}));
Object.keys(records).forEach(k => {
if (k.startsWith(pendingDeleteSongId + '|')) delete records[k];
});
if (currentSongId === pendingDeleteSongId) { currentSongId = null; currentDiff = null; }
pendingDeleteSongId = null;
delSongOverlay.classList.remove('open');
saveData();
renderTree();
});
// ---------- 编辑弹窗 ----------
const editorOverlay = document.getElementById('editorOverlay');
const editorTitle = document.getElementById('editorTitle'); const diffSwitch = document.getElementById('diffSwitch');
const levelInput = document.getElementById('levelInput');
const scoreInput = document.getElementById('scoreInput');
const accInput = document.getElementById('accInput');
const apCheck = document.getElementById('apCheck');
const fcCheck = document.getElementById('fcCheck');
const rankDisplay = document.getElementById('rankDisplay');
const DIFFS = ['EZ','HD','IN','AT'];
function openEditor(songId, diff) {
const found = findSongById(songId);
if (!found) return;
currentSongId = songId;
currentDiff = diff;
renderTree();
editorOverlay.classList.add('open');
editorTitle.textContent = found.song.name;
renderDiffSwitch();
loadDiffData();
}
function renderDiffSwitch() {
diffSwitch.innerHTML = DIFFS.map(d => {
const rec = records[currentSongId + '|' + d];
const active = d === currentDiff ? `active ${d}` : ''; const lv = rec && rec.level && rec.level.trim() ? `Lv.${escapeHtml(rec.level.trim())}` : '未设等级';
return `<button class="${active}" data-diff="${d}">
<span class="sw-name">${d}</span>
<span class="sw-lv">${lv}</span>
</button>`;
}).join('');
diffSwitch.querySelectorAll('button').forEach(el => {
el.addEventListener('click', function() {
currentDiff = this.dataset.diff;
renderDiffSwitch();
loadDiffData();
});
});
}
function loadDiffData() {
const key = currentSongId + '|' + currentDiff;
const rec = records[key] || { score: null, acc: null, ap: false, fc: false, level: '' };
levelInput.value = rec.level || '';
scoreInput.value = rec.score ?? '';
accInput.value = rec.acc ?? '';
apCheck.checked = !!rec.ap;
fcCheck.checked = !!rec.fc;
applyLockAndRank();
}
function applyLockAndRank() {
if (apCheck.checked) {
scoreInput.value = 1000000;
scoreInput.disabled = true;
} else {
scoreInput.disabled = false;
}
const ap = apCheck.checked;
const fc = fcCheck.checked;
const scoreVal = scoreInput.value === '' ? null : Number(scoreInput.value);
const rank = getRank(scoreVal, fc, ap);
rankDisplay.textContent = rank.label;
rankDisplay.className = 'rank-display ' + rank.cls;
}
scoreInput.addEventListener('input', applyLockAndRank);
apCheck.addEventListener('change', function() {
if (this.checked) fcCheck.checked = false;
applyLockAndRank();
});
fcCheck.addEventListener('change', function() {
if (this.checked) apCheck.checked = false;
applyLockAndRank();
});
editorOverlay.addEventListener('click', function(e) {
if (e.target === editorOverlay) editorOverlay.classList.remove('open');
});
document.getElementById('saveBtn').addEventListener('click', function() {
if (!currentSongId || !currentDiff) return;
const key = currentSongId + '|' + currentDiff;
const ap = apCheck.checked;
const fc = fcCheck.checked;
let score = scoreInput.value === '' ? null : Number(scoreInput.value);
if (ap) score = 1000000;
records[key] = {
score,
acc: accInput.value === '' ? null : Number(accInput.value),
ap, fc,
level: levelInput.value.trim()
};
saveData();
renderTree();
renderDiffSwitch();
applyLockAndRank();
editorOverlay.classList.remove('open');
});
document.getElementById('deleteRecordBtn').addEventListener('click', function() {
if (!currentSongId || !currentDiff) return;
const key = currentSongId + '|' + currentDiff;
if (records[key]) {
if (confirm('清除该曲目此难度的记录?')) {
delete records[key];
saveData();
renderTree();
loadDiffData();
renderDiffSwitch();
}
}
});
// ---------- 添加曲目 ----------
const nameOverlay = document.getElementById('nameOverlay');
const nameInput = document.getElementById('nameInput');
const pickOverlay = document.getElementById('pickOverlay');
const pickList = document.getElementById('pickList');
document.getElementById('addSongBtn').addEventListener('click', function() {
pendingSongName = null;
nameInput.value = '';
nameOverlay.classList.add('open');
setTimeout(() => nameInput.focus(), 50); });
nameOverlay.addEventListener('click', function(e) {
if (e.target === nameOverlay) nameOverlay.classList.remove('open');
});
document.getElementById('nameCancelBtn').addEventListener('click', function() {
nameOverlay.classList.remove('open');
});
document.getElementById('nameOkBtn').addEventListener('click', function() {
const v = nameInput.value.trim();
if (!v) { alert('曲名不能为空'); return; }
pendingSongName = v;
nameOverlay.classList.remove('open');
openChapterPick();
});
nameInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') document.getElementById('nameOkBtn').click();
});
function openChapterPick() {
let html = '';
CHAPTERS.forEach(g => g.chapters.forEach(ch => {
html += `<div class="pick-item" data-cid="${ch.id}">${g.group} / ${ch.name} <small>(${ch.songs.length})</small></div>`;
}));
pickList.innerHTML = html; pickList.querySelectorAll('.pick-item').forEach(el => {
el.addEventListener('click', function() {
const cid = this.dataset.cid;
let targetCh = null;
CHAPTERS.forEach(g => g.chapters.forEach(ch => { if (ch.id === cid) targetCh = ch; }));
if (!targetCh) return;
targetCh.songs.push({ id: 's' + (songIdCounter++), name: pendingSongName });
pendingSongName = null;
saveData();
renderTree();
pickOverlay.classList.remove('open');
});
});
pickOverlay.classList.add('open');
}
pickOverlay.addEventListener('click', function(e) {
if (e.target === pickOverlay) pickOverlay.classList.remove('open');
});
document.getElementById('pickCancelBtn').addEventListener('click', function() {
pickOverlay.classList.remove('open');
});
// ---------- 导出 / 导入 ----------
document.getElementById('exportBtn').addEventListener('click', function() { const blob = new Blob([JSON.stringify({
version: DATA_VERSION, records, chapters: CHAPTERS, songIdCounter
}, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `phigros_recorder_${new Date().toISOString().slice(0,10)}.json`;
a.click(); URL.revokeObjectURL(url);
});
document.getElementById('importBtn').addEventListener('click', function() {
document.getElementById('importFile').click();
});
document.getElementById('importFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
try {
const data = JSON.parse(ev.target.result);
if (data.records) records = data.records;
if (data.chapters) {
data.chapters.forEach(g => {
const tg = CHAPTERS.find(x => x.group === g.group);
if (!tg) return;
g.chapters.forEach(sc => { const tc = tg.chapters.find(x => x.id === sc.id);
if (tc && sc.songs) tc.songs = sc.songs;
});
});
}
if (data.songIdCounter) songIdCounter = data.songIdCounter;
ensureIds();
saveData();
renderTree();
alert('导入成功' + (data.version ? `(数据版本 ${data.version})` : '(旧格式)'));
} catch(err) { alert('导入失败:' + err.message); }
};
reader.readAsText(file);
e.target.value = '';
});
document.getElementById('clearBtn').addEventListener('click', function() {
if (confirm('清空所有记录和曲目?不可恢复。')) {
localStorage.removeItem(STORAGE_KEY);
location.reload();
}
});
// ---------- 启动 ----------
loadData(); renderTree();
document.querySelectorAll('.chapter-header').forEach(el => {
el.classList.add('open');
el.nextElementSibling.classList.add('open');
});
})();
</script>
</body>
</html> 使用截图




相关文章
- 11-08 【AI创作】Phigros歌曲数据记录工具
- 11-08 价值几千黄金量化EA源码
- 11-30 QQ号码展示网站源码_多套首页模板 带后台
- 07-05 抢先视频V4.0.0 汇集全网影视资源
- 01-22 HuoCMS|免费开源可商用CMS建站系统HuoCMS 2.0下载(thinkphp内核)
- 03-09 期待已久的gpt修复版本来了,内置3.5接口!
- 01-07 Windows12系统网页版HTML源码
- 08-18 小众鼠标特效引导页源码
- 08-11 星宇图床PHP源码V2.0 非第三方接口
- 08-10 Ai智能客服系统在线客服源码+多语言+桌面推送
- 08-09 最新彩虹知识付费模版
- 07-14 2022年最新改良版彩虹代刷知识付费模板系统
取消回复欢迎你发表评论:
- 协助本站优化一下
- 最近发表
- 1剧岛V2.1.7 追剧变得更加轻松
- 2落叶楼阅读V1.0.5 小说漫画二合一阅读App
- 3【AI创作】Phigros歌曲数据记录工具
- 4视频转文字提取v9会员版 一款短视频视频无水印提取工具
- 5软件制作辅助工具箱珍藏版
- 6MMSpace虚拟机v1.2.0 一机双系统 安卓手机跑鸿蒙系统 支持后台挂机
- 7车鱼视听v3.20.1 车机影音软件 聚合抖音 汽水音乐 今日头条等平台
- 8只锁不停锁屏工具V1.1
- 9安天智甲终端防御(个人版)
- 10价值几千黄金量化EA源码
- 11HivisionIDPhotos 本地离线智能AI证件照制作与排版换底色工具
- 12麻将学习器v1
- 13Y2002电音V2.8.1.1 海量动感舞曲随心播放
- 14特狗影视V4.1.4 海量影视内容实时更新
- 15【囧次元】1.5.8.0 新番热番实时更新 功能齐全
Copyright© XGW9.COM版权所有〖小庚资源网〗
〖恒创科技〗为本站提供专业云计算服务
本站发布的内容来源于互联网,如果有侵权内容,请联系我们删除!E-mail:xgzyw6@outlook.com
关于我们|我要投稿|免责声明|XML地图








暂无评论,来添加一个吧。