小庚资源网 · 免费提供绿色软件、活动线报以及其他网络资源,好货不私藏!
当前位置:网站首页 > 网站源码 >正文

【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>&#127925; 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">&#127769;</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 = '&#9728;&#65039;';
      themeLabel.textContent = '浅色';
    } else {
      themeIcon.textContent = '&#127769;';
      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">&#9654;</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">&#9654;</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="删除此曲目">&#128465;</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>

使用截图

【AI创作】Phigros歌曲数据记录工具

【AI创作】Phigros歌曲数据记录工具

【AI创作】Phigros歌曲数据记录工具

【AI创作】Phigros歌曲数据记录工具

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

取消回复欢迎你发表评论:

Copyright© XGW9.COM版权所有〖小庚资源网〗
〖恒创科技〗为本站提供专业云计算服务  
本站发布的内容来源于互联网,如果有侵权内容,请联系我们删除!E-mail:xgzyw6@outlook.com
关于我们|我要投稿|免责声明|XML地图