课程表

自行修改课程时间 自动同步最准确时间

适配设备:RW

作者:giao_114gg

下载量:26 · 点赞:0

可信源码风险提示:保存或下载前请阅读源码和权限声明。
SHA-2565a0abd2b1eb03ef7e89ae652e14eb26010ce25a6ae2de9db31a83119fd216eeb
const DATA_KEY = 'bandclass_clock_v1';

// ==== 课表数据(从备份提取,周一~周日) ====
const TABLE = {
  periods: [
    { start: '08:00', end: '08:40' },
    { start: '08:50', end: '09:30' },
    { start: '10:30', end: '11:10' },
    { start: '11:20', end: '12:00' },
    { start: '13:20', end: '14:00' },
    { start: '14:10', end: '14:50' },
    { start: '15:05', end: '15:45' },
    { start: '15:55', end: '16:35' },
    { start: '16:45', end: '17:25' }
  ],
  weeks: [
    ['政治','体育','语文','语文','英语','数学','物理','美术','历史'],
    ['数学','英语','物理','体育','语文','地理','信息技术','社团','社团'],
    ['历史','物理','英语','数学','生物','地理','语文','体育','自习'],
    ['语文','体育','数学','英语','政治','物理','班会','自习','自习'],
    ['体育','数学','音乐','英语','生物','语文','通用技术','自习',''],
    ['','','','','','','','',''],
    ['','','','','','','','','']
  ]
};

const WEEK_NAMES = ['一','二','三','四','五','六','日'];

let state = { offset: 0, lastSync: 0, lastSyncSrc: '', view: 'clock' };
let renderTimer = null;
let syncTimer = null;
let lastSec = -1;

async function saveState() {
  try { await script.data.set(DATA_KEY, JSON.parse(JSON.stringify(state))); } catch (e) {}
}

async function loadState() {
  try {
    const s = await script.data.get(DATA_KEY, null);
    if (s && typeof s === 'object') {
      state.offset = Number(s.offset) || 0;
      state.lastSync = Number(s.lastSync) || 0;
      state.lastSyncSrc = String(s.lastSyncSrc || '');
    }
  } catch (e) {}
}

function now() { return new Date(Date.now() + state.offset); }
function pad(n) { return n < 10 ? '0' + n : String(n); }
function fmtTime(d) { return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); }
function fmtDate(d) { return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()); }

function fmtCountdown(ms) {
  if (ms <= 0) return '00:00';
  const t = Math.floor(ms / 1000);
  const h = Math.floor(t / 3600);
  const m = Math.floor((t % 3600) / 60);
  const s = t % 60;
  return h > 0 ? h + ':' + pad(m) + ':' + pad(s) : pad(m) + ':' + pad(s);
}

function parseHM(hm) {
  const p = hm.split(':');
  return (+p[0]) * 3600000 + (+p[1]) * 60000;
}

function getDayIdx(d) {
  const jd = d.getDay();
  return jd === 0 ? 6 : jd - 1;
}

function analyze(d) {
  const dayIdx = getDayIdx(d);
  const lessons = TABLE.weeks[dayIdx] || [];
  const nowMs = d.getHours() * 3600000 + d.getMinutes() * 60000 + d.getSeconds() * 1000 + d.getMilliseconds();

  const items = TABLE.periods.map((p, i) => ({
    idx: i,
    name: lessons[i] || '',
    start: p.start,
    end: p.end,
    startMs: parseHM(p.start),
    endMs: parseHM(p.end)
  }));

  let current = null;
  for (const it of items) {
    if (nowMs >= it.startMs && nowMs < it.endMs) { current = it; break; }
  }

  const isEmpty = !lessons.some(n => n);
  const first = items[0];

  let status = 'idle';
  let label = '';
  let cdMs = 0;
  let cdTo = '';

  if (isEmpty) {
    status = 'weekend';
    let found = false;
    for (let k = 1; k <= 7; k++) {
      const idx = getDayIdx(new Date(d.getTime() + k * 86400000));
      if (TABLE.weeks[idx].some(n => n)) {
        const name = k === 1 ? '明天' : '周' + WEEK_NAMES[idx];
        label = '今日无课 · ' + name + '开课';
        cdTo = '距开课';
        cdMs = k * 86400000 - nowMs + first.startMs;
        found = true;
        break;
      }
    }
    if (!found) { label = '今日无课'; cdTo = '—'; cdMs = 0; }
  } else if (current && current.name) {
    status = 'class';
    label = current.name + ' 正在上课';
    cdTo = '距下课';
    cdMs = current.endMs - nowMs;
  } else if (nowMs < first.startMs) {
    status = 'before';
    label = '未开始 · 第一节 ' + (first.name || '无');
    cdTo = '距上课';
    cdMs = first.startMs - nowMs;
  } else {
    let nx = null;
    for (const it of items) {
      if (it.startMs > nowMs && it.name) { nx = it; break; }
    }
    if (nx) {
      status = 'break';
      label = '课间 · 下节 ' + nx.name;
      cdTo = '距上课';
      cdMs = nx.startMs - nowMs;
    } else {
      status = 'after';
      label = '今日已放学';
      cdTo = '距明天第一节课';
      cdMs = 86400000 - nowMs + first.startMs;
    }
  }

  return { dayIdx, lessons, current, items, status, label, countdownMs: cdMs, countdownTo: cdTo };
}

// ==== 网络时间 ====
function httpGetJSON(url) {
  return new Promise(resolve => {
    try {
      system.http.request({
        url: url,
        header: { 'User-Agent': 'Mozilla/5.0' },
        responseType: 'text',
        success: r => { try { resolve(JSON.parse(r.data)); } catch (e) { resolve(null); } },
        fail: () => resolve(null)
      });
    } catch (e) { resolve(null); }
  });
}

async function tryTaobao() {
  const d = await httpGetJSON('https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp');
  if (d && d.data && d.data.t) {
    const t = Number(d.data.t);
    if (t > 0) return t;
  }
  return null;
}

async function trySuning() {
  const d = await httpGetJSON('https://quan.suning.com/getSysTime.do');
  if (d && d.sysTime1) {
    const s = String(d.sysTime1);
    if (s.length >= 14) {
      const dt = new Date(+s.slice(0,4), +s.slice(4,6)-1, +s.slice(6,8),
                          +s.slice(8,10), +s.slice(10,12), +s.slice(12,14));
      if (!isNaN(dt.getTime())) return dt.getTime();
    }
  }
  return null;
}

async function syncNetTime() {
  const t1 = await tryTaobao();
  if (t1) {
    state.offset = t1 - Date.now();
    state.lastSync = Date.now();
    state.lastSyncSrc = '淘宝';
    await saveState();
    return '淘宝';
  }
  const t2 = await trySuning();
  if (t2) {
    state.offset = t2 - Date.now();
    state.lastSync = Date.now();
    state.lastSyncSrc = '苏宁';
    await saveState();
    return '苏宁';
  }
  return null;
}

// ==== 视图 ====
function render() {
  if (state.view === 'clock') renderClock();
  else if (state.view === 'face') renderFace();
  else if (state.view === 'schedule') renderSchedule();
  else renderSettings();
}

function statusColor(st) {
  if (st === 'class') return '#52c41a';
  if (st === 'break') return '#ffd75e';
  if (st === 'before') return '#4fc3f7';
  return '#888888';
}

function renderClock() {
  const d = now();
  const a = analyze(d);
  const sc = statusColor(a.status);

  const children = [
    ui.spacer(6),
    ui.text(fmtTime(d), { size: 52, align: 'center', color: '#ffffff', lines: 1 }),
    ui.text(fmtDate(d) + ' 周' + WEEK_NAMES[a.dayIdx], { size: 14, align: 'center', color: '#888888', lines: 1 }),
    ui.spacer(8),
    ui.column([
      ui.text(a.label, { size: 16, align: 'center', color: sc, lines: 2 }),
      ui.spacer(2),
      ui.text(a.countdownTo + ' ' + fmtCountdown(a.countdownMs),
              { size: 22, align: 'center', color: '#ffffff', lines: 1 })
    ], { background: '#1c1c1e', radius: 10, padding: 10, justify: 'center' }),
    ui.spacer(8)
  ];

  const today = a.items.filter(it => it.name);
  if (today.length > 0) {
    children.push(ui.text('今日课程', { size: 12, color: '#888888', lines: 1 }));
    for (let i = 0; i < today.length && i < 4; i++) {
      const it = today[i];
      const active = a.current === it;
      children.push(ui.row([
        ui.text(it.start.slice(0,5) + '-' + it.end.slice(0,5),
                { size: 11, color: active ? '#52c41a' : '#666666', flex: 1, lines: 1 }),
        ui.text(it.name, { size: 13, color: active ? '#52c41a' : '#cccccc', flex: 2, lines: 1 })
      ], { padding: 3, gap: 4 }));
    }
  }

  children.push(ui.spacer(8));
  children.push(ui.row([
    ui.button('同步', async () => {
      script.toast('同步中...');
      const r = await syncNetTime();
      script.toast(r ? '已同步:' + r : '同步失败');
      render();
    }, { flex: 1, tone: 'primary', height: 38 }),
    ui.button('表盘', () => { state.view = 'face'; render(); }, { flex: 1, tone: 'warning', height: 38 }),
    ui.button('课表', () => { state.view = 'schedule'; render(); }, { flex: 1, tone: 'neutral', height: 38 }),
    ui.button('设置', () => { state.view = 'settings'; render(); }, { flex: 1, tone: 'neutral', height: 38 })
  ], { gap: 4 }));

  ui.render(children);
}

function renderFace() {
  const d = now();
  const a = analyze(d);
  const sc = statusColor(a.status);

  ui.render([
    ui.spacer(20),
    ui.text(fmtTime(d), { size: 64, align: 'center', color: '#ffffff', lines: 1 }),
    ui.text('周' + WEEK_NAMES[a.dayIdx] + ' · ' + fmtDate(d),
            { size: 14, align: 'center', color: '#888888', lines: 1 }),
    ui.spacer(16),
    ui.text(a.label, { size: 18, align: 'center', color: sc, lines: 1 }),
    ui.text(a.countdownTo + ' ' + fmtCountdown(a.countdownMs),
            { size: 28, align: 'center', color: '#ffffff', lines: 1 }),
    ui.spacer(20),
    ui.button('返回', () => { state.view = 'clock'; render(); }, { tone: 'neutral', height: 44 })
  ]);
}

function renderSchedule() {
  const d = now();
  const a = analyze(d);

  const children = [
    ui.row([
      ui.button('返回', () => { state.view = 'clock'; render(); },
                { size: 13, tone: 'neutral', flex: 1, height: 36 }),
      ui.text('周' + WEEK_NAMES[a.dayIdx] + ' 完整课表',
              { size: 15, align: 'center', flex: 2, lines: 1 })
    ], { gap: 6 }),
    ui.spacer(6)
  ];

  for (const it of a.items) {
    const active = a.current === it;
    children.push(ui.row([
      ui.text(pad(it.idx+1) + ' ' + it.start.slice(0,5) + '-' + it.end.slice(0,5),
              { size: 11, color: '#888888', flex: 2, lines: 1 }),
      ui.text(it.name || '—',
              { size: 13, color: active ? '#52c41a' : (it.name ? '#cccccc' : '#555555'), flex: 2, lines: 1 })
    ], { padding: 5, gap: 4, background: active ? '#1a2b1c' : '', radius: 4 }));
  }

  ui.render(children);
}

function renderSettings() {
  const d = now();
  const lastSyncStr = state.lastSync ? new Date(state.lastSync).toLocaleString() : '从未';
  const offsetStr = (state.offset / 1000).toFixed(2) + ' 秒';

  ui.render([
    ui.row([
      ui.button('返回', () => { state.view = 'clock'; render(); },
                { size: 13, tone: 'neutral', flex: 1, height: 36 }),
      ui.text('设置', { size: 16, align: 'center', flex: 2, lines: 1 })
    ], { gap: 6 }),
    ui.spacer(6),
    ui.text('当前时间', { size: 12, color: '#888888', lines: 1 }),
    ui.text(fmtTime(d), { size: 20, align: 'center', color: '#ffffff', lines: 1 }),
    ui.spacer(6),
    ui.column([
      ui.text('时间源: ' + (state.lastSyncSrc || '未同步'),
              { size: 12, align: 'center', color: '#cccccc', lines: 1 }),
      ui.text('上次同步: ' + lastSyncStr,
              { size: 11, align: 'center', color: '#888888', lines: 1 }),
      ui.text('偏差: ' + offsetStr,
              { size: 11, align: 'center', color: '#888888', lines: 1 })
    ], { background: '#1c1c1e', radius: 8, padding: 8 }),
    ui.spacer(8),
    ui.button('立即同步网络时间', async () => {
      script.toast('同步中...');
      const r = await syncNetTime();
      script.toast(r ? '已同步:' + r : '同步失败');
      render();
    }, { tone: 'primary', height: 44 }),
    ui.spacer(4),
    ui.button('手动校准偏移', async () => {
      const res = await dialog.number({
        title: '手动校准',
        message: '输入偏移秒数(正=时间提前,负=时间延后)',
        value: Math.round(state.offset / 1000),
        decimals: 0
      });
      if (res.action !== 'confirm') return;
      const v = Number(res.value);
      if (!isFinite(v)) return;
      state.offset = v * 1000;
      state.lastSync = Date.now();
      state.lastSyncSrc = '手动';
      await saveState();
      render();
    }, { tone: 'warning', height: 40 }),
    ui.spacer(4),
    ui.button('重置全部数据', async () => {
      const r = await dialog.confirm({
        title: '重置', message: '清空所有数据?',
        confirmText: '清空', cancelText: '取消'
      });
      if (r.action !== 'confirm') return;
      state = { offset: 0, lastSync: 0, lastSyncSrc: '', view: 'clock' };
      await saveState();
      render();
    }, { tone: 'danger', height: 40 })
  ]);
}

// ==== 主流程 ====
async function main() {
  await loadState();

  if (Date.now() - state.lastSync > 10 * 60 * 1000) {
    try { await syncNetTime(); } catch (e) {}
  }

  render();

  if (renderTimer) clearInterval(renderTimer);
  renderTimer = setInterval(() => {
    if (state.view === 'settings') return;
    const s = now().getSeconds();
    if (s !== lastSec) { lastSec = s; render(); }
  }, 200);

  if (syncTimer) clearInterval(syncTimer);
  syncTimer = setInterval(async () => {
    if (Date.now() - state.lastSync > 30 * 60 * 1000) {
      await syncNetTime();
    }
  }, 60 * 1000);
}

return main();