可信源码风险提示:保存或下载前请阅读源码和权限声明。
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();