计算日期间隔计算器代码

image

https://tool.52ipc.top/DateTimeCalculator.html

F12 自取

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>日期时间计算器 - 精确计算日期间隔、推算目标日期、量化时间差异</title>
    <meta name="description" content="免费在线日期时间计算器,支持日期间隔计算、日期推算、时间差精确计算。可计算两个日期之间相差多少天、周、月、年,支持闰年判断、ISO 周数查询等功能。">
    <meta name="keywords" content="日期计算器,时间计算器,日期间隔计算,日期推算,时间差计算,天数计算,闰年查询,ISO 周数,工作日计算,日期工具">
    <meta name="author" content="DateTime Calculator">
    <meta name="robots" content="index, follow">


<!--    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">-->
    <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/7.0.1/css/all.min.css" rel="stylesheet">

    <style>
        :root {
            --bg: #0c0f14;
            --bg2: #141821;
            --card: #1a1f2e;
            --card-hover: #1f2538;
            --border: #2a3042;
            --fg: #e8ecf4;
            --fg-dim: #8892a8;
            --accent: #e8943a;
            --accent-glow: rgba(232, 148, 58, 0.25);
            --accent2: #3ae8b0;
            --accent2-glow: rgba(58, 232, 176, 0.15);
            --danger: #e85a5a;
            --radius: 14px;
            --radius-sm: 8px;
        }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Noto Sans SC', 'Space Grotesk', sans-serif;
            background: var(--bg); color: var(--fg);
            min-height: 100vh; overflow-x: hidden; position: relative;
        }

        /* 动态背景 */
        .bg-layer { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
        .bg-blob {
            position: absolute; border-radius: 50%; filter: blur(100px); opacity: 0.3;
            animation: blobFloat 22s ease-in-out infinite alternate;
        }
        .bg-blob:nth-child(1) { width: 500px; height: 500px; background: var(--accent); top: -15%; left: -10%; }
        .bg-blob:nth-child(2) { width: 400px; height: 400px; background: var(--accent2); bottom: -10%; right: -5%; animation-delay: -8s; opacity: 0.18; }
        .bg-blob:nth-child(3) { width: 300px; height: 300px; background: #6366f1; top: 50%; left: 60%; animation-delay: -15s; opacity: 0.12; }
        @keyframes blobFloat {
            0% { transform: translate(0,0) scale(1); }
            33% { transform: translate(40px,-30px) scale(1.05); }
            66% { transform: translate(-20px,20px) scale(0.95); }
            100% { transform: translate(30px,-10px) scale(1.02); }
        }
        .bg-grid {
            position: fixed; inset: 0; z-index: 0; pointer-events: none;
            background-image: linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px),
            linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px);
            background-size: 60px 60px;
        }

        .container { position: relative; z-index: 1; max-width: 960px; margin: 0 auto; padding: 30px 20px 60px; }

        /* 顶部 */
        header { text-align: center; margin-bottom: 36px; }
        .logo-row { display: flex; align-items: center; justify-content: center; gap: 14px; margin-bottom: 8px; }
        .logo-icon {
            width: 48px; height: 48px; border-radius: 12px;
            background: linear-gradient(135deg, var(--accent), #d47a20);
            display: flex; align-items: center; justify-content: center;
            font-size: 22px; color: #fff; box-shadow: 0 4px 20px var(--accent-glow);
        }
        header h1 {
            font-family: 'Space Grotesk', sans-serif; font-size: 32px; font-weight: 700;
            letter-spacing: -0.5px;
            background: linear-gradient(135deg, var(--fg), var(--accent));
            -webkit-background-clip: text; -webkit-text-fill-color: transparent;
        }
        header p { color: var(--fg-dim); font-size: 14px; margin-top: 4px; }

        /* 实时时钟 */
        .clock-bar {
            background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
            padding: 16px 24px; display: flex; align-items: center; justify-content: center;
            gap: 20px; margin-bottom: 28px; flex-wrap: wrap;
        }
        .clock-time {
            font-family: 'Space Grotesk', sans-serif; font-size: 36px; font-weight: 700;
            letter-spacing: 2px; color: var(--accent); text-shadow: 0 0 30px var(--accent-glow);
        }
        .clock-date { font-size: 15px; color: var(--fg-dim); }
        .clock-seconds { font-size: 18px; color: var(--accent2); font-weight: 500; min-width: 28px; text-align: center; }
        .clock-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: pulse 1s ease-in-out infinite; }
        @keyframes pulse { 0%,100%{ opacity:1; transform:scale(1); } 50%{ opacity:0.3; transform:scale(0.7); } }

        /* 标签导航 */
        .tabs {
            display: flex; gap: 6px; margin-bottom: 24px;
            background: var(--bg2); border-radius: 12px; padding: 5px; border: 1px solid var(--border);
        }
        .tab-btn {
            flex: 1; padding: 12px 16px; border: none; background: transparent;
            color: var(--fg-dim); font-size: 14px; font-weight: 500; border-radius: 9px;
            cursor: pointer; transition: all 0.3s; font-family: inherit;
            display: flex; align-items: center; justify-content: center; gap: 8px;
        }
        .tab-btn:hover { color: var(--fg); background: rgba(255,255,255,0.04); }
        .tab-btn.active { background: var(--card); color: var(--accent); box-shadow: 0 2px 12px rgba(0,0,0,0.3); }
        .tab-btn i { font-size: 15px; }

        /* 面板 */
        .panel { display: none; animation: fadeUp 0.35s ease; }
        .panel.active { display: block; }
        @keyframes fadeUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }

        /* 卡片 */
        .card {
            background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
            padding: 28px; margin-bottom: 20px; transition: border-color 0.3s, box-shadow 0.3s;
        }
        .card:hover { border-color: rgba(232,148,58,0.2); box-shadow: 0 4px 30px rgba(0,0,0,0.2); }
        .card-title { font-size: 16px; font-weight: 600; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }
        .card-title i { color: var(--accent); font-size: 18px; }

        /* 表单 */
        .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 14px; }
        .form-group { display: flex; flex-direction: column; gap: 6px; }
        .form-group.full { grid-column: 1 / -1; }
        .form-label { font-size: 12px; color: var(--fg-dim); font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }
        .form-input, .form-select {
            background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-sm);
            padding: 11px 14px; color: var(--fg); font-size: 14px; font-family: inherit;
            transition: border-color 0.25s, box-shadow 0.25s; outline: none; width: 100%;
        }
        .form-input:focus, .form-select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
        .form-select {
            cursor: pointer; appearance: none;
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%238892a8' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E");
            background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px;
        }
        .form-select option { background: var(--card); color: var(--fg); }

        /* 隐藏原生日期/时间输入 */
        .native-hidden { position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none; }

        /* ============ 自定义日期选择器 ============ */
        .dp-trigger {
            display: flex; align-items: center; gap: 10px;
            background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-sm);
            padding: 10px 14px; cursor: pointer; transition: border-color 0.25s, box-shadow 0.25s;
            user-select: none; position: relative;
        }
        .dp-trigger:hover { border-color: rgba(232,148,58,0.4); }
        .dp-trigger.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
        .dp-trigger i.fa-calendar { color: var(--accent); font-size: 16px; }
        .dp-trigger-text { flex: 1; font-size: 14px; color: var(--fg); }
        .dp-trigger-text.placeholder { color: var(--fg-dim); }
        .dp-trigger .fa-angle-down { color: var(--fg-dim); font-size: 12px; transition: transform 0.25s; }
        .dp-trigger.active .fa-angle-down { transform: rotate(180deg); }

        /* 时间触发器 */
        .tp-trigger {
            display: flex; align-items: center; gap: 10px;
            background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-sm);
            padding: 10px 14px; cursor: pointer; transition: border-color 0.25s, box-shadow 0.25s;
            user-select: none;
        }
        .tp-trigger:hover { border-color: rgba(232,148,58,0.4); }
        .tp-trigger.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
        .tp-trigger i.fa-clock { color: var(--accent2); font-size: 16px; }
        .tp-trigger-text { flex: 1; font-size: 14px; color: var(--fg); font-family: 'Space Grotesk', sans-serif; }
        .tp-trigger-text.placeholder { color: var(--fg-dim); font-family: inherit; }

        /* 弹出层 */
        .picker-overlay {
            position: fixed; inset: 0; z-index: 100; display: none;
        }
        .picker-overlay.open { display: block; }

        /* 日历面板 */
        .calendar-panel {
            position: absolute; z-index: 110;
            background: var(--card); border: 1px solid var(--border);
            border-radius: var(--radius); padding: 16px;
            box-shadow: 0 12px 48px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.03);
            min-width: 300px; display: none;
            animation: calFadeIn 0.2s ease;
        }
        .calendar-panel.open { display: block; }
        @keyframes calFadeIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }

        .cal-header {
            display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px;
        }
        .cal-header-title {
            font-family: 'Space Grotesk', sans-serif; font-size: 15px; font-weight: 600;
            color: var(--fg); cursor: pointer; padding: 4px 8px; border-radius: 6px;
            transition: background 0.2s;
        }
        .cal-header-title:hover { background: rgba(255,255,255,0.06); }
        .cal-nav {
            width: 32px; height: 32px; border: none; background: transparent;
            color: var(--fg-dim); border-radius: 8px; cursor: pointer;
            display: flex; align-items: center; justify-content: center;
            transition: all 0.2s; font-size: 14px;
        }
        .cal-nav:hover { background: rgba(255,255,255,0.08); color: var(--fg); }

        .cal-weekdays {
            display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 6px;
        }
        .cal-weekday {
            text-align: center; font-size: 11px; color: var(--fg-dim); padding: 4px 0;
            font-weight: 600; text-transform: uppercase;
        }
        .cal-weekday.weekend { color: rgba(232,148,58,0.5); }

        .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
        .cal-day {
            aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
            font-size: 13px; border-radius: 8px; cursor: pointer; transition: all 0.15s;
            color: var(--fg); border: none; background: transparent; font-family: 'Space Grotesk', sans-serif;
            position: relative;
        }
        .cal-day:hover { background: rgba(255,255,255,0.08); }
        .cal-day.other-month { color: var(--fg-dim); opacity: 0.35; }
        .cal-day.other-month:hover { opacity: 0.6; }
        .cal-day.today { color: var(--accent); font-weight: 700; }
        .cal-day.today::after {
            content: ''; position: absolute; bottom: 3px; left: 50%; transform: translateX(-50%);
            width: 4px; height: 4px; border-radius: 50%; background: var(--accent);
        }
        .cal-day.selected {
            background: var(--accent) !important; color: #fff !important; font-weight: 700;
            box-shadow: 0 2px 10px var(--accent-glow);
        }
        .cal-day.selected::after { display: none; }
        .cal-day.in-range { background: rgba(232,148,58,0.1); }
        .cal-day.range-start { border-radius: 8px 0 0 8px; }
        .cal-day.range-end { border-radius: 0 8px 8px 0; }
        .cal-day.range-start.range-end { border-radius: 8px; }
        .cal-day.weekend-day { color: rgba(232,148,58,0.7); }
        .cal-day.weekend-day.selected { color: #fff; }

        .cal-footer {
            display: flex; justify-content: space-between; align-items: center;
            margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);
        }
        .cal-today-btn {
            padding: 6px 14px; border: 1px solid var(--border); background: transparent;
            color: var(--fg-dim); border-radius: 6px; font-size: 12px; cursor: pointer;
            transition: all 0.2s; font-family: inherit;
        }
        .cal-today-btn:hover { border-color: var(--accent); color: var(--accent); }
        .cal-clear-btn {
            padding: 6px 14px; border: 1px solid var(--border); background: transparent;
            color: var(--fg-dim); border-radius: 6px; font-size: 12px; cursor: pointer;
            transition: all 0.2s; font-family: inherit;
        }
        .cal-clear-btn:hover { border-color: var(--danger); color: var(--danger); }

        /* 年月选择器 */
        .ym-panel {
            position: absolute; z-index: 115;
            background: var(--card); border: 1px solid var(--border);
            border-radius: var(--radius); padding: 16px;
            box-shadow: 0 12px 48px rgba(0,0,0,0.5);
            min-width: 300px; display: none;
            animation: calFadeIn 0.2s ease;
        }
        .ym-panel.open { display: block; }
        .ym-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
        .ym-header-title { font-family: 'Space Grotesk', sans-serif; font-size: 15px; font-weight: 600; }
        .ym-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
        .ym-cell {
            padding: 10px 8px; text-align: center; border-radius: 8px;
            cursor: pointer; font-size: 13px; color: var(--fg);
            border: 1px solid transparent; transition: all 0.15s; background: transparent;
            font-family: inherit;
        }
        .ym-cell:hover { background: rgba(255,255,255,0.06); border-color: var(--border); }
        .ym-cell.current { border-color: var(--accent); color: var(--accent); font-weight: 600; }
        .ym-cell.selected { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }

        /* ============ 时间选择器 ============ */
        .time-panel {
            position: absolute; z-index: 110;
            background: var(--card); border: 1px solid var(--border);
            border-radius: var(--radius); padding: 20px 16px;
            box-shadow: 0 12px 48px rgba(0,0,0,0.5);
            min-width: 240px; display: none;
            animation: calFadeIn 0.2s ease;
        }
        .time-panel.open { display: block; }
        .time-col-wrap { display: flex; gap: 6px; justify-content: center; align-items: center; }
        .time-col { display: flex; flex-direction: column; align-items: center; gap: 4px; }
        .time-col-label { font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 2px; }
        .time-arrow {
            width: 36px; height: 28px; border: none; background: transparent;
            color: var(--fg-dim); border-radius: 6px; cursor: pointer;
            display: flex; align-items: center; justify-content: center;
            transition: all 0.15s; font-size: 14px;
        }
        .time-arrow:hover { background: rgba(255,255,255,0.08); color: var(--fg); }
        .time-arrow:active { background: rgba(232,148,58,0.15); color: var(--accent); }
        .time-value {
            font-family: 'Space Grotesk', sans-serif; font-size: 32px; font-weight: 700;
            color: var(--fg); min-width: 48px; text-align: center; line-height: 1;
            padding: 4px 0;
        }
        .time-sep { font-family: 'Space Grotesk', sans-serif; font-size: 28px; font-weight: 700; color: var(--fg-dim); padding-top: 18px; }
        .time-footer { display: flex; justify-content: center; margin-top: 14px; gap: 8px; }
        .time-ok-btn {
            padding: 8px 24px; border: none; border-radius: 6px;
            background: var(--accent); color: #fff; font-size: 13px;
            font-weight: 600; cursor: pointer; font-family: inherit;
            transition: all 0.2s; box-shadow: 0 2px 10px var(--accent-glow);
        }
        .time-ok-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 16px var(--accent-glow); }
        .time-now-btn {
            padding: 8px 16px; border: 1px solid var(--border); background: transparent;
            color: var(--fg-dim); border-radius: 6px; font-size: 13px;
            cursor: pointer; font-family: inherit; transition: all 0.2s;
        }
        .time-now-btn:hover { border-color: var(--accent2); color: var(--accent2); }

        /* 交换按钮 */
        .swap-btn {
            display: flex; align-items: center; justify-content: center;
            width: 40px; height: 40px; border-radius: 50%;
            background: var(--bg2); border: 1px solid var(--border);
            color: var(--fg-dim); cursor: pointer; transition: all 0.3s;
            margin: 6px auto; font-size: 14px;
        }
        .swap-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); transform: rotate(180deg); }

        /* 计算按钮 */
        .calc-btn {
            display: flex; align-items: center; justify-content: center; gap: 8px;
            width: 100%; padding: 14px; border: none; border-radius: var(--radius-sm);
            background: linear-gradient(135deg, var(--accent), #d47a20);
            color: #fff; font-size: 15px; font-weight: 600; font-family: inherit;
            cursor: pointer; transition: all 0.3s;
            box-shadow: 0 4px 16px var(--accent-glow); margin-top: 20px;
        }
        .calc-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 24px var(--accent-glow); }
        .calc-btn:active { transform: translateY(0); }

        /* 结果 */
        .result-area { display: none; animation: fadeUp 0.4s ease; }
        .result-area.show { display: block; }
        .result-box {
            background: linear-gradient(135deg, rgba(232,148,58,0.08), rgba(58,232,176,0.05));
            border: 1px solid rgba(232,148,58,0.2); border-radius: var(--radius); padding: 24px; margin-top: 20px;
        }
        .result-main {
            font-family: 'Space Grotesk', sans-serif; font-size: 26px; font-weight: 700;
            color: var(--accent); margin-bottom: 16px; line-height: 1.3;
            text-shadow: 0 0 20px var(--accent-glow);
        }
        .result-details { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; }
        .result-item {
            background: rgba(0,0,0,0.2); border-radius: var(--radius-sm); padding: 14px;
            text-align: center; border: 1px solid rgba(255,255,255,0.04); transition: transform 0.2s, border-color 0.2s;
        }
        .result-item:hover { transform: translateY(-2px); border-color: rgba(232,148,58,0.3); }
        .result-value { font-family: 'Space Grotesk', sans-serif; font-size: 24px; font-weight: 700; color: var(--fg); display: block; }
        .result-label { font-size: 11px; color: var(--fg-dim); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }

        .result-date-display {
            font-family: 'Space Grotesk', sans-serif; font-size: 32px; font-weight: 700;
            background: linear-gradient(135deg, var(--accent), var(--accent2));
            -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 8px;
        }
        .result-weekday { font-size: 16px; color: var(--accent2); margin-bottom: 12px; }

        .time-result-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
        .time-result-item {
            text-align: center; padding: 16px 8px; background: rgba(0,0,0,0.25);
            border-radius: var(--radius-sm); border: 1px solid rgba(255,255,255,0.04);
        }
        .time-result-val { font-family: 'Space Grotesk', sans-serif; font-size: 32px; font-weight: 700; color: var(--fg); }
        .time-result-unit { font-size: 12px; color: var(--fg-dim); margin-top: 4px; }
        .time-total-row {
            margin-top: 8px; padding: 10px 16px; background: rgba(0,0,0,0.15);
            border-radius: var(--radius-sm); display: flex; justify-content: space-between;
            align-items: center; font-size: 13px; color: var(--fg-dim);
        }
        .time-total-row span:last-child {
            font-family: 'Space Grotesk', sans-serif; color: var(--accent2); font-weight: 600; font-size: 15px;
        }

        /* 快捷按钮 */
        .quick-btns { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
        .quick-btn {
            padding: 7px 14px; border: 1px solid var(--border); background: var(--bg2);
            color: var(--fg-dim); border-radius: 20px; font-size: 12px; cursor: pointer;
            transition: all 0.25s; font-family: inherit;
        }
        .quick-btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(232,148,58,0.08); }

        /* Toast */
        .toast {
            position: fixed; top: 20px; right: 20px; z-index: 999;
            background: var(--danger); color: #fff; padding: 12px 20px;
            border-radius: var(--radius-sm); font-size: 13px;
            box-shadow: 0 4px 20px rgba(232,90,90,0.3);
            transform: translateX(120%); transition: transform 0.35s ease;
        }
        .toast.show { transform: translateX(0); }

        /* 响应式 */
        @media (max-width: 600px) {
            header h1 { font-size: 24px; }
            .clock-time { font-size: 26px; }
            .tabs { flex-direction: column; }
            .tab-btn { justify-content: flex-start; padding: 10px 16px; }
            .result-main { font-size: 20px; }
            .result-date-display { font-size: 24px; }
            .time-result-grid { grid-template-columns: repeat(2, 1fr); }
            .form-grid { grid-template-columns: 1fr; }
            .card { padding: 20px; }
            .calendar-panel { min-width: 280px; }
            .cal-day { font-size: 12px; }
        }
        @media (prefers-reduced-motion: reduce) {
            *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
        }
    </style>
</head>
<body>

<div class="bg-layer" aria-hidden="true">
    <div class="bg-blob"></div><div class="bg-blob"></div><div class="bg-blob"></div>
</div>
<div class="bg-grid" aria-hidden="true"></div>
<div class="toast" id="toast" role="alert"></div>

<div class="container">
    <header>
        <div class="logo-row">
            <div class="logo-icon"><i class="fa-solid fa-calendar-days"></i></div>
            <h1>DateTime Calculator</h1>
        </div>
        <p>精确计算日期间隔、推算目标日期、量化时间差异</p>
    </header>

    <!-- 实时时钟 -->
    <div class="clock-bar" aria-label="实时时钟">
        <div>
            <div class="clock-date" id="clockDate"></div>
            <div style="display:flex;align-items:baseline;gap:4px;margin-top:2px;">
                <span class="clock-time" id="clockTime"></span>
                <span class="clock-seconds" id="clockSec"></span>
            </div>
        </div>
        <div class="clock-dot" aria-hidden="true"></div>
        <div style="text-align:right;">
            <div style="font-size:12px;color:var(--fg-dim);text-transform:uppercase;letter-spacing:1px;">星期</div>
            <div style="font-size:20px;font-weight:600;color:var(--accent2);margin-top:2px;" id="clockWeekday"></div>
        </div>
    </div>

    <!-- 标签导航 -->
    <nav class="tabs" role="tablist" aria-label="计算模式">
        <button class="tab-btn active" role="tab" data-tab="interval" aria-selected="true"><i class="fa-solid fa-arrows-left-right"></i>日期间隔</button>
        <button class="tab-btn" role="tab" data-tab="offset" aria-selected="false"><i class="fa-solid fa-forward"></i>日期推算</button>
        <button class="tab-btn" role="tab" data-tab="timediff" aria-selected="false"><i class="fa-solid fa-clock"></i>时间差计算</button>
    </nav>

    <!-- 面板1:日期间隔 -->
    <section class="panel active" id="panel-interval" role="tabpanel">
        <div class="card">
            <div class="card-title"><i class="fa-solid fa-ruler-horizontal"></i>计算两个日期之间的间隔</div>
            <div class="form-grid">
                <div class="form-group full">
                    <label class="form-label">起始日期</label>
                    <div class="dp-trigger" id="intStartTrigger" tabindex="0" role="combobox" aria-label="选择起始日期">
                        <i class="fa-solid fa-calendar"></i>
                        <span class="dp-trigger-text placeholder" id="intStartText">请选择日期</span>
                        <i class="fa-solid fa-angle-down"></i>
                    </div>
                </div>
            </div>
            <div style="display:flex;justify-content:center;">
                <button class="swap-btn" id="swapDates" aria-label="交换日期" title="交换日期"><i class="fa-solid fa-arrow-down-up-across-line"></i></button>
            </div>
            <div class="form-grid" style="margin-top:6px;">
                <div class="form-group full">
                    <label class="form-label">结束日期</label>
                    <div class="dp-trigger" id="intEndTrigger" tabindex="0" role="combobox" aria-label="选择结束日期">
                        <i class="fa-solid fa-calendar"></i>
                        <span class="dp-trigger-text placeholder" id="intEndText">请选择日期</span>
                        <i class="fa-solid fa-angle-down"></i>
                    </div>
                </div>
            </div>
            <div class="quick-btns">
                <button class="quick-btn" data-quick="today">今天</button>
                <button class="quick-btn" data-quick="yesterday">昨天</button>
                <button class="quick-btn" data-quick="tomorrow">明天</button>
                <button class="quick-btn" data-quick="thisMonthStart">本月第一天</button>
                <button class="quick-btn" data-quick="thisMonthEnd">本月最后一天</button>
                <button class="quick-btn" data-quick="thisYearStart">今年第一天</button>
                <button class="quick-btn" data-quick="thisYearEnd">今年最后一天</button>
            </div>
            <button class="calc-btn" id="calcInterval"><i class="fa-solid fa-calculator"></i>计算间隔</button>
        </div>
        <div class="result-area" id="resultInterval">
            <div class="result-box">
                <div class="result-main" id="intMain"></div>
                <div class="result-details" id="intDetails"></div>
                <div style="margin-top:16px;">
                    <div class="time-total-row"><span>总小时数</span><span id="intHours"></span></div>
                    <div class="time-total-row"><span>总分钟数</span><span id="intMinutes"></span></div>
                    <div class="time-total-row"><span>总秒数</span><span id="intSeconds"></span></div>
                </div>
            </div>
        </div>
    </section>

    <!-- 面板2:日期推算 -->
    <section class="panel" id="panel-offset" role="tabpanel">
        <div class="card">
            <div class="card-title"><i class="fa-solid fa-calendar-plus"></i>从指定日期推算目标日期</div>
            <div class="form-grid">
                <div class="form-group full">
                    <label class="form-label">基准日期</label>
                    <div class="dp-trigger" id="offsetBaseTrigger" tabindex="0" role="combobox" aria-label="选择基准日期">
                        <i class="fa-solid fa-calendar"></i>
                        <span class="dp-trigger-text placeholder" id="offsetBaseText">请选择日期</span>
                        <i class="fa-solid fa-angle-down"></i>
                    </div>
                </div>
                <div class="form-group">
                    <label class="form-label">方向</label>
                    <select class="form-select" id="offsetDir" aria-label="方向">
                        <option value="1">之后 (+)</option>
                        <option value="-1">之前 (-)</option>
                    </select>
                </div>
                <div class="form-group"><label class="form-label">年</label><input type="number" class="form-input" id="offsetY" value="0" min="0" aria-label="年数"></div>
                <div class="form-group"><label class="form-label">月</label><input type="number" class="form-input" id="offsetM" value="0" min="0" aria-label="月数"></div>
                <div class="form-group"><label class="form-label">周</label><input type="number" class="form-input" id="offsetW" value="0" min="0" aria-label="周数"></div>
                <div class="form-group"><label class="form-label">天</label><input type="number" class="form-input" id="offsetD" value="0" min="0" aria-label="天数"></div>
            </div>
            <div class="quick-btns">
                <button class="quick-btn" data-off="100d">100天后</button>
                <button class="quick-btn" data-off="-100d">100天前</button>
                <button class="quick-btn" data-off="1y">1年后</button>
                <button class="quick-btn" data-off="-1y">1年前</button>
                <button class="quick-btn" data-off="6m">6个月后</button>
                <button class="quick-btn" data-off="1w">1周后</button>
                <button class="quick-btn" data-off="1000d">1000天后</button>
            </div>
            <button class="calc-btn" id="calcOffset"><i class="fa-solid fa-calculator"></i>推算日期</button>
        </div>
        <div class="result-area" id="resultOffset">
            <div class="result-box">
                <div class="result-date-display" id="offDate"></div>
                <div class="result-weekday" id="offWeekday"></div>
                <div class="result-details" id="offDetails"></div>
            </div>
        </div>
    </section>

    <!-- 面板3:时间差计算 -->
    <section class="panel" id="panel-timediff" role="tabpanel">
        <div class="card">
            <div class="card-title"><i class="fa-solid fa-hourglass-half"></i>精确计算两个时间点的差异</div>
            <div class="form-grid">
                <div class="form-group">
                    <label class="form-label">起始日期</label>
                    <div class="dp-trigger" id="tdStartDTrigger" tabindex="0" role="combobox" aria-label="选择起始日期">
                        <i class="fa-solid fa-calendar"></i>
                        <span class="dp-trigger-text placeholder" id="tdStartDText">选择日期</span>
                        <i class="fa-solid fa-angle-down"></i>
                    </div>
                </div>
                <div class="form-group">
                    <label class="form-label">起始时间</label>
                    <div class="tp-trigger" id="tdStartTTrigger" tabindex="0" role="combobox" aria-label="选择起始时间">
                        <i class="fa-solid fa-clock"></i>
                        <span class="tp-trigger-text placeholder" id="tdStartTText">选择时间</span>
                    </div>
                </div>
                <div class="form-group">
                    <label class="form-label">结束日期</label>
                    <div class="dp-trigger" id="tdEndDTrigger" tabindex="0" role="combobox" aria-label="选择结束日期">
                        <i class="fa-solid fa-calendar"></i>
                        <span class="dp-trigger-text placeholder" id="tdEndDText">选择日期</span>
                        <i class="fa-solid fa-angle-down"></i>
                    </div>
                </div>
                <div class="form-group">
                    <label class="form-label">结束时间</label>
                    <div class="tp-trigger" id="tdEndTTrigger" tabindex="0" role="combobox" aria-label="选择结束时间">
                        <i class="fa-solid fa-clock"></i>
                        <span class="tp-trigger-text placeholder" id="tdEndTText">选择时间</span>
                    </div>
                </div>
            </div>
            <div class="quick-btns">
                <button class="quick-btn" data-td="nowToEnd">现在到今天结束</button>
                <button class="quick-btn" data-td="startToNow">今天开始到现在</button>
                <button class="quick-btn" data-td="now24h">过去24小时</button>
            </div>
            <button class="calc-btn" id="calcTimediff"><i class="fa-solid fa-calculator"></i>计算时间差</button>
        </div>
        <div class="result-area" id="resultTimediff">
            <div class="result-box">
                <div class="result-main" id="tdMain"></div>
                <div class="time-result-grid" id="tdGrid"></div>
                <div style="margin-top:14px;">
                    <div class="time-total-row"><span>总秒数</span><span id="tdTotalSec"></span></div>
                    <div class="time-total-row"><span>总分钟数</span><span id="tdTotalMin"></span></div>
                    <div class="time-total-row"><span>总小时数</span><span id="tdTotalHour"></span></div>
                    <div class="time-total-row"><span>总天数</span><span id="tdTotalDay"></span></div>
                </div>
            </div>
        </div>
    </section>
</div>

<!-- 全局弹窗遮罩 -->
<div class="picker-overlay" id="pickerOverlay"></div>

<!-- 日历面板(共享) -->
<div class="calendar-panel" id="calendarPanel">
    <div class="cal-header">
        <button class="cal-nav" id="calPrev" aria-label="上一月"><i class="fa-solid fa-chevron-left"></i></button>
        <span class="cal-header-title" id="calTitle" role="button" tabindex="0">2024年1月</span>
        <button class="cal-nav" id="calNext" aria-label="下一月"><i class="fa-solid fa-chevron-right"></i></button>
    </div>
    <div class="cal-weekdays" id="calWeekdays"></div>
    <div class="cal-days" id="calDays"></div>
    <div class="cal-footer">
        <button class="cal-today-btn" id="calTodayBtn">今天</button>
        <button class="cal-clear-btn" id="calClearBtn">清除</button>
    </div>
</div>

<!-- 年月选择面板 -->
<div class="ym-panel" id="ymPanel">
    <div class="ym-header">
        <button class="cal-nav" id="ymPrev" aria-label="上一年"><i class="fa-solid fa-chevron-left"></i></button>
        <span class="ym-header-title" id="ymTitle">2024年</span>
        <button class="cal-nav" id="ymNext" aria-label="下一年"><i class="fa-solid fa-chevron-right"></i></button>
    </div>
    <div class="ym-grid" id="ymGrid"></div>
</div>

<!-- 时间选择面板(共享) -->
<div class="time-panel" id="timePanel">
    <div class="time-col-wrap">
        <div class="time-col">
            <div class="time-col-label">时</div>
            <button class="time-arrow" data-dir="up" data-field="hour"><i class="fa-solid fa-chevron-up"></i></button>
            <div class="time-value" id="tpHour">00</div>
            <button class="time-arrow" data-dir="down" data-field="hour"><i class="fa-solid fa-chevron-down"></i></button>
        </div>
        <div class="time-sep">:</div>
        <div class="time-col">
            <div class="time-col-label">分</div>
            <button class="time-arrow" data-dir="up" data-field="min"><i class="fa-solid fa-chevron-up"></i></button>
            <div class="time-value" id="tpMin">00</div>
            <button class="time-arrow" data-dir="down" data-field="min"><i class="fa-solid fa-chevron-down"></i></button>
        </div>
        <div class="time-sep">:</div>
        <div class="time-col">
            <div class="time-col-label">秒</div>
            <button class="time-arrow" data-dir="up" data-field="sec"><i class="fa-solid fa-chevron-up"></i></button>
            <div class="time-value" id="tpSec">00</div>
            <button class="time-arrow" data-dir="down" data-field="sec"><i class="fa-solid fa-chevron-down"></i></button>
        </div>
    </div>
    <div class="time-footer">
        <button class="time-now-btn" id="tpNowBtn">当前时间</button>
        <button class="time-ok-btn" id="tpOkBtn">确定</button>
    </div>
</div>

<script>
    // ========== 工具 ==========
    const $ = s => document.querySelector(s);
    const $$ = s => document.querySelectorAll(s);
    const pad = n => String(n).padStart(2, '0');
    const WEEKDAYS = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
    const WEEKDAY_SHORT = ['日','一','二','三','四','五','六'];

    function showToast(msg) {
        const t = $('#toast'); t.textContent = msg; t.classList.add('show');
        clearTimeout(t._timer); t._timer = setTimeout(() => t.classList.remove('show'), 2800);
    }
    function formatDate(d) { return `${d.getFullYear()}年${pad(d.getMonth()+1)}月${pad(d.getDate())}日`; }
    function numFmt(n) { return n.toLocaleString('zh-CN'); }
    function dateStr(d) { return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; }
    function timeStr(d) { return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }
    function parseDateStr(s) { const p = s.split('-'); return new Date(+p[0], +p[1]-1, +p[2]); }
    function parseTimeStr(s) { const p = s.split(':'); return { h: +p[0]||0, m: +p[1]||0, s: +p[2]||0 }; }

    // ========== 实时时钟 ==========
    function updateClock() {
        const now = new Date();
        $('#clockDate').textContent = formatDate(now);
        $('#clockTime').textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
        $('#clockSec').textContent = pad(now.getSeconds());
        $('#clockWeekday').textContent = WEEKDAYS[now.getDay()];
    }
    updateClock(); setInterval(updateClock, 1000);

    // ========== 标签切换 ==========
    $$('.tab-btn').forEach(btn => {
        btn.addEventListener('click', () => {
            $$('.tab-btn').forEach(b => { b.classList.remove('active'); b.setAttribute('aria-selected','false'); });
            btn.classList.add('active'); btn.setAttribute('aria-selected','true');
            $$('.panel').forEach(p => p.classList.remove('active'));
            $(`#panel-${btn.dataset.tab}`).classList.add('active');
            closeAllPickers();
        });
    });

    // ========== 日期选择器类 ==========
    class DatePicker {
        constructor(triggerId, textId) {
            this.trigger = $(triggerId);
            this.textEl = $(textId);
            this.value = null; // "YYYY-MM-DD" 或 null
            this.onSelect = null;
            this._bindEvents();
        }

        _bindEvents() {
            this.trigger.addEventListener('click', (e) => { e.stopPropagation(); datePickerManager.open(this); });
            this.trigger.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); datePickerManager.open(this); } });
        }

        setValue(dateString) {
            this.value = dateString;
            if (dateString) {
                const d = parseDateStr(dateString);
                this.textEl.textContent = formatDate(d);
                this.textEl.classList.remove('placeholder');
            } else {
                this.textEl.textContent = this.trigger.closest('.panel')?.id === 'panel-timediff' ? '选择日期' : '请选择日期';
                this.textEl.classList.add('placeholder');
            }
        }

        getDate() { return this.value ? parseDateStr(this.value) : null; }
    }

    // ========== 时间选择器类 ==========
    class TimePicker {
        constructor(triggerId, textId) {
            this.trigger = $(triggerId);
            this.textEl = $(textId);
            this.hour = 0; this.min = 0; this.sec = 0;
            this.value = null; // "HH:MM:SS"
            this.onSelect = null;
            this._bindEvents();
        }

        _bindEvents() {
            this.trigger.addEventListener('click', (e) => { e.stopPropagation(); timePickerManager.open(this); });
            this.trigger.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); timePickerManager.open(this); } });
        }

        setValue(timeString) {
            this.value = timeString;
            if (timeString) {
                const t = parseTimeStr(timeString);
                this.hour = t.h; this.min = t.m; this.sec = t.s;
                this.textEl.textContent = timeString;
                this.textEl.classList.remove('placeholder');
            } else {
                this.textEl.textContent = '选择时间';
                this.textEl.classList.add('placeholder');
            }
        }

        setFromComponents() {
            this.value = `${pad(this.hour)}:${pad(this.min)}:${pad(this.sec)}`;
            this.textEl.textContent = this.value;
            this.textEl.classList.remove('placeholder');
        }
    }

    // ========== 日期选择器管理器 ==========
    const datePickerManager = {
        panel: $('#calendarPanel'),
        ymPanel: $('#ymPanel'),
        overlay: $('#pickerOverlay'),
        currentPicker: null,
        viewYear: 0,
        viewMonth: 0, // 0-based
        ymViewYear: 0,

        open(picker) {
            closeAllPickers();
            this.currentPicker = picker;
            picker.trigger.classList.add('active');

            if (picker.value) {
                const d = parseDateStr(picker.value);
                this.viewYear = d.getFullYear();
                this.viewMonth = d.getMonth();
            } else {
                const now = new Date();
                this.viewYear = now.getFullYear();
                this.viewMonth = now.getMonth();
            }

            this.overlay.classList.add('open');
            this._renderWeekdays();
            this._renderDays();
            this._positionPanel();
        },

        close() {
            if (this.currentPicker) this.currentPicker.trigger.classList.remove('active');
            this.currentPicker = null;
            this.panel.classList.remove('open');
            this.ymPanel.classList.remove('open');
            this.overlay.classList.remove('open');
        },

        _renderWeekdays() {
            const el = $('#calWeekdays');
            el.innerHTML = WEEKDAY_SHORT.map((w, i) =>
                `<div class="cal-weekday${(i===0||i===6)?' weekend':''}">${w}</div>`
            ).join('');
        },

        _renderDays() {
            $('#calTitle').textContent = `${this.viewYear}年${this.viewMonth + 1}月`;
            const el = $('#calDays');
            el.innerHTML = '';

            const firstDay = new Date(this.viewYear, this.viewMonth, 1);
            const lastDay = new Date(this.viewYear, this.viewMonth + 1, 0);
            const startDow = firstDay.getDay(); // 0=周日
            const daysInMonth = lastDay.getDate();

            // 上月填充
            const prevMonthLast = new Date(this.viewYear, this.viewMonth, 0).getDate();
            for (let i = startDow - 1; i >= 0; i--) {
                const day = prevMonthLast - i;
                const btn = document.createElement('button');
                btn.className = 'cal-day other-month';
                btn.textContent = day;
                btn.addEventListener('click', () => { this.viewMonth--; if (this.viewMonth < 0) { this.viewMonth = 11; this.viewYear--; } this._renderDays(); });
                el.appendChild(btn);
            }

            const todayStr = dateStr(new Date());
            const selectedStr = this.currentPicker?.value || '';

            for (let d = 1; d <= daysInMonth; d++) {
                const ds = `${this.viewYear}-${pad(this.viewMonth+1)}-${pad(d)}`;
                const btn = document.createElement('button');
                let cls = 'cal-day';
                if (ds === todayStr) cls += ' today';
                if (ds === selectedStr) cls += ' selected';
                const dow = new Date(this.viewYear, this.viewMonth, d).getDay();
                if (dow === 0 || dow === 6) cls += ' weekend-day';
                btn.className = cls;
                btn.textContent = d;
                btn.addEventListener('click', () => this._selectDay(ds));
                el.appendChild(btn);
            }

            // 下月填充
            const totalCells = startDow + daysInMonth;
            const remaining = (7 - totalCells % 7) % 7;
            for (let d = 1; d <= remaining; d++) {
                const btn = document.createElement('button');
                btn.className = 'cal-day other-month';
                btn.textContent = d;
                btn.addEventListener('click', () => { this.viewMonth++; if (this.viewMonth > 11) { this.viewMonth = 0; this.viewYear++; } this._renderDays(); });
                el.appendChild(btn);
            }
        },

        _selectDay(ds) {
            this.currentPicker.setValue(ds);
            if (this.currentPicker.onSelect) this.currentPicker.onSelect(ds);
            this.close();
        },

        _positionPanel() {
            const rect = this.currentPicker.trigger.getBoundingClientRect();
            const panel = this.panel;
            panel.classList.add('open');
            // 先显示以获取尺寸
            const pw = panel.offsetWidth;
            const ph = panel.offsetHeight;
            let left = rect.left;
            let top = rect.bottom + 6;
            // 防止超出右边
            if (left + pw > window.innerWidth - 10) left = window.innerWidth - pw - 10;
            if (left < 10) left = 10;
            // 防止超出底部
            if (top + ph > window.innerHeight - 10) {
                top = rect.top - ph - 6;
                if (top < 10) top = 10;
            }
            panel.style.position = 'fixed';
            panel.style.left = left + 'px';
            panel.style.top = top + 'px';
        },

        openYearMonth() {
            this.ymViewYear = this.viewYear;
            this._renderYM();
            const panelRect = this.panel.getBoundingClientRect();
            this.ymPanel.classList.add('open');
            this.ymPanel.style.position = 'fixed';
            this.ymPanel.style.left = panelRect.left + 'px';
            this.ymPanel.style.top = panelRect.top + 'px';
            this.ymPanel.style.width = panelRect.width + 'px';
        },

        closeYearMonth() { this.ymPanel.classList.remove('open'); },

        _renderYM() {
            $('#ymTitle').textContent = `${this.ymViewYear}年`;
            const el = $('#ymGrid');
            el.innerHTML = '';
            for (let m = 0; m < 12; m++) {
                const btn = document.createElement('button');
                btn.className = 'ym-cell';
                if (m === this.viewMonth && this.ymViewYear === this.viewYear) btn.className += ' current';
                btn.textContent = `${m + 1}月`;
                btn.addEventListener('click', () => {
                    this.viewYear = this.ymViewYear;
                    this.viewMonth = m;
                    this.closeYearMonth();
                    this._renderDays();
                    this._positionPanel();
                });
                el.appendChild(btn);
            }
        },

        prevMonth() {
            this.viewMonth--;
            if (this.viewMonth < 0) { this.viewMonth = 11; this.viewYear--; }
            this._renderDays();
        },

        nextMonth() {
            this.viewMonth++;
            if (this.viewMonth > 11) { this.viewMonth = 0; this.viewYear++; }
            this._renderDays();
        },

        selectToday() {
            this._selectDay(dateStr(new Date()));
        },

        clear() {
            this.currentPicker.setValue(null);
            this.close();
        }
    };

    // 日历导航事件
    $('#calPrev').addEventListener('click', (e) => { e.stopPropagation(); datePickerManager.prevMonth(); });
    $('#calNext').addEventListener('click', (e) => { e.stopPropagation(); datePickerManager.nextMonth(); });
    $('#calTitle').addEventListener('click', (e) => { e.stopPropagation(); datePickerManager.openYearMonth(); });
    $('#calTitle').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); datePickerManager.openYearMonth(); } });
    $('#calTodayBtn').addEventListener('click', (e) => { e.stopPropagation(); datePickerManager.selectToday(); });
    $('#calClearBtn').addEventListener('click', (e) => { e.stopPropagation(); datePickerManager.clear(); });
    $('#ymPrev').addEventListener('click', (e) => { e.stopPropagation(); datePickerManager.ymViewYear--; datePickerManager._renderYM(); });
    $('#ymNext').addEventListener('click', (e) => { e.stopPropagation(); datePickerManager.ymViewYear++; datePickerManager._renderYM(); });
    $('#pickerOverlay').addEventListener('click', closeAllPickers);

    // ========== 时间选择器管理器 ==========
    const timePickerManager = {
        panel: $('#timePanel'),
        currentPicker: null,

        open(picker) {
            closeAllPickers();
            this.currentPicker = picker;
            picker.trigger.classList.add('active');

            if (picker.value) {
                const t = parseTimeStr(picker.value);
                this._setValues(t.h, t.m, t.s);
            } else {
                this._setValues(0, 0, 0);
            }

            $('#pickerOverlay').classList.add('open');
            this._positionPanel();
        },

        close() {
            if (this.currentPicker) this.currentPicker.trigger.classList.remove('active');
            this.currentPicker = null;
            this.panel.classList.remove('open');
            $('#pickerOverlay').classList.remove('open');
        },

        _setValues(h, m, s) {
            this.currentPicker.hour = h;
            this.currentPicker.min = m;
            this.currentPicker.sec = s;
            $('#tpHour').textContent = pad(h);
            $('#tpMin').textContent = pad(m);
            $('#tpSec').textContent = pad(s);
        },

        confirm() {
            this.currentPicker.setFromComponents();
            if (this.currentPicker.onSelect) this.currentPicker.onSelect(this.currentPicker.value);
            this.close();
        },

        setNow() {
            const now = new Date();
            this._setValues(now.getHours(), now.getMinutes(), now.getSeconds());
        },

        adjust(field, dir) {
            const p = this.currentPicker;
            if (field === 'hour') {
                p.hour = (p.hour + (dir === 'up' ? 1 : -1) + 24) % 24;
            } else if (field === 'min') {
                p.min = (p.min + (dir === 'up' ? 1 : -1) + 60) % 60;
            } else {
                p.sec = (p.sec + (dir === 'up' ? 1 : -1) + 60) % 60;
            }
            $('#tpHour').textContent = pad(p.hour);
            $('#tpMin').textContent = pad(p.min);
            $('#tpSec').textContent = pad(p.sec);
        },

        _positionPanel() {
            const rect = this.currentPicker.trigger.getBoundingClientRect();
            const panel = this.panel;
            panel.classList.add('open');
            const pw = panel.offsetWidth;
            const ph = panel.offsetHeight;
            let left = rect.left;
            let top = rect.bottom + 6;
            if (left + pw > window.innerWidth - 10) left = window.innerWidth - pw - 10;
            if (left < 10) left = 10;
            if (top + ph > window.innerHeight - 10) {
                top = rect.top - ph - 6;
                if (top < 10) top = 10;
            }
            panel.style.position = 'fixed';
            panel.style.left = left + 'px';
            panel.style.top = top + 'px';
        }
    };

    // 时间选择器事件
    $$('.time-arrow').forEach(btn => {
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            timePickerManager.adjust(btn.dataset.field, btn.dataset.dir);
        });
    });
    $('#tpOkBtn').addEventListener('click', (e) => { e.stopPropagation(); timePickerManager.confirm(); });
    $('#tpNowBtn').addEventListener('click', (e) => { e.stopPropagation(); timePickerManager.setNow(); });

    // 阻止面板内点击冒泡关闭
    $('#calendarPanel').addEventListener('click', e => e.stopPropagation());
    $('#ymPanel').addEventListener('click', e => e.stopPropagation());
    $('#timePanel').addEventListener('click', e => e.stopPropagation());

    // ESC 关闭
    document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAllPickers(); });

    function closeAllPickers() {
        datePickerManager.close();
        datePickerManager.closeYearMonth();
        timePickerManager.close();
    }

    // ========== 创建选择器实例 ==========
    const intStartPicker = new DatePicker('#intStartTrigger', '#intStartText');
    const intEndPicker = new DatePicker('#intEndTrigger', '#intEndText');
    const offsetBasePicker = new DatePicker('#offsetBaseTrigger', '#offsetBaseText');
    const tdStartDPicker = new DatePicker('#tdStartDTrigger', '#tdStartDText');
    const tdEndDPicker = new DatePicker('#tdEndDTrigger', '#tdEndDText');
    const tdStartTPicker = new TimePicker('#tdStartTTrigger', '#tdStartTText');
    const tdEndTPicker = new TimePicker('#tdEndTTrigger', '#tdEndTText');

    // 设置默认值
    const todayS = dateStr(new Date());
    intStartPicker.setValue(todayS);
    intEndPicker.setValue(todayS);
    offsetBasePicker.setValue(todayS);
    tdStartDPicker.setValue(todayS);
    tdEndDPicker.setValue(todayS);
    tdStartTPicker.setValue('00:00:00');
    tdEndTPicker.setValue(timeStr(new Date()));

    // ========== 日期间隔计算 ==========
    $('#calcInterval').addEventListener('click', () => {
        if (!intStartPicker.value || !intEndPicker.value) { showToast('请选择起始和结束日期'); return; }
        let s = intStartPicker.getDate(), e = intEndPicker.getDate();
        let reversed = false;
        if (s > e) { [s, e] = [e, s]; reversed = true; }

        let years = e.getFullYear() - s.getFullYear();
        let months = e.getMonth() - s.getMonth();
        let days = e.getDate() - s.getDate();
        if (days < 0) { months--; days += new Date(e.getFullYear(), e.getMonth(), 0).getDate(); }
        if (months < 0) { years--; months += 12; }

        const totalMs = e - s;
        const totalDays = Math.round(totalMs / 86400000);
        const weeks = Math.floor(totalDays / 7);
        const remainDays = totalDays % 7;

        let parts = [];
        if (years > 0) parts.push(`${years} 年`);
        if (months > 0) parts.push(`${months} 个月`);
        if (days > 0) parts.push(`${days} 天`);
        if (!parts.length) parts.push('0 天');

        $('#intMain').textContent = `${parts.join(' ')}${reversed ? '(已自动交换顺序)' : ''}`;
        $('#intDetails').innerHTML = `
    <div class="result-item"><span class="result-value">${years}</span><span class="result-label">年</span></div>
    <div class="result-item"><span class="result-value">${months}</span><span class="result-label">月</span></div>
    <div class="result-item"><span class="result-value">${days}</span><span class="result-label">天</span></div>
    <div class="result-item"><span class="result-value">${weeks}</span><span class="result-label">周零${remainDays}天</span></div>
    <div class="result-item"><span class="result-value">${numFmt(totalDays)}</span><span class="result-label">总天数</span></div>`;
        $('#intHours').textContent = numFmt(Math.round(totalMs / 3600000));
        $('#intMinutes').textContent = numFmt(Math.round(totalMs / 60000));
        $('#intSeconds').textContent = numFmt(Math.round(totalMs / 1000));
        $('#resultInterval').classList.add('show');
    });

    // 交换
    $('#swapDates').addEventListener('click', () => {
        const tmp = intStartPicker.value;
        intStartPicker.setValue(intEndPicker.value);
        intEndPicker.setValue(tmp);
    });

    // 快捷日期
    $$('[data-quick]').forEach(btn => {
        btn.addEventListener('click', () => {
            const now = new Date();
            let d;
            switch(btn.dataset.quick) {
                case 'today': d = now; break;
                case 'yesterday': d = new Date(now - 86400000); break;
                case 'tomorrow': d = new Date(now.getTime() + 86400000); break;
                case 'thisMonthStart': d = new Date(now.getFullYear(), now.getMonth(), 1); break;
                case 'thisMonthEnd': d = new Date(now.getFullYear(), now.getMonth() + 1, 0); break;
                case 'thisYearStart': d = new Date(now.getFullYear(), 0, 1); break;
                case 'thisYearEnd': d = new Date(now.getFullYear(), 11, 31); break;
            }
            intEndPicker.setValue(dateStr(d));
        });
    });

    // ========== 日期推算 ==========
    $('#calcOffset').addEventListener('click', () => {
        if (!offsetBasePicker.value) { showToast('请选择基准日期'); return; }
        const base = offsetBasePicker.getDate();
        const dir = parseInt($('#offsetDir').value);
        const y = parseInt($('#offsetY').value) || 0;
        const m = parseInt($('#offsetM').value) || 0;
        const w = parseInt($('#offsetW').value) || 0;
        const d = parseInt($('#offsetD').value) || 0;
        if (!y && !m && !w && !d) { showToast('请输入要推算的时间量'); return; }

        const result = new Date(base);
        result.setFullYear(result.getFullYear() + y * dir);
        result.setMonth(result.getMonth() + m * dir);
        result.setDate(result.getDate() + (w * 7 + d) * dir);

        const today = new Date(); today.setHours(0,0,0,0);
        const diffDays = Math.round((result - today) / 86400000);
        const diffText = diffDays > 0 ? `${diffDays} 天后` : diffDays < 0 ? `${Math.abs(diffDays)} 天前` : '就是今天';

        $('#offDate').textContent = formatDate(result);
        $('#offWeekday').textContent = WEEKDAYS[result.getDay()] + ` · 距今 ${diffText}`;

        const dayOfYear = Math.ceil((result - new Date(result.getFullYear(), 0, 1)) / 86400000) + 1;
        const isLeap = (result.getFullYear() % 4 === 0 && result.getFullYear() % 100 !== 0) || result.getFullYear() % 400 === 0;
        const jan1 = new Date(result.getFullYear(), 0, 1);
        const isoWeek = Math.ceil((Math.floor((result - jan1) / 86400000) + 1 + jan1.getDay()) / 7);

        $('#offDetails').innerHTML = `
    <div class="result-item"><span class="result-value">${dayOfYear}</span><span class="result-label">年中第几天</span></div>
    <div class="result-item"><span class="result-value">${(isLeap?366:365) - dayOfYear}</span><span class="result-label">距年末天数</span></div>
    <div class="result-item"><span class="result-value">${isLeap?'是':'否'}</span><span class="result-label">闰年</span></div>
    <div class="result-item"><span class="result-value">Q${Math.ceil((result.getMonth()+1)/3)}</span><span class="result-label">季度</span></div>
    <div class="result-item"><span class="result-value">W${isoWeek}</span><span class="result-label">ISO周数</span></div>`;
        $('#resultOffset').classList.add('show');
    });

    $$('[data-off]').forEach(btn => {
        btn.addEventListener('click', () => {
            const match = btn.dataset.off.match(/^(-?\d+)([ymwd])$/);
            if (!match) return;
            const num = parseInt(match[1]), unit = match[2];
            $('#offsetY').value = 0; $('#offsetM').value = 0; $('#offsetW').value = 0; $('#offsetD').value = 0;
            $('#offsetDir').value = num >= 0 ? '1' : '-1';
            const abs = Math.abs(num);
            if (unit==='y') $('#offsetY').value = abs;
            else if (unit==='m') $('#offsetM').value = abs;
            else if (unit==='w') $('#offsetW').value = abs;
            else $('#offsetD').value = abs;
            $('#calcOffset').click();
        });
    });

    // ========== 时间差计算 ==========
    $('#calcTimediff').addEventListener('click', () => {
        if (!tdStartDPicker.value || !tdStartTPicker.value || !tdEndDPicker.value || !tdEndTPicker.value) {
            showToast('请选择完整的日期和时间'); return;
        }
        let s = new Date(`${tdStartDPicker.value}T${tdStartTPicker.value}`);
        let e = new Date(`${tdEndDPicker.value}T${tdEndTPicker.value}`);
        let reversed = false;
        if (s > e) { [s, e] = [e, s]; reversed = true; }

        const diffMs = e - s;
        const totalSec = Math.floor(diffMs / 1000);
        const days = Math.floor(totalSec / 86400);
        const hours = Math.floor((totalSec % 86400) / 3600);
        const minutes = Math.floor((totalSec % 3600) / 60);
        const seconds = totalSec % 60;

        let parts = [];
        if (days > 0) parts.push(`${days} 天`);
        if (hours > 0) parts.push(`${hours} 小时`);
        if (minutes > 0) parts.push(`${minutes} 分钟`);
        if (seconds > 0 || !parts.length) parts.push(`${seconds} 秒`);
        $('#tdMain').textContent = `${parts.join(' ')}${reversed ? '(已自动交换顺序)' : ''}`;

        $('#tdGrid').innerHTML = `
    <div class="time-result-item"><div class="time-result-val">${pad(days)}</div><div class="time-result-unit">天</div></div>
    <div class="time-result-item"><div class="time-result-val">${pad(hours)}</div><div class="time-result-unit">时</div></div>
    <div class="time-result-item"><div class="time-result-val">${pad(minutes)}</div><div class="time-result-unit">分</div></div>
    <div class="time-result-item"><div class="time-result-val">${pad(seconds)}</div><div class="time-result-unit">秒</div></div>`;
        $('#tdTotalSec').textContent = numFmt(totalSec) + ' 秒';
        $('#tdTotalMin').textContent = numFmt(Math.floor(diffMs / 60000)) + ' 分钟';
        $('#tdTotalHour').textContent = numFmt(Math.floor(diffMs / 3600000)) + ' 小时';
        $('#tdTotalDay').textContent = numFmt(Math.floor(diffMs / 86400000)) + ' 天';
        $('#resultTimediff').classList.add('show');
    });

    $$('[data-td]').forEach(btn => {
        btn.addEventListener('click', () => {
            const now = new Date();
            const ts = dateStr(now);
            switch(btn.dataset.td) {
                case 'nowToEnd':
                    tdStartDPicker.setValue(ts); tdStartTPicker.setValue(timeStr(now));
                    tdEndDPicker.setValue(ts); tdEndTPicker.setValue('23:59:59');
                    break;
                case 'startToNow':
                    tdStartDPicker.setValue(ts); tdStartTPicker.setValue('00:00:00');
                    tdEndDPicker.setValue(ts); tdEndTPicker.setValue(timeStr(now));
                    break;
                case 'now24h':
                    const ago = new Date(now - 86400000);
                    tdStartDPicker.setValue(dateStr(ago)); tdStartTPicker.setValue(timeStr(ago));
                    tdEndDPicker.setValue(ts); tdEndTPicker.setValue(timeStr(now));
                    break;
            }
            $('#calcTimediff').click();
        });
    });

    // 回车触发计算
    document.addEventListener('keydown', e => {
        if (e.key !== 'Enter') return;
        const panel = document.activeElement?.closest('.panel');
        if (!panel) return;
        if (panel.id === 'panel-interval') $('#calcInterval').click();
        else if (panel.id === 'panel-offset') $('#calcOffset').click();
        else if (panel.id === 'panel-timediff') $('#calcTimediff').click();
    });
</script>
</body>
</html>

posted @ 2026-04-01 15:24  Micky233  阅读(31)  评论(0)    收藏  举报