SOURCE

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes">
  <title>�� 班级积分乐园 · 班主任系统</title>
  <style>
    * {
      box-sizing: border-box;
      font-family: 'Segoe UI', 'Comic Sans MS', 'Chalkboard SE', cursive, sans-serif;
      margin: 0;
      padding: 0;
    }
    body {
      background: linear-gradient(145deg, #fdeff2 0%, #fff5f7 100%);
      min-height: 100vh;
      padding: 1.5rem 1rem 3rem;
    }
    .container {
      max-width: 1400px;
      margin: 0 auto;
      background-color: rgba(255, 248, 245, 0.7);
      backdrop-filter: blur(2px);
      border-radius: 60px 60px 40px 40px;
      padding: 1.8rem 1.5rem 2.5rem;
      box-shadow: 0 15px 30px rgba(255, 150, 180, 0.2), 0 5px 15px rgba(255, 120, 150, 0.1);
      border: 2px solid #ffe2e9;
    }
    h1 {
      font-size: 2.2rem;
      font-weight: 600;
      color: #a64b6b;
      text-shadow: 3px 3px 0 #fcd1dc;
      letter-spacing: 2px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 12px;
      flex-wrap: wrap;
      margin-bottom: 0.5rem;
    }
    h1 small {
      font-size: 1rem;
      background: #ffd9e2;
      padding: 0.3rem 1.2rem;
      border-radius: 40px;
      color: #7a3f54;
      font-weight: 400;
      box-shadow: inset 0 1px 4px #ffeef2;
    }
    .toolbar {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      justify-content: center;
      gap: 12px 18px;
      margin: 1.2rem 0 2rem;
      background: #ffeef2;
      padding: 0.8rem 1.8rem;
      border-radius: 50px;
      border: 1px solid #ffd0db;
    }
    .toolbar button {
      background: white;
      border: none;
      padding: 0.6rem 1.4rem;
      border-radius: 40px;
      font-size: 1rem;
      font-weight: 600;
      color: #5d3f4a;
      box-shadow: 0 4px 0 #dbb6c2;
      cursor: pointer;
      transition: 0.08s linear;
      display: inline-flex;
      align-items: center;
      gap: 6px;
      border: 1px solid #ffc1d0;
      background: #fffaff;
    }
    .toolbar button:active {
      transform: translateY(4px);
      box-shadow: 0 1px 0 #dbb6c2;
    }
    .toolbar .batch-group {
      display: flex;
      gap: 6px;
      align-items: center;
      background: #ffdce5;
      padding: 0.2rem 0.8rem 0.2rem 1.2rem;
      border-radius: 40px;
    }
    .toolbar .batch-group span {
      font-weight: 500;
      color: #6a4352;
    }
    .badge {
      background: #b35d7a;
      color: white;
      padding: 0.2rem 1rem;
      border-radius: 30px;
      font-size: 0.9rem;
    }
    .student-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
      gap: 14px;
      margin: 1.8rem 0 1.5rem;
    }
    .student-card {
      background: #ffffffdd;
      backdrop-filter: blur(4px);
      background: #fff9fb;
      border-radius: 40px 40px 30px 30px;
      padding: 0.9rem 0.6rem 0.7rem;
      box-shadow: 0 8px 0 #f0ced9, 0 8px 18px #fccfdb;
      border: 1px solid #ffdae4;
      transition: 0.1s ease;
      display: flex;
      flex-direction: column;
      align-items: center;
      position: relative;
    }
    .student-card:hover {
      transform: scale(1.01);
      background: #fff6f9;
    }
    .student-id {
      background: #fccfdb;
      color: #572d3e;
      font-weight: 700;
      font-size: 0.8rem;
      padding: 0.15rem 0.9rem;
      border-radius: 30px;
      display: inline-block;
      margin-bottom: 4px;
      letter-spacing: 0.5px;
    }
    .student-name {
      font-size: 1.2rem;
      font-weight: 600;
      color: #38222b;
      background: transparent;
      border: none;
      text-align: center;
      width: 100%;
      padding: 2px 0;
      border-bottom: 2px dashed #ffb7cb;
      margin: 0 0 4px 0;
      outline: none;
      transition: 0.1s;
    }
    .student-name:focus {
      border-bottom: 2px solid #c95d7e;
      background: #fff0f4;
      border-radius: 20px;
    }
    .stats {
      display: flex;
      justify-content: center;
      gap: 12px 18px;
      flex-wrap: wrap;
      width: 100%;
      margin: 5px 0 8px;
      background: #ffecf0;
      padding: 6px 8px;
      border-radius: 40px;
    }
    .stats span {
      background: white;
      padding: 0.15rem 0.9rem;
      border-radius: 30px;
      font-size: 0.9rem;
      font-weight: 600;
      color: #4c2d39;
      box-shadow: inset 0 1px 4px #fccfdb;
    }
    .stats .coin {
      background: #fce1b0;
      color: #7a5427;
    }
    .btn-group {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: 6px;
      margin: 6px 0 4px;
      width: 100%;
    }
    .btn-group button {
      border: none;
      background: white;
      padding: 0.25rem 0.9rem;
      border-radius: 40px;
      font-weight: 600;
      font-size: 0.85rem;
      box-shadow: 0 3px 0 #dbb6c2;
      cursor: pointer;
      transition: 0.05s linear;
      border: 1px solid #fec9d6;
      flex: 1 0 auto;
      min-width: 44px;
      background: #fef6f8;
    }
    .btn-group .add {
      background: #d9f0d9;
      border-color: #b1d8b1;
      color: #1d5a1d;
      box-shadow: 0 3px 0 #a3c9a3;
    }
    .btn-group .sub {
      background: #fdd9d9;
      border-color: #f5bebe;
      color: #8f2f2f;
      box-shadow: 0 3px 0 #dba6a6;
    }
    .btn-group .coin-btn {
      background: #f7e4b0;
      border-color: #eacb8a;
      color: #6f4d1c;
      box-shadow: 0 3px 0 #cfb070;
    }
    .btn-group button:active {
      transform: translateY(3px);
      box-shadow: 0 1px 0 #dbb6c2;
    }
    .btn-group .redeem {
      background: #f7d0f0;
      border-color: #dbaad0;
      color: #5d2d5a;
      box-shadow: 0 3px 0 #c99abb;
    }
    .rank-section {
      margin-top: 2rem;
      background: #fff2f5;
      border-radius: 60px;
      padding: 0.8rem 1.2rem 1.8rem;
      border: 2px solid #ffd0db;
    }
    .rank-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      flex-wrap: wrap;
      gap: 10px;
    }
    .rank-header h2 {
      color: #732c47;
      font-weight: 600;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    #rankBtn {
      background: #ffb7cb;
      border: none;
      padding: 0.5rem 2rem;
      border-radius: 40px;
      font-weight: 700;
      color: #3b1f29;
      box-shadow: 0 5px 0 #b47189;
      cursor: pointer;
      transition: 0.05s linear;
      font-size: 1rem;
      border: 1px solid #ffa5bd;
    }
    #rankBtn:active {
      transform: translateY(5px);
      box-shadow: 0 1px 0 #b47189;
    }
    #rankList {
      margin-top: 15px;
      max-height: 240px;
      overflow-y: auto;
      padding: 0 4px;
      display: flex;
      flex-direction: column;
      gap: 5px;
      background: #ffeef2;
      border-radius: 30px;
      padding: 10px 12px;
    }
    .rank-item {
      display: flex;
      justify-content: space-between;
      background: white;
      padding: 0.4rem 1.2rem;
      border-radius: 50px;
      border: 1px solid #fbcad6;
      font-weight: 500;
      color: #3b222c;
    }
    .rank-item span:first-child {
      font-weight: 600;
      color: #b1456a;
    }
    .hidden {
      display: none;
    }
    .footer-tip {
      margin: 1rem 0 0;
      text-align: center;
      color: #a86b7c;
      font-weight: 400;
    }
    /* 可爱滚动条 */
    #rankList::-webkit-scrollbar {
      width: 8px;
    }
    #rankList::-webkit-scrollbar-track {
      background: #fddae4;
      border-radius: 20px;
    }
    #rankList::-webkit-scrollbar-thumb {
      background: #e7a6bb;
      border-radius: 20px;
    }
    @media (max-width: 580px) {
      .container { padding: 1rem 0.8rem; }
      .student-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
    }
  </style>
</head>
<body>
<div class="container" id="app">
  <h1>
    �� 班级积分乐园
    <small>1班 · 100位小可爱</small>
  </h1>

  <div class="toolbar">
    <div class="batch-group">
      <span>�� 批量</span>
      <button @click="batchAdd(1)">➕全+1</button>
      <button @click="batchAdd(-1)">➖全-1</button>
      <button @click="batchAdd(5)">��全+5</button>
      <button @click="batchAdd(-5)">��️全-5</button>
    </div>
    <button @click="resetAll">�� 重置所有</button>
  </div>

  <!-- 学生卡片网格 -->
  <div class="student-grid">
    <div class="student-card" v-for="(stu, index) in students" :key="stu.id">
      <span class="student-id">#{{ stu.id }}</span>
      <input class="student-name" type="text" v-model="stu.name" placeholder="输入名字" maxlength="6">
      <div class="stats">
        <span>⭐ {{ stu.score }}</span>
        <span class="coin">�� {{ stu.coin }}</span>
      </div>
      <div class="btn-group">
        <button class="add" @click="changeScore(stu.id, 1)">+1</button>
        <button class="sub" @click="changeScore(stu.id, -1)">-1</button>
        <button class="add" @click="changeScore(stu.id, 5)">+5</button>
        <button class="sub" @click="changeScore(stu.id, -5)">-5</button>
        <button class="coin-btn" @click="changeCoinOnly(stu.id, -1)">��-1</button>
        <button class="redeem" @click="redeemReward(stu.id)">��兑换</button>
      </div>
    </div>
  </div>

  <!-- 排行榜区域 -->
  <div class="rank-section">
    <div class="rank-header">
      <h2>�� 积分排行榜</h2>
      <button id="rankBtn" @click="showRank = !showRank">{{ showRank ? '�� 收起' : '�� 显示排行榜' }}</button>
    </div>
    <div id="rankList" :class="{ hidden: !showRank }">
      <div v-if="showRank && sortedRank.length === 0" style="padding: 12px; text-align: center;">还没有积分呢~ 加油哦 ��</div>
      <div class="rank-item" v-for="(item, idx) in sortedRank" :key="item.id">
        <span>{{ idx+1 }}. #{{ item.id }} {{ item.name || '小可爱' }}</span>
        <span>⭐ {{ item.score }}  �� {{ item.coin }}</span>
      </div>
    </div>
  </div>
  <div class="footer-tip">✨ 积分自动1:1转换兑换币 · 兑换仅扣币不影响积分</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  (function(){
    // 生成100个学生数据 (学号1~100)
    function buildStudents() {
      const arr = [];
      for (let i = 1; i <= 100; i++) {
        arr.push({
          id: i,
          name: '',
          score: 0,
          coin: 0
        });
      }
      return arr;
    }

    // 本地存储 key
    const STORAGE_KEY = 'classroom_100_integral_system';

    // 读取存储,若没有则初始化
    function loadData() {
      const stored = localStorage.getItem(STORAGE_KEY);
      if (stored) {
        try {
          const parsed = JSON.parse(stored);
          // 保证100个,如果长度不对则重建 (防止意外)
          if (Array.isArray(parsed) && parsed.length === 100) {
            return parsed;
          }
        } catch (e) {}
      }
      return buildStudents();
    }

    // 保存
    function saveData(data) {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
    }

    new Vue({
      el: '#app',
      data: {
        students: loadData(),
        showRank: false
      },
      computed: {
        // 排序: 按积分降序,积分相同按学号
        sortedRank() {
          const copy = [...this.students];
          copy.sort((a, b) => {
            if (b.score !== a.score) return b.score - a.score;
            return a.id - b.id;
          });
          return copy;
        }
      },
      watch: {
        students: {
          deep: true,
          handler(newVal) {
            saveData(newVal);
          }
        }
      },
      methods: {
        // 改变积分 (同时自动改变兑换币 1:1)
        changeScore(id, delta) {
          const stu = this.students.find(s => s.id === id);
          if (!stu) return;
          // 积分变化
          stu.score = Math.max(0, stu.score + delta);
          // 兑换币同步变化 (1:1)
          stu.coin = Math.max(0, stu.coin + delta);
          // 确保兑换币不会因为积分减少而变成负数,上面已经Math.max处理
          // 但如果有额外情况,保证coin >=0
          if (stu.coin < 0) stu.coin = 0;
          // 自动保存由watch触发
        },
        // 仅改变兑换币 (独立操作,不影响积分)  用于兑换奖励或者扣除币
        changeCoinOnly(id, delta) {
          const stu = this.students.find(s => s.id === id);
          if (!stu) return;
          const newCoin = stu.coin + delta;
          if (newCoin < 0) {
            alert('�� 兑换币不够啦~ 当前 ��' + stu.coin);
            return;
          }
          stu.coin = newCoin;
        },
        // 兑换奖励:消耗兑换币,积分不变
        redeemReward(id) {
          const stu = this.students.find(s => s.id === id);
          if (!stu) return;
          if (stu.coin <= 0) {
            alert('�� 兑换币不足,无法兑换奖励。当前�� ' + stu.coin);
            return;
          }
          // 可以自定义兑换消耗,这里设定每次兑换消耗1个兑换币 (可调整)
          // 但根据需求“兑换奖励之后兑换币相应减少”,扣1个币作为示例。
          // 也可改为扣除指定数量,但需求没有明确,常见是1币兑换一次奖励。
          // 添加一个更灵活的:可输入扣几个? 但设计简洁,用默认1,并提供“扣除兑换币按钮”单独扣除。
          // 此处兑换默认扣1币 (也可改变)
          const cost = 1;
          if (stu.coin < cost) {
            alert('�� 兑换币不足 ' + cost + ' 个,当前只有 ' + stu.coin);
            return;
          }
          stu.coin -= cost;
          alert('�� 兑换成功! 消耗 ' + cost + ' 兑换币,获得小奖励~ 积分不变 ��');
        },
        // 批量加减分 (同时影响积分和兑换币)
        batchAdd(delta) {
          if (delta === 0) return;
          if (delta > 0) {
            if (!confirm(`确定全班每位同学 +${delta} 积分 & 兑换币吗?`)) return;
          } else {
            if (!confirm(`确定全班每位同学 ${delta} 积分 & 兑换币吗? (减分将同步减少兑换币)`)) return;
          }
          for (let stu of this.students) {
            // 确保不出现负数
            const newScore = Math.max(0, stu.score + delta);
            const newCoin = Math.max(0, stu.coin + delta);
            stu.score = newScore;
            stu.coin = newCoin;
          }
        },
        // 重置所有数据
        resetAll() {
          if (!confirm('⚠️ 重置所有积分、兑换币、名字? (不可撤销)')) return;
          this.students = buildStudents();
          saveData(this.students);
        }
      },
      mounted() {
        // 当数据初始化后,确保所有学生都有名字占位 (但保留空)
        // 如果有需要可以预置一些名字?但让班主任自己输入更可爱。
      }
    });
  })();
</script>
</body>
</html>
console 命令行工具 X clear

                    
>
console