[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>高精度北京时间 - 全端响应式典藏版</title>
<style>
:root {
--bg-color: #0d0d0d; --text-color: #ffffff;
--panel-bg: rgba(255, 255, 255, 0.05); --panel-border: rgba(255, 255, 255, 0.1);
--date-color: #aaaaaa; --ms-color: #4facfe;
--status-syncing: #ff9800; --status-success: #4caf50; --status-error: #f44336;
--modal-bg: #1a1a1a; --modal-text: #eee; --input-bg: #2a2a2a;
--input-border: #444; --btn-bg: #4facfe; --btn-text: #fff;
--clock-scale: 1.0;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
}
body.theme-light {
--bg-color: #f5f7fa; --text-color: #333333; --panel-bg: rgba(255, 255, 255, 0.8); --panel-border: rgba(0, 0, 0, 0.1);
--date-color: #666666; --ms-color: #0078d7; --modal-bg: #ffffff; --modal-text: #333; --input-bg: #f0f0f0;
--input-border: #ccc; --btn-bg: #0078d7;
}
body.theme-cyberpunk {
--bg-color: #050505; --text-color: #0ff; --panel-bg: rgba(0, 15, 30, 0.8); --panel-border: #0ff;
--date-color: #f0f; --ms-color: #f0f; --modal-bg: #001122; --modal-text: #0ff; --input-bg: #000;
--input-border: #f0f; --btn-bg: #f0f; --btn-text: #000;
}
body.theme-cyberpunk .main-time { text-shadow: 0 0 5px rgba(0, 255, 255, 0.5), 0 0 1px rgba(0, 255, 255, 0.8); }
body.theme-matrix {
--bg-color: #000000; --text-color: #00ff00; --panel-bg: rgba(0, 20, 0, 0.6); --panel-border: #00ff00;
--date-color: #00aa00; --ms-color: #00ff00; --modal-bg: #001100; --modal-text: #00ff00; --input-bg: #000;
--input-border: #00ff00; --btn-bg: #00aa00; --btn-text: #000; --font-family: "Courier New", Courier, monospace;
}
body.theme-neon {
--bg-color: #120424; --text-color: #ffffff; --panel-bg: rgba(20, 5, 40, 0.7); --panel-border: #ff00ff;
--date-color: #00ffff; --ms-color: #ff00ff; --modal-bg: #1a0532; --modal-text: #ffffff; --input-bg: #0a0118;
--input-border: #00ffff; --btn-bg: #ff00ff; --btn-text: #ffffff;
}
body.theme-neon .main-time { text-shadow: 0 0 10px #ff00ff, 0 0 20px #00ffff; }
body.theme-minimal {
--bg-color: #ffffff; --text-color: #111111; --panel-bg: transparent; --panel-border: transparent;
--date-color: #888888; --ms-color: #111111; --modal-bg: #ffffff; --modal-text: #111; --input-bg: #f9f9f9;
--input-border: #ddd; --btn-bg: #111; --btn-text: #fff;
}
body.theme-minimal .clock-container { box-shadow: none; border: none; backdrop-filter: none; background: transparent; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; background-color: var(--bg-color); color: var(--text-color); font-family: var(--font-family); overflow: hidden; transition: background-color 0.3s, color 0.3s; }
/* --- 控件基础样式 --- */
.zen-btn { position: absolute; top: 25px; left: 25px; width: 28px; height: 28px; cursor: pointer; fill: var(--text-color); z-index: 90; transition: opacity 0.8s ease-in-out, transform 0.2s; }
.zen-btn:hover { transform: scale(1.1); opacity: 1 !important; transition: opacity 0.2s; }
.top-controls { position: absolute; top: 25px; right: 25px; display: flex; align-items: center; gap: 15px; z-index: 50; transition: opacity 0.3s, transform 0.3s; }
.control-icon { width: 28px; height: 28px; cursor: pointer; opacity: 0.5; transition: 0.2s; fill: var(--text-color); }
.control-icon:hover { opacity: 1; }
.zoom-controls { display: flex; gap: 10px; background: var(--panel-bg); padding: 5px 12px; border-radius: 20px; border: 1px solid var(--panel-border);}
.zoom-btn { font-size: 1.2rem; font-weight: bold; cursor: pointer; opacity: 0.6; user-select: none; }
.zoom-btn:hover { opacity: 1; }
.sync-status { margin-top: 15px; font-size: 0.85rem; letter-spacing: 1px; font-weight: 600; transition: opacity 0.3s; height: 20px;}
/* --- 极简模式控制 --- */
body.minimal-mode .top-controls { opacity: 0; pointer-events: none; transform: translateY(-10px); }
body.minimal-mode .sync-status { opacity: 0; pointer-events: none; }
body.minimal-mode .timezone-label { opacity: 0.4; }
/* --- 时钟主体 (PC 端默认样式) --- */
.clock-container { text-align: center; background: var(--panel-bg); padding: 50px 80px; border-radius: 30px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); border: 1px solid var(--panel-border); transform: scale(var(--clock-scale)); transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), background 0.3s, border 0.3s; }
.date-display { font-size: 1.8rem; color: var(--date-color); margin-bottom: 20px; letter-spacing: 3px; }
.time-display { display: flex; align-items: baseline; justify-content: center; font-variant-numeric: tabular-nums; }
.main-time { font-size: 8rem; font-weight: 700; letter-spacing: 2px; }
.milliseconds { font-size: 4rem; font-weight: 400; color: var(--ms-color); margin-left: 8px; width: 125px; text-align: left; }
.timezone-label { margin-top: 25px; font-size: 1.1rem; color: var(--date-color); letter-spacing: 5px; text-transform: uppercase; transition: opacity 0.3s;}
/* --- 模态框与设置 --- */
.modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.7); display: flex; justify-content: center; align-items: center; z-index: 100; visibility: hidden; opacity: 0; transition: opacity 0.2s; }
.modal-overlay.show { visibility: visible; opacity: 1; }
.modal { background: var(--modal-bg); color: var(--modal-text); border-radius: 20px; width: 560px; max-width: 95%; height: 580px; border: 1px solid var(--panel-border); box-shadow: 0 20px 60px rgba(0,0,0,0.6); display: flex; flex-direction: column; overflow: hidden; }
.modal-tabs { display: flex; background: rgba(0,0,0,0.2); border-bottom: 1px solid var(--panel-border); flex-shrink: 0; }
.tab-btn { flex: 1; padding: 15px 10px; text-align: center; cursor: pointer; opacity: 0.5; font-weight: bold; transition: 0.2s; font-size: 0.95rem;}
.tab-btn.active { opacity: 1; border-bottom: 3px solid var(--btn-bg); background: rgba(255,255,255,0.05); color: var(--btn-bg); }
.tab-content { padding: 20px 25px; display: none; flex: 1; flex-direction: column; overflow-y: auto; }
.tab-content.active { display: flex; }
.setting-group { margin-bottom: 15px; flex-shrink: 0;}
.setting-group label { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; font-weight: bold; font-size: 0.95rem;}
select, input[type="text"] { width: 100%; padding: 10px; border-radius: 8px; border: 1px solid var(--input-border); background: var(--input-bg); color: var(--modal-text); font-size: 0.95rem;}
.source-list { flex: 1; overflow-y: auto; background: rgba(0,0,0,0.2); border-radius: 10px; padding: 5px; margin-bottom: 10px; min-height: 150px;}
.source-item { display: flex; align-items: center; padding: 10px 12px; border-bottom: 1px solid var(--panel-border); font-size: 0.95rem; gap: 8px; cursor: pointer; transition: background 0.2s; border-radius: 6px; }
.source-item:last-child { border-bottom: none; }
.source-item:hover { background: rgba(255,255,255,0.05); }
.source-item.intent-selected { background: rgba(255,255,255,0.08); border-left: 3px solid var(--btn-bg); }
.source-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0.8;}
.source-item.intent-selected .source-name { opacity: 1; font-weight: bold; }
.ping-result { font-size: 0.8rem; font-weight: bold; white-space: nowrap; margin-right: 5px; }
.check-mark { color: var(--status-success); font-weight: bold; font-size: 1.1rem; display: none; margin-right: 5px;}
.source-item.actual-active .check-mark { display: block; }
.del-btn { background: none; color: var(--status-error); border: none; cursor: pointer; padding: 5px; opacity: 0.7;}
.del-btn:hover { opacity: 1; }
.switch { position: relative; display: inline-block; width: 44px; height: 22px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #444; transition: .4s; border-radius: 22px; }
.slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: var(--status-success); }
input:checked + .slider:before { transform: translateX(22px); }
.modal-footer { padding: 15px 25px; border-top: 1px solid var(--panel-border); display: flex; gap: 10px; flex-shrink: 0; }
.primary-btn { padding: 10px; border: none; border-radius: 8px; cursor: pointer; background: var(--btn-bg); color: var(--btn-text); font-weight: bold; transition: opacity 0.2s;}
.primary-btn:hover:not(:disabled) { opacity: 0.8; }
.primary-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* ================= 移动端全新重构排版方案 ================= */
/* 设备级策略:如果是移动端,强制干掉由于手动缩放带来的越界问题 */
body.device-mobile .clock-container { transform: none !important; }
body.device-mobile .zoom-controls { display: none; }
/* 移动端 竖屏适配 (Portrait) */
@media screen and (max-width: 768px) {
.clock-container {
width: 100vw; height: 100vh;
padding: 0 4vw; border-radius: 0; border: none; box-shadow: none; background: transparent !important;
display: flex; flex-direction: column; justify-content: center; align-items: center;
}
.top-controls { top: 3vh; right: 5vw; gap: 10px; }
.zen-btn { top: 3vh; left: 5vw; width: 32px; height: 32px; }
.date-display { font-size: 4.5vw; margin-bottom: 2vh; letter-spacing: 1vw; }
.time-display { width: 100%; justify-content: center; align-items: baseline; }
/* 流式字体引擎:无论屏幕多窄都能完美适配 */
.main-time { font-size: 16vw; letter-spacing: 0; text-align: center; }
.milliseconds { font-size: 7vw; width: 11vw; margin-left: 1vw; text-align: left; }
.timezone-label { font-size: 3.2vw; margin-top: 4vh; letter-spacing: 0.5vw; }
.sync-status { font-size: 3vw; margin-top: 2vh; max-width: 90%; word-wrap: break-word; text-align: center; line-height: 1.5; }
.modal { width: 92vw; height: 85vh; max-height: none; }
.tab-btn { font-size: 3.5vw; padding: 12px 2px; }
.setting-group label { font-size: 3.8vw; }
.source-name { font-size: 3.5vw; }
}
/* 移动端 横屏适配 (Landscape,屏幕高度极度受限情况) */
@media screen and (max-height: 500px) and (orientation: landscape) {
.clock-container {
width: 100vw; height: 100vh;
padding: 0; border-radius: 0; border: none; box-shadow: none; background: transparent !important;
display: flex; flex-direction: column; justify-content: center; align-items: center;
}
.top-controls { top: 4vh; right: 4vw; }
.zen-btn { top: 4vh; left: 4vw; }
/* 横屏改用高度视口单位 (vh) 进行极致压缩 */
.date-display { font-size: 5vh; margin-bottom: 3vh; letter-spacing: 1vh; }
.main-time { font-size: 26vh; letter-spacing: 0; }
.milliseconds { font-size: 10vh; width: 14vh; margin-left: 1vh; }
.timezone-label { font-size: 4vh; margin-top: 4vh; letter-spacing: 0.5vh; }
.sync-status { font-size: 3.5vh; margin-top: 2vh; }
.modal { width: 85vw; height: 85vh; }
.tab-btn { font-size: 2.5vh; }
}
</style>
</head>
<body class="theme-dark">
<svg id="zen-btn" class="zen-btn" viewBox="0 0 24 24">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>
<div class="top-controls">
<div class="zoom-controls">
<div class="zoom-btn">-</div>
<div style="font-size: 0.8rem; opacity: 0.4; line-height: 25px;">ZOOM</div>
<div class="zoom-btn">+</div>
</div>
<svg class="control-icon" id="settings-btn" viewBox="0 0 24 24">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.73,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.43-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.49-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>
</div>
<div class="clock-container" id="clock-body">
<div class="date-display" id="date">--</div>
<div class="time-display">
<span class="main-time" id="main-time">00:00:00</span>
<span class="milliseconds" id="ms">.000</span>
</div>
<div class="timezone-label">中国标准时间 · BEIJING TIME</div>
<div class="sync-status" id="sync-status">--</div>
</div>
<div class="modal-overlay" id="modal-overlay">
<div class="modal">
<div class="modal-tabs">
<div class="tab-btn active">显示设置</div>
<div class="tab-btn">授时中心</div>
<div class="tab-btn">校准引擎</div>
</div>
<div class="tab-content active">
<div class="setting-group">
<label>UI 主题风格</label>
<select id="theme-select">
<option value="theme-dark">深色极致 (Dark)</option>
<option value="theme-light">浅色极简 (Light)</option>
<option value="theme-cyberpunk">赛博朋克 (Cyberpunk)</option>
<option value="theme-matrix">黑客帝国 (Matrix)</option>
<option value="theme-neon">霓虹幻影 (Neon)</option>
<option value="theme-minimal">极简主义 (Minimal)</option>
</select>
</div>
<p style="font-size: 0.85rem; opacity: 0.5; line-height: 1.6; margin-top: 10px;">
* 专注模式:点击左上角「全屏图标」隐藏干扰 UI。<br>
* 缩放控制:PC端可用 +/- 无极调节大小铺满屏幕。
</p>
</div>
<div class="tab-content">
<div class="setting-group" style="margin-bottom: 5px;">
<label style="display: flex; justify-content: space-between; align-items: center;">
<span>自动无缝切换可用源</span>
<div class="switch">
<input type="checkbox" id="auto-switch-toggle">
<span class="slider"></span>
</div>
</label>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 0.8rem; opacity: 0.6;">* 绿勾为当前生效节点</span>
<button class="primary-btn" style="padding: 4px 10px; font-size: 0.8rem;" id="btn-test-all">测试所有源</button>
</div>
<div class="source-list" id="source-list"></div>
<div style="display: flex; gap: 8px; flex-shrink: 0;">
<input type="text" id="new-source-url" placeholder="添加 API (支持JSONP)...">
<button class="primary-btn" style="flex: 0 0 60px; padding: 0;">添加</button>
</div>
</div>
<div class="tab-content">
<div class="setting-group">
<label style="margin-bottom: 10px; color: var(--ms-color);">算法模型</label>
<select id="calib-select">
<option value="ntp">NTP过滤 (连发3次,取最低延迟) [推荐]</option>
<option value="average">平稳均值 (连发3次,取网络波动均值)</option>
<option value="standard">单次探测 (首包即用, 极速响应)</option>
</select>
</div>
<div class="setting-group" style="margin-top: 20px; background: rgba(0,0,0,0.15); padding: 15px; border-radius: 10px; border: 1px solid var(--panel-border);">
<label style="margin-bottom: 10px;">网络线路诊断工具</label>
<button id="test-calib-btn" class="primary-btn" style="width: 100%;">对当前节点发起测速诊断</button>
<div id="calib-result" style="font-size: 0.85rem; color: var(--date-color); margin-top: 15px; min-height: 20px; line-height: 1.5;">系统已就绪。点击获取 RTT 延迟。</div>
</div>
</div>
<div class="modal-footer">
<button class="primary-btn" style="width: 100%; font-size: 1rem;">保存配置并应用</button>
</div>
</div>
</div>
<script>
/* ================= 底层设备智能探针 ================= */
// 判断是否为移动端设备,如果是,则给body打上标签,禁用一切手动缩放
const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobileDevice) document.body.classList.add('device-mobile');
else document.body.classList.add('device-pc');
/* ================= 沉浸模式 (呼吸防静止) ================= */
let idleTimer;
const zenBtn = document.getElementById('zen-btn');
function wakeUpZenBtn() {
zenBtn.style.opacity = '0.5';
clearTimeout(idleTimer);
idleTimer = setTimeout(() => { if (!zenBtn.matches(':hover')) zenBtn.style.opacity = '0.05'; }, 2500);
}
document.addEventListener('mousemove', wakeUpZenBtn);
document.addEventListener('touchstart', wakeUpZenBtn);
wakeUpZenBtn();
function toggleZenMode() {
config.zenMode = !config.zenMode;
document.body.classList.toggle('minimal-mode', config.zenMode);
save();
}
/* ================= 核心:极速源清单 ================= */
const defaultSources = [
{ id: 'local', name: '💻 离线系统本地晶振', url: 'local', isDefault: true },
{ id: 1, name: '淘宝 ACS (智能双擎)', url: 'https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp', isDefault: true },
{ id: 2, name: '腾讯视频 (JSONP穿透)', url: 'https://vv.video.qq.com/checktime?otype=json', isDefault: true },
{ id: 3, name: '苏宁易购 (HTTPS)', url: 'https://f.m.suning.com/api/ct.do', isDefault: true },
{ id: 4, name: '美团 Cube (严格CORS)', url: 'https://cube.meituan.com/ipromotion/cube/toc/component/base/getServerCurrentTime', isDefault: true }
];
let config = JSON.parse(localStorage.getItem('clockConfigV10')) || {
theme: 'theme-dark', sources: defaultSources, activeSourceId: 'local',
autoSwitch: true, zoom: 1.0, calibration: 'ntp', zenMode: false
};
let referenceRealTime = 0; let referencePerformanceTime = 0;
let isSynced = false; let actualUsingSourceId = null;
/* ================= 万能双引擎网络探针 (Fetch + JSONP) ================= */
function parseAnyDate(obj) {
if (typeof obj === 'number') return obj > 1e12 ? obj : obj * 1000;
if (typeof obj === 'string') {
if (/^\d{10,13}$/.test(obj.trim())) return parseInt(obj.trim()) * (obj.trim().length === 10 ? 1000 : 1);
if (/^\d{14}$/.test(obj.trim())) {
let s = obj.trim();
return 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)).getTime();
}
}
if (typeof obj === 'object' && obj !== null) {
for (let k in obj) { let r = parseAnyDate(obj[k]); if (r) return r; }
}
return null;
}
async function probeSourceOnce(source, timeoutMs = 2500) {
const t0 = performance.now();
if (source.url === 'local') throw new Error('Local');
try {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
const res = await fetch(source.url, { cache: 'no-store', signal: controller.signal });
clearTimeout(id);
const textData = await res.text();
let parsedData;
try { parsedData = JSON.parse(textData); }
catch(e) {
const match = textData.match(/\{[\s\S]*\}/);
if (match) { try { parsedData = JSON.parse(match[0]); } catch(e2) { parsedData = textData; } }
else parsedData = textData;
}
const ts = parseAnyDate(parsedData);
if (ts) {
const t1 = performance.now();
return { offset: ts + (t1 - t0)/2 - t1, rtt: t1 - t0 };
}
throw new Error('Fetch解析失败');
}
catch (fetchErr) {
return await new Promise((resolve, reject) => {
const script = document.createElement('script');
const cbName = 'jsonp_cb_' + Date.now() + '_' + Math.floor(Math.random()*1000);
window[cbName] = function(data) { window._lastJsonpData = data; };
window._lastJsonpData = null;
let scriptUrl = source.url;
if (!scriptUrl.includes('callback=')) scriptUrl += (scriptUrl.includes('?') ? '&' : '?') + 'callback=' + cbName;
if (!scriptUrl.includes('type=jsonp')) scriptUrl += '&type=jsonp';
script.src = scriptUrl;
const timer = setTimeout(() => { cleanup(); reject(new Error('超时或阻断')); }, timeoutMs);
function cleanup() {
clearTimeout(timer);
if (script.parentNode) script.parentNode.removeChild(script);
delete window[cbName];
}
script.onload = () => {
const t1 = performance.now();
let ts = null;
if (window.QZOutputJson && window.QZOutputJson.t) ts = window.QZOutputJson.t * 1000;
else if (window._lastJsonpData) ts = parseAnyDate(window._lastJsonpData);
if (ts) resolve({ offset: ts + (t1 - t0)/2 - t1, rtt: t1 - t0 });
else reject(new Error('不支持JSONP'));
cleanup();
};
script.onerror = () => { cleanup(); reject(new Error('跨域阻断')); };
document.head.appendChild(script);
});
}
}
async function executeCalibration(source) {
let mode = config.calibration || 'ntp';
let targetCount = (mode === 'standard') ? 1 : 3;
let samples = [];
for (let i = 0; i < (mode==='standard'?3:5); i++) {
try {
const calib = await probeSourceOnce(source);
samples.push(calib);
if (samples.length >= targetCount) break;
} catch(e) {}
if (samples.length < targetCount) await new Promise(r => setTimeout(r, 150));
}
if (samples.length === 0) throw new Error('失效');
if (mode === 'ntp') return samples.reduce((min, curr) => curr.rtt < min.rtt ? curr : min, samples[0]);
if (mode === 'average') return { offset: samples.reduce((a, b) => a + b.offset, 0)/samples.length, rtt: samples.reduce((a, b) => a + b.rtt, 0)/samples.length };
return samples[0];
}
async function syncNetworkTime() {
const statusEl = document.getElementById('sync-status');
statusEl.textContent = '双引擎探针寻源中...'; statusEl.style.color = 'var(--status-syncing)';
actualUsingSourceId = null; renderSources();
let attemptSources = config.autoSwitch ? config.sources.filter(s => s.id !== 'local') : [config.sources.find(s => s.id === config.activeSourceId)];
for (let source of attemptSources) {
if (!source || source.url === 'local') continue;
try {
const calib = await executeCalibration(source);
referencePerformanceTime = performance.now();
referenceRealTime = referencePerformanceTime + calib.offset;
isSynced = true; actualUsingSourceId = source.id;
statusEl.textContent = `成功锚定: ${source.name} [${config.calibration.toUpperCase()}: ${Math.round(calib.rtt)}ms]`;
statusEl.style.color = 'var(--status-success)';
renderSources(); return;
} catch (e) {}
}
isSynced = false; actualUsingSourceId = 'local';
statusEl.textContent = '网络拦截或超时,启动本地离线兜底';
statusEl.style.color = 'var(--status-error)';
renderSources();
}
/* ================= 诊断测速控制 ================= */
async function testAllSources() {
const btn = document.getElementById('btn-test-all'); btn.disabled = true; btn.textContent = '并行探测中...';
for (let s of config.sources) {
if (s.id === 'local') continue;
const pingEl = document.getElementById(`ping-${s.id}`);
pingEl.textContent = '测速...'; pingEl.style.color = 'var(--status-syncing)';
try {
const calib = await probeSourceOnce(s, 3000);
pingEl.textContent = `${Math.round(calib.rtt)}ms`;
pingEl.style.color = 'var(--status-success)';
} catch (e) {
pingEl.textContent = e.message.includes('CORS') ? '跨域拦截' : '阻断/超时';
pingEl.style.color = 'var(--status-error)';
}
}
btn.disabled = false; btn.textContent = '测试所有源';
}
async function testLatencies() {
const resEl = document.getElementById('calib-result'); resEl.textContent = '连发探测中...'; resEl.style.color = 'var(--status-syncing)';
const btn = document.getElementById('test-calib-btn'); btn.disabled = true;
try {
const s = config.sources.find(x => x.id === actualUsingSourceId && x.id !== 'local') || config.sources[1];
let rtts = [];
for(let i=0; i<3; i++) { const c = await probeSourceOnce(s, 3000); rtts.push(c.rtt); await new Promise(r=>setTimeout(r,100)); }
resEl.innerHTML = `测速节点: <b>${s.name}</b><br>标准单次: <span style="color:#fff">${Math.round(rtts[0])}ms</span> | NTP极致: <span style="color:#4caf50">${Math.round(Math.min(...rtts))}ms</span> | 平滑均值: <span style="color:#ff9800">${Math.round(rtts.reduce((a,b)=>a+b,0)/3)}ms</span>`;
} catch(e) {
resEl.textContent = '探测失败:目标接口可能严格禁用了 CORS 或网络异常。'; resEl.style.color = 'var(--status-error)';
} finally { btn.disabled = false; }
}
/* ================= 渲染与 UI 控制 ================= */
function update() {
const now = isSynced ? (referenceRealTime + (performance.now() - referencePerformanceTime)) : Date.now();
const d = new Date(now + 28800000);
document.getElementById('date').textContent = `${d.getUTCFullYear()}年${String(d.getUTCMonth()+1).padStart(2,'0')}月${String(d.getUTCDate()).padStart(2,'0')}日`;
document.getElementById('main-time').textContent = `${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')}:${String(d.getUTCSeconds()).padStart(2,'0')}`;
document.getElementById('ms').textContent = `.${String(d.getUTCMilliseconds()).padStart(3,'0')}`;
requestAnimationFrame(update);
}
function adjustZoom(delta) { config.zoom = Math.min(Math.max(config.zoom + delta, 0.5), 2.0); document.documentElement.style.setProperty('--clock-scale', config.zoom); save(); }
function switchTab(index) { document.querySelectorAll('.tab-btn').forEach((b, i) => b.classList.toggle('active', i === index)); document.querySelectorAll('.tab-content').forEach((c, i) => c.classList.toggle('active', i === index)); }
function renderSources() {
const list = document.getElementById('source-list'); list.innerHTML = '';
config.sources.forEach(s => {
const item = document.createElement('div');
item.className = `source-item ${(!config.autoSwitch && s.id === config.activeSourceId) ? 'intent-selected' : ''} ${(s.id === actualUsingSourceId) ? 'actual-active' : ''}`;
item.onclick = () => { config.activeSourceId = s.id; config.autoSwitch = false; save(); renderSources(); syncNetworkTime(); };
item.innerHTML = `
<span class="source-name">${s.name}</span>
<span class="ping-result" id="ping-${s.id}"></span>
<span class="check-mark">✔</span>
${!s.isDefault ? `<button class="del-btn">删除</button>` : ''}
`;
list.appendChild(item);
});
document.getElementById('auto-switch-toggle').checked = config.autoSwitch;
}
function toggleAutoSwitch() { config.autoSwitch = document.getElementById('auto-switch-toggle').checked; save(); syncNetworkTime(); }
function changeCalibMode(mode) { config.calibration = mode; save(); syncNetworkTime(); }
function addSource() {
let url = document.getElementById('new-source-url').value.trim(); if(!url) return;
if(!url.startsWith('http')) url = 'http://' + url;
try { config.sources.push({ id: Date.now(), name: `自定义 (${new URL(url).hostname})`, url: url, isDefault: false }); document.getElementById('new-source-url').value = ''; save(); renderSources(); } catch(e) { alert('URL格式错误'); }
}
function applyTempTheme(theme) { document.body.className = theme + (config.zenMode ? ' minimal-mode' : ''); document.body.className += isMobileDevice ? ' device-mobile' : ' device-pc'; }
function save() { localStorage.setItem('clockConfigV10', JSON.stringify(config)); }
function closeModal() { document.getElementById('modal-overlay').classList.remove('show'); config.theme = document.getElementById('theme-select').value; save(); }
document.getElementById('settings-btn').onclick = () => { renderSources(); document.getElementById('modal-overlay').classList.add('show'); };
document.documentElement.style.setProperty('--clock-scale', config.zoom);
applyTempTheme(config.theme);
requestAnimationFrame(update);
syncNetworkTime();
setInterval(syncNetworkTime, 5 * 60 * 1000);
</script>
</body>
</html>