小组件 养鸡厂 信息查询

经验创意 · 18 次浏览
我的梦想捐钱修路建学校 创建于 4小时10分钟前

GPT老师,改自Scriptable脚本。

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
  <title>鸡厂订阅信息</title>
  <style>
    :root{
      --bg0:#06101c;
      --bg1:#0b1627;
      --bg2:#0d1a2f;
      --line:rgba(167, 196, 255, .18);
      --line2:rgba(255,255,255,.07);
      --text:#edf4ff;
      --muted:rgba(237,244,255,.66);
      --muted2:rgba(237,244,255,.46);
      --accent:#78adff;
      --accent2:#77f0ff;
      --good:#52e0bf;
      --warn:#ffcc66;
      --bad:#ff7e8d;
      --shadow:0 22px 54px rgba(0,0,0,.34);
      --r:18px;
    }

    *{box-sizing:border-box}
    html,body{
      margin:0;
      width:100%;
      height:100%;
      overflow:hidden;
      background:transparent;
    }
    body{
      font-family:"Microsoft YaHei", sans-serif;
      color:var(--text);
      -webkit-font-smoothing:antialiased;
      -moz-osx-font-smoothing:grayscale;
      user-select:none;
    }

    .card{
      width:100%;
      height:100%;
      position:relative;
      overflow:hidden;
      border-radius:18px;
      box-sizing:border-box;
      border:1px solid var(--line);
      background:
        radial-gradient(120% 120% at 0% 0%, rgba(120,173,255,.18) 0%, rgba(120,173,255,0) 42%),
        radial-gradient(110% 110% at 100% 0%, rgba(119,240,255,.11) 0%, rgba(119,240,255,0) 44%),
        linear-gradient(180deg, rgba(13,26,47,.98) 0%, rgba(7,16,28,.98) 100%);
      box-shadow:var(--shadow), inset 0 1px 0 rgba(255,255,255,.05);
      backdrop-filter: blur(12px);
    }
    .card::before{
      content:"";
      position:absolute;
      inset:0;
      pointer-events:none;
      border-radius:18px;
      background:linear-gradient(180deg, rgba(255,255,255,.09) 0%, rgba(255,255,255,0) 16%);
      opacity:.55;
    }

    .shell{
      position:absolute;
      inset:0;
      padding:12px;
      display:flex;
      flex-direction:column;
      gap:10px;
      min-height:0;
    }

    .topbar{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:8px;
      min-width:0;
    }
    .titleWrap{
      display:flex;
      align-items:center;
      gap:10px;
      min-width:0;
    }
    .badge{
      width:11px;
      height:11px;
      border-radius:50%;
      flex:0 0 auto;
      background:linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%);
      box-shadow:0 0 0 4px rgba(120,173,255,.12);
    }
    .titleBlock{
      min-width:0;
      display:flex;
      flex-direction:column;
      gap:2px;
    }
    .title{
      font-size:16px;
      font-weight:800;
      line-height:1.15;
      letter-spacing:.2px;
      white-space:nowrap;
      overflow:hidden;
      text-overflow:ellipsis;
    }
    .subtitle{
      font-size:11px;
      color:var(--muted);
      line-height:1.2;
      white-space:nowrap;
      overflow:hidden;
      text-overflow:ellipsis;
    }
    .topActions{
      display:flex;
      gap:8px;
      align-items:center;
      flex:0 0 auto;
    }
    .statusPill{
      max-width:120px;
      padding:7px 10px;
      border-radius:999px;
      border:1px solid var(--line2);
      background:rgba(255,255,255,.04);
      color:var(--muted);
      font-size:11px;
      white-space:nowrap;
      overflow:hidden;
      text-overflow:ellipsis;
    }

    .btn{
      appearance:none;
      border:none;
      outline:none;
      cursor:pointer;
      height:31px;
      padding:0 12px;
      border-radius:12px;
      font-size:12px;
      font-weight:700;
      letter-spacing:.1px;
      color:var(--text);
      border:1px solid rgba(120,173,255,.25);
      background:linear-gradient(180deg, rgba(120,173,255,.22), rgba(120,173,255,.10));
      box-shadow:inset 0 1px 0 rgba(255,255,255,.08);
      transition:transform .12s ease, border-color .12s ease, background .12s ease, opacity .12s ease;
    }
    .btn:hover{
      border-color:rgba(120,173,255,.38);
      background:linear-gradient(180deg, rgba(120,173,255,.30), rgba(120,173,255,.14));
    }
    .btn:active{
      transform:translateY(1px) scale(.99);
    }
    .btn.secondary{
      color:var(--muted);
      border-color:rgba(255,255,255,.08);
      background:rgba(255,255,255,.045);
    }
    .btn.secondary:hover{
      color:var(--text);
      border-color:rgba(255,255,255,.13);
      background:rgba(255,255,255,.065);
    }
    .btn.danger{
      border-color:rgba(255,126,141,.22);
      background:linear-gradient(180deg, rgba(255,126,141,.18), rgba(255,126,141,.08));
    }
    .btn.danger:hover{
      border-color:rgba(255,126,141,.36);
      background:linear-gradient(180deg, rgba(255,126,141,.24), rgba(255,126,141,.12));
    }

    .main{
      flex:1 1 auto;
      min-height:0;
      display:grid;
      grid-template-columns: 128px minmax(0,1fr);
      gap:10px;
      transition:opacity .14s ease, transform .14s ease, filter .14s ease;
    }
    body.menu-open .main{
      opacity:0;
      pointer-events:none;
      transform:scale(.985);
      filter:blur(1px);
    }

    .hero{
      border-radius:16px;
      border:1px solid rgba(255,255,255,.08);
      background:linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.028));
      box-shadow:inset 0 1px 0 rgba(255,255,255,.04);
      padding:12px 10px;
      display:flex;
      flex-direction:column;
      align-items:center;
      justify-content:center;
      min-height:0;
    }

    .meter{
      width:104px;
      aspect-ratio:1;
      border-radius:50%;
      position:relative;
      display:grid;
      place-items:center;
      background:conic-gradient(from 180deg, var(--good) 0deg, var(--accent2) 102deg, rgba(255,255,255,.10) 102deg 360deg);
      box-shadow:0 0 0 1px rgba(255,255,255,.06), inset 0 0 18px rgba(0,0,0,.25);
      flex:0 0 auto;
    }
    .meter::before{
      content:"";
      position:absolute;
      inset:10px;
      border-radius:50%;
      background:linear-gradient(180deg, rgba(11,22,39,.98), rgba(6,14,24,.98));
      box-shadow:inset 0 1px 0 rgba(255,255,255,.06);
    }
    .meter::after{
      content:"";
      position:absolute;
      inset:0;
      border-radius:50%;
      background:radial-gradient(circle at 30% 24%, rgba(255,255,255,.18), rgba(255,255,255,0) 54%);
      pointer-events:none;
    }
    .meterInner{
      position:relative;
      z-index:1;
      width:72px;
      text-align:center;
      display:flex;
      flex-direction:column;
      align-items:center;
      gap:2px;
    }
    .meterValue{
      font-size:21px;
      line-height:1;
      font-weight:900;
      letter-spacing:.2px;
    }
    .meterLabel{
      font-size:11px;
      color:var(--muted);
      line-height:1.2;
    }
    .meterMeta{
      margin-top:9px;
      width:100%;
      display:flex;
      flex-direction:column;
      gap:4px;
      align-items:center;
    }
    .tiny{
      width:100%;
      text-align:center;
      font-size:11px;
      line-height:1.35;
      color:var(--muted);
      word-break:break-word;
    }

    .right{
      min-width:0;
      min-height:0;
      display:flex;
      flex-direction:column;
      gap:8px;
      justify-content:flex-start;
    }

    .summary{
      display:flex;
      flex-direction:column;
      gap:7px;
      min-width:0;
      flex:1 1 auto;
    }
    .stat{
      border-radius:14px;
      padding:9px 9px 8px;
      border:1px solid rgba(255,255,255,.07);
      background:linear-gradient(180deg, rgba(255,255,255,.045), rgba(255,255,255,.025));
      min-width:0;
    }
    .stat .k{
      font-size:11px;
      color:var(--muted);
      line-height:1.2;
      margin-bottom:4px;
    }
    .stat .v{
      font-size:16px;
      font-weight:900;
      line-height:1.08;
      white-space:nowrap;
      overflow:hidden;
      text-overflow:ellipsis;
      letter-spacing:.1px;
    }
    .stat .s{
      margin-top:4px;
      font-size:10px;
      color:var(--muted2);
      line-height:1.2;
      white-space:nowrap;
      overflow:hidden;
      text-overflow:ellipsis;
    }

    .foot{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:8px;
      font-size:10px;
      line-height:1.2;
      color:var(--muted2);
      padding:0 2px;
      min-width:0;
    }
    .foot strong{
      color:var(--muted);
      font-weight:700;
    }

    .toast{
      position:absolute;
      left:50%;
      bottom:12px;
      transform:translateX(-50%) translateY(10px);
      opacity:0;
      pointer-events:none;
      padding:8px 11px;
      border-radius:999px;
      border:1px solid rgba(255,255,255,.10);
      background:rgba(7,13,23,.88);
      color:var(--text);
      font-size:11px;
      line-height:1.2;
      box-shadow:0 10px 26px rgba(0,0,0,.24);
      transition:opacity .16s ease, transform .16s ease;
      max-width:calc(100% - 24px);
      white-space:nowrap;
      overflow:hidden;
      text-overflow:ellipsis;
    }
    .toast.show{
      opacity:1;
      transform:translateX(-50%) translateY(0);
    }

    .menuMask{
      position:absolute;
      inset:0;
      background:rgba(2,7,14,.42);
      opacity:0;
      pointer-events:none;
      transition:opacity .16s ease;
    }
    .menuMask.show{
      opacity:1;
      pointer-events:auto;
    }
    .menuPanel{
      position:absolute;
      left:12px;
      right:12px;
      top:50px;
      bottom:12px;
      border-radius:18px;
      overflow:hidden;
      border:1px solid rgba(167,196,255,.18);
      background:
        radial-gradient(120% 120% at 0% 0%, rgba(120,173,255,.14) 0%, rgba(120,173,255,0) 42%),
        linear-gradient(180deg, rgba(11,22,39,.99), rgba(7,13,23,.99));
      box-shadow:0 20px 50px rgba(0,0,0,.44), inset 0 1px 0 rgba(255,255,255,.04);
      transform:translateY(-8px) scale(.985);
      opacity:0;
      pointer-events:none;
      transition:opacity .16s ease, transform .16s ease;
      display:flex;
      flex-direction:column;
      min-height:0;
    }
    .menuMask.show .menuPanel{
      opacity:1;
      pointer-events:auto;
      transform:translateY(0) scale(1);
    }

    .menuHeader{
      padding:12px;
      border-bottom:1px solid rgba(255,255,255,.07);
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:10px;
      flex:0 0 auto;
    }
    .tabs{
      display:flex;
      gap:8px;
      flex-wrap:wrap;
      min-width:0;
    }
    .tab{
      height:30px;
      padding:0 12px;
      border-radius:999px;
      border:1px solid rgba(255,255,255,.08);
      background:rgba(255,255,255,.04);
      color:var(--muted);
      font-size:12px;
      font-weight:700;
      cursor:pointer;
      transition:background .12s ease, border-color .12s ease, color .12s ease, transform .12s ease;
    }
    .tab:hover{
      color:var(--text);
      background:rgba(255,255,255,.07);
    }
    .tab.active{
      color:var(--text);
      border-color:rgba(120,173,255,.34);
      background:linear-gradient(180deg, rgba(120,173,255,.24), rgba(120,173,255,.12));
    }

    .menuBody{
      flex:1 1 auto;
      min-height:0;
      overflow:auto;
      padding:12px;
    }
    .menuBody::-webkit-scrollbar,
    .list::-webkit-scrollbar,
    .debugText::-webkit-scrollbar{
      width:8px;
      height:8px;
    }
    .menuBody::-webkit-scrollbar-track,
    .list::-webkit-scrollbar-track,
    .debugText::-webkit-scrollbar-track{
      background:rgba(255,255,255,.03);
      border-radius:999px;
    }
    .menuBody::-webkit-scrollbar-thumb,
    .list::-webkit-scrollbar-thumb,
    .debugText::-webkit-scrollbar-thumb{
      background:rgba(154,188,255,.22);
      border:1px solid rgba(255,255,255,.08);
      border-radius:999px;
    }
    .menuBody::-webkit-scrollbar-thumb:hover,
    .list::-webkit-scrollbar-thumb:hover,
    .debugText::-webkit-scrollbar-thumb:hover{
      background:rgba(154,188,255,.32);
    }

    .section{
      display:none;
      flex-direction:column;
      gap:10px;
      min-height:0;
    }
    .section.active{display:flex;}

    .form{
      display:grid;
      gap:8px;
      padding:10px;
      border-radius:16px;
      background:linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.025));
      border:1px solid rgba(255,255,255,.07);
    }
    .row2{
      display:grid;
      grid-template-columns:1fr 1fr;
      gap:8px;
    }
    .field{
      display:flex;
      flex-direction:column;
      gap:5px;
      min-width:0;
    }
    .label{
      font-size:11px;
      color:var(--muted);
      line-height:1.2;
    }
    input{
      width:100%;
      height:34px;
      border-radius:11px;
      border:1px solid rgba(255,255,255,.09);
      background:rgba(4,11,20,.36);
      color:var(--text);
      padding:0 10px;
      outline:none;
      font-size:12px;
      min-width:0;
      transition:border-color .12s ease, background .12s ease, box-shadow .12s ease;
      user-select:text;
    }
    input::placeholder{color:rgba(237,244,255,.34)}
    input:hover{
      border-color:rgba(255,255,255,.14);
      background:rgba(4,11,20,.42);
    }
    input:focus{
      border-color:rgba(120,173,255,.45);
      box-shadow:0 0 0 3px rgba(120,173,255,.12);
      background:rgba(4,11,20,.5);
    }

    .buttonRow{
      display:flex;
      flex-wrap:wrap;
      gap:8px;
      align-items:center;
    }
    .listWrap{
      flex:1 1 auto;
      min-height:0;
      display:flex;
      flex-direction:column;
      gap:8px;
      padding:10px;
      border-radius:16px;
      background:linear-gradient(180deg, rgba(255,255,255,.035), rgba(255,255,255,.02));
      border:1px solid rgba(255,255,255,.07);
      overflow:hidden;
    }
    .sectionTitle{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:8px;
      font-size:12px;
      font-weight:700;
      color:var(--muted);
      letter-spacing:.15px;
    }
    .pill{
      padding:3px 8px;
      border-radius:999px;
      background:rgba(255,255,255,.05);
      border:1px solid rgba(255,255,255,.07);
      color:var(--muted);
      font-size:11px;
      white-space:nowrap;
      overflow:hidden;
      text-overflow:ellipsis;
      max-width:100%;
    }
    .sep{
      height:1px;
      background:linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.09), rgba(255,255,255,0));
    }
    .list{
      flex:1 1 auto;
      min-height:0;
      overflow:auto;
      padding-right:4px;
    }
    .item{
      display:grid;
      grid-template-columns:1fr auto;
      gap:10px;
      align-items:center;
      padding:9px 10px;
      margin-bottom:8px;
      border-radius:13px;
      border:1px solid rgba(255,255,255,.07);
      background:rgba(255,255,255,.03);
      transition:background .12s ease, border-color .12s ease;
    }
    .item:hover{
      background:rgba(255,255,255,.05);
      border-color:rgba(120,173,255,.18);
    }
    .item.active{
      background:linear-gradient(180deg, rgba(120,173,255,.12), rgba(255,255,255,.035));
      border-color:rgba(120,173,255,.28);
      box-shadow:inset 0 1px 0 rgba(255,255,255,.05);
    }
    .itemMain{
      min-width:0;
      display:flex;
      flex-direction:column;
      gap:4px;
    }
    .itemTitle{
      font-size:13px;
      font-weight:800;
      line-height:1.2;
      white-space:nowrap;
      overflow:hidden;
      text-overflow:ellipsis;
    }
    .itemMeta{
      display:flex;
      flex-wrap:wrap;
      gap:6px;
      min-width:0;
    }
    .itemActions{
      display:flex;
      gap:6px;
      align-items:center;
    }
    .mini{
      height:28px;
      padding:0 10px;
      border-radius:10px;
      font-size:11px;
    }

    .debugBox{
      padding:10px;
      border-radius:14px;
      border:1px solid rgba(255,255,255,.07);
      background:rgba(255,255,255,.03);
      display:flex;
      flex-direction:column;
      gap:8px;
      min-height:0;
    }
    .debugText{
      max-height:170px;
      overflow:auto;
      font-size:11px;
      line-height:1.5;
      color:var(--muted);
      word-break:break-all;
      white-space:pre-wrap;
    }

    .empty{
      padding:14px 8px;
      text-align:center;
      color:var(--muted);
      font-size:12px;
      line-height:1.6;
    }

    .yanm-drag{
      -webkit-app-region:drag;
      app-region:drag;
    }
    .no-drag, button, input, .item, .tab{
      -webkit-app-region:no-drag;
      app-region:no-drag;
    }

    .resizeGrip{
      position:absolute;
      right:7px;
      bottom:7px;
      width:16px;
      height:16px;
      border-radius:5px;
      background:linear-gradient(135deg, rgba(120,173,255,.24), rgba(119,240,255,.22));
      border:1px solid rgba(255,255,255,.12);
      opacity:.62;
      pointer-events:none;
    }

    @media (max-width: 390px){
      .main{
        grid-template-columns:1fr;
      }
      .hero{
        flex-direction:row;
        justify-content:flex-start;
        gap:12px;
      }
      .meterMeta{
        margin-top:0;
        align-items:flex-start;
      }
      .tiny{text-align:left}
      .summary{
        grid-template-columns:1fr;
      }
      .row2{
        grid-template-columns:1fr;
      }
    }
  </style>
</head>
<body>
  <div class="card">
    <div class="shell">
      <div class="topbar">
        <div class="titleWrap">
          <div class="badge"></div>
          <div class="titleBlock">
            <div class="title yanm-drag" data-yanm-drag="true">鸡厂订阅信息</div>
            <div class="subtitle" id="subTitle">本地兜底已加载,正在同步宿主状态…</div>
          </div>
        </div>
        <div class="topActions">
          <div class="statusPill" id="statusPill">准备中</div>
          <button class="btn secondary no-drag" id="btnMenu">管理</button>
          <button class="btn secondary no-drag" id="btnRefresh">刷新</button>
        </div>
      </div>

      <div class="main">
        <div class="hero">
          <div class="meter" id="meter">
            <div class="meterInner">
              <div class="meterValue" id="meterValue">--%</div>
              <div class="meterLabel">剩余</div>
            </div>
          </div>
          <div class="meterMeta">
            <div class="tiny" id="heroTitle">未选择订阅</div>
            <div class="tiny" id="heroMeta">请通过“管理”同步订阅与用量数据</div>
          </div>
        </div>

        <div class="right">
          <div class="summary">
            <div class="stat">
              <div class="k">剩余流量</div>
              <div class="v" id="statRest">0 B</div>
              <div class="s" id="statRestPct">--%</div>
            </div>
            <div class="stat">
              <div class="k">累计使用</div>
              <div class="v" id="statUsed">0 B</div>
              <div class="s" id="statUsedPct">--%</div>
            </div>
            <div class="stat">
              <div class="k">今日上传</div>
              <div class="v" id="statToday">0 B</div>
              <div class="s" id="statTodayPct">--%</div>
            </div>
          </div>
        </div>
      </div>

      <div class="foot">
        <div><strong>状态链路:</strong>内存 → localStorage → 宿主 state.set</div>
        <div id="footHint">http.request / state.get</div>
      </div>
    </div>

    <div class="menuMask" id="menuMask">
      <div class="menuPanel">
        <div class="menuHeader">
          <div class="tabs">
            <button class="tab active no-drag" data-tab="edit">编辑订阅</button>
            <button class="tab no-drag" data-tab="list">订阅列表</button>
            <button class="tab no-drag" data-tab="debug">调试</button>
          </div>
          <button class="btn secondary no-drag" id="btnCloseMenu">关闭</button>
        </div>

        <div class="menuBody">
          <div class="section active" id="sec-edit">
            <div class="form">
              <div class="sectionTitle">
                <span>编辑订阅</span>
                <span id="activeTag2" class="pill">未选中</span>
              </div>

              <div class="row2">
                <div class="field">
                  <div class="label">鸡厂名</div>
                  <input id="inputTitle" type="text" placeholder="例如:默认订阅" autocomplete="off" />
                </div>
                <div class="field">
                  <div class="label">自定义 UA</div>
                  <input id="inputUA" type="text" placeholder="例如:Mozilla/5.0 ..." autocomplete="off" />
                </div>
              </div>

              <div class="field">
                <div class="label">订阅地址</div>
                <input id="inputUrl" type="text" placeholder="粘贴订阅链接" autocomplete="off" />
              </div>

              <div class="buttonRow">
                <button class="btn no-drag" id="btnSave">保存/更新</button>
                <button class="btn secondary no-drag" id="btnTest">测试订阅</button>
                <button class="btn secondary no-drag" id="btnAdd">新增空白项</button>
                <button class="btn danger no-drag" id="btnDelete">删除当前</button>
              </div>
            </div>
          </div>

          <div class="section" id="sec-list">
            <div class="listWrap">
              <div class="sectionTitle">
                <span>订阅列表</span>
                <span id="countTag" class="pill">0 项</span>
              </div>
              <div class="sep"></div>
              <div class="list" id="list"></div>
            </div>
          </div>

          <div class="section" id="sec-debug">
            <div class="debugBox">
              <div class="sectionTitle">
                <span>调试</span>
                <span id="debugTag" class="pill">等待请求</span>
              </div>
              <div class="buttonRow">
                <button class="btn no-drag" id="btnCopyDebug">一键复制所有调试信息</button>
                <button class="btn secondary no-drag" id="btnClearDebug">清空调试</button>
              </div>
              <div class="debugText" id="debugText">未开始请求。</div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div class="toast" id="toast"></div>
    <div class="resizeGrip no-drag" data-yanm-resize="true"></div>
  </div>

  <script>
    (function () {
      const KEY = "yanm.subscription.panel.v2";
      const DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";

      const DEFAULT_STATE = {
        accounts: [
          {
            id: "default-1",
            title: "默认订阅",
            url: "",
            ua: DEFAULT_UA
          }
        ],
        activeId: "default-1",
        metrics: {
          total: 0,
          used: 0,
          rest: 0,
          upload: 0,
          download: 0,
          today: 0
        },
        lastUpdate: 0,
        status: "本地兜底已加载",
        lastDebug: ""
      };

      const $ = (id) => document.getElementById(id);
      const els = {
        body: document.body,
        meter: $("meter"),
        meterValue: $("meterValue"),
        heroTitle: $("heroTitle"),
        heroMeta: $("heroMeta"),
        statRest: $("statRest"),
        statUsed: $("statUsed"),
        statToday: $("statToday"),
        statRestPct: $("statRestPct"),
        statUsedPct: $("statUsedPct"),
        statTodayPct: $("statTodayPct"),
        subTitle: $("subTitle"),
        statusPill: $("statusPill"),
        footHint: $("footHint"),
        menuMask: $("menuMask"),
        toast: $("toast"),
        btnMenu: $("btnMenu"),
        btnRefresh: $("btnRefresh"),
        btnCloseMenu: $("btnCloseMenu"),
        btnSave: $("btnSave"),
        btnTest: $("btnTest"),
        btnAdd: $("btnAdd"),
        btnDelete: $("btnDelete"),
        btnCopyDebug: $("btnCopyDebug"),
        btnClearDebug: $("btnClearDebug"),
        inputTitle: $("inputTitle"),
        inputUA: $("inputUA"),
        inputUrl: $("inputUrl"),
        list: $("list"),
        activeTag2: $("activeTag2"),
        countTag: $("countTag"),
        debugTag: $("debugTag"),
        debugText: $("debugText"),
        secEdit: $("sec-edit"),
        secList: $("sec-list"),
        secDebug: $("sec-debug")
      };

      let state = deepClone(DEFAULT_STATE);
      let activeTab = "edit";
      let toastTimer = null;
      let requestSeq = 0;
      let saving = false;

      function deepClone(v) {
        return JSON.parse(JSON.stringify(v));
      }

      function now() {
        return Date.now ? Date.now() : new Date().getTime();
      }

      function clamp(n, min, max) {
        return Math.max(min, Math.min(max, n));
      }

      function fmtBytes(n) {
        let num = Number(n) || 0;
        if (!isFinite(num) || num <= 0) return "0 B";
        const units = ["B", "KB", "MB", "GB", "TB", "PB"];
        let i = 0;
        while (num >= 1024 && i < units.length - 1) {
          num /= 1024;
          i++;
        }
        const dec = num >= 100 ? 0 : num >= 10 ? 1 : 2;
        return `${num.toFixed(dec)} ${units[i]}`;
      }

      function setStatus(text) {
        els.statusPill.textContent = text;
        els.subTitle.textContent = text;
      }

      function showToast(text, ms = 1700) {
        els.toast.textContent = text;
        els.toast.classList.add("show");
        clearTimeout(toastTimer);
        toastTimer = setTimeout(() => els.toast.classList.remove("show"), ms);
      }

      function setDebug(tag, text) {
        els.debugTag.textContent = tag || "调试";
        els.debugText.textContent = text || "";
      }

      function persistLocal() {
        try {
          localStorage.setItem(KEY, JSON.stringify(state));
        } catch (e) {}
      }

      async function persistHost() {
        if (!(window.yanm && typeof window.yanm.invoke === "function")) return;
        try {
          await window.yanm.invoke("state.set", {
            key: KEY,
            value: JSON.stringify(state)
          });
        } catch (e) {}
      }

      function persist() {
        persistLocal();
        if (!saving) {
          saving = true;
          Promise.resolve().then(() => persistHost()).finally(() => {
            saving = false;
          });
        }
      }

      function normalizeState(input) {
        const base = deepClone(DEFAULT_STATE);
        if (!input || typeof input !== "object") return base;

        if (Array.isArray(input.accounts) && input.accounts.length) {
          base.accounts = input.accounts.map((item, idx) => ({
            id: item && item.id ? String(item.id) : `acc-${idx + 1}-${now()}`,
            title: item && item.title != null ? String(item.title) : `订阅 ${idx + 1}`,
            url: item && item.url != null ? String(item.url) : "",
            ua: item && item.ua != null ? String(item.ua) : DEFAULT_UA
          }));
        }

        if (typeof input.activeId === "string") base.activeId = input.activeId;
        if (input.metrics && typeof input.metrics === "object") {
          base.metrics = {
            total: Number(input.metrics.total) || 0,
            used: Number(input.metrics.used) || 0,
            rest: Number(input.metrics.rest) || 0,
            upload: Number(input.metrics.upload) || 0,
            download: Number(input.metrics.download) || 0,
            today: Number(input.metrics.today) || 0
          };
        }
        if (typeof input.lastUpdate === "number") base.lastUpdate = input.lastUpdate;
        if (typeof input.status === "string") base.status = input.status;
        if (typeof input.lastDebug === "string") base.lastDebug = input.lastDebug;

        if (!base.accounts.length) base.accounts = deepClone(DEFAULT_STATE.accounts);
        if (!base.accounts.some(a => a.id === base.activeId)) base.activeId = base.accounts[0].id;
        return base;
      }

      function activeAccount() {
        return state.accounts.find(a => a.id === state.activeId) || state.accounts[0] || null;
      }

      function applyState(next, reason) {
        state = normalizeState(next);
        render();
        persist();
        if (reason) setStatus(reason);
      }

      function readLocal() {
        try {
          const raw = localStorage.getItem(KEY);
          if (!raw) return null;
          return JSON.parse(raw);
        } catch (e) {
          return null;
        }
      }

      async function readHost() {
        if (!(window.yanm && typeof window.yanm.invoke === "function")) return null;
        try {
          const res = await window.yanm.invoke("state.get", { key: KEY });
          if (typeof res === "string") return JSON.parse(res || "{}");
          if (res && typeof res === "object") {
            if (typeof res.value === "string") return JSON.parse(res.value || "{}");
            if (res.value && typeof res.value === "object") return res.value;
            return res;
          }
          return null;
        } catch (e) {
          return null;
        }
      }

      function updateFields() {
        const a = activeAccount();
        els.inputTitle.value = a ? (a.title || "") : "";
        els.inputUrl.value = a ? (a.url || "") : "";
        els.inputUA.value = a ? (a.ua || DEFAULT_UA) : DEFAULT_UA;
      }

      function renderMeter() {
        const m = state.metrics || DEFAULT_STATE.metrics;
        const total = Number(m.total) || 0;
        const rest = Number(m.rest) || 0;
        const used = Number(m.used) || 0;
        const today = Number(m.today) || 0;

        const restPct = total > 0 ? clamp(Math.round((rest / total) * 100), 0, 100) : 0;
        const usedPct = total > 0 ? clamp(Math.round((used / total) * 100), 0, 100) : 0;
        const todayPct = total > 0 ? clamp(Math.round((today / total) * 100), 0, 100) : 0;
        const arc = restPct * 3.6;

        els.meter.style.background = `conic-gradient(from 180deg, var(--good) 0deg, var(--accent2) ${arc}deg, rgba(255,255,255,.10) ${arc}deg 360deg)`;
        els.meterValue.textContent = total > 0 ? `${restPct}%` : "--%";
        els.statRest.textContent = fmtBytes(rest);
        els.statUsed.textContent = fmtBytes(used);
        els.statToday.textContent = fmtBytes(today);
        els.statRestPct.textContent = total > 0 ? `${restPct}% of total` : "无总量";
        els.statUsedPct.textContent = total > 0 ? `${usedPct}% of total` : "无总量";
        els.statTodayPct.textContent = total > 0 ? `${todayPct}% of total` : "无总量";
      }

      function renderList() {
        const a = activeAccount();
        els.countTag.textContent = `${state.accounts.length} 项`;
        els.activeTag2.textContent = a ? (a.title || "未命名") : "未选中";

        if (!state.accounts.length) {
          els.list.innerHTML = `<div class="empty">暂无订阅项<br>点击“新增空白项”开始</div>`;
          return;
        }

        els.list.innerHTML = state.accounts.map((item, idx) => {
          const active = item.id === state.activeId ? "active" : "";
          const urlTip = item.url ? (item.url.length > 36 ? `${item.url.slice(0, 16)} … ${item.url.slice(-12)}` : item.url) : "未填写订阅地址";
          const uaTip = item.ua ? (item.ua.length > 34 ? `${item.ua.slice(0, 34)}…` : item.ua) : "默认 UA";
          return `
            <div class="item ${active}" data-id="${escapeHtml(item.id)}">
              <div class="itemMain">
                <div class="itemTitle">${escapeHtml(item.title || `订阅 ${idx + 1}`)}</div>
                <div class="itemMeta"><span class="pill">${escapeHtml(urlTip)}</span></div>
                <div class="itemMeta"><span class="pill">${escapeHtml(uaTip)}</span></div>
              </div>
              <div class="itemActions">
                <button class="btn mini secondary no-drag" data-act="use" data-id="${escapeHtml(item.id)}">选中</button>
                <button class="btn mini danger no-drag" data-act="del" data-id="${escapeHtml(item.id)}">删</button>
              </div>
            </div>
          `;
        }).join("");

        [...els.list.querySelectorAll(".item")].forEach(item => {
          item.addEventListener("click", (ev) => {
            if (ev.target && ev.target.closest && ev.target.closest("button")) return;
            selectAccount(item.getAttribute("data-id"));
          });
        });

        [...els.list.querySelectorAll("button[data-act]")].forEach(btn => {
          btn.addEventListener("click", (ev) => {
            ev.stopPropagation();
            const id = btn.getAttribute("data-id");
            if (btn.getAttribute("data-act") === "use") selectAccount(id);
            if (btn.getAttribute("data-act") === "del") deleteAccount(id);
          });
        });
      }

      function renderHeader() {
        const a = activeAccount();
        els.heroTitle.textContent = a ? (a.title || "未命名订阅") : "未选择订阅";
        els.heroMeta.textContent = a && a.url ? "可同步订阅与流量信息" : "请先填写订阅地址再执行测试或保存";
        els.footHint.textContent = `最后更新:${state.lastUpdate ? new Date(state.lastUpdate).toLocaleString("zh-CN") : "未更新"}`;
        if (state.lastDebug) setDebug("最近响应", state.lastDebug);
      }

      function render() {
        renderHeader();
        renderMeter();
        renderList();
        updateFields();
      }

      function escapeHtml(str) {
        return String(str == null ? "" : str)
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;")
          .replace(/"/g, "&quot;");
      }

      function selectAccount(id) {
        const item = state.accounts.find(a => a.id === id);
        if (!item) return;
        state.activeId = item.id;
        render();
        persist();
        setStatus(`已选中:${item.title || "未命名"}`);
        showToast(`已选中 ${item.title || "未命名"}`);
      }

      function newId() {
        return `acc-${now()}-${Math.random().toString(16).slice(2, 8)}`;
      }

      function addBlankAccount() {
        const base = activeAccount() || DEFAULT_STATE.accounts[0];
        const item = {
          id: newId(),
          title: "新订阅",
          url: "",
          ua: base ? (base.ua || DEFAULT_UA) : DEFAULT_UA
        };
        state.accounts.unshift(item);
        state.activeId = item.id;
        render();
        persist();
        setStatus("已新增空白订阅项");
        showToast("已新增空白项");
      }

      function upsertCurrent() {
        const title = String(els.inputTitle.value || "").trim();
        const url = String(els.inputUrl.value || "").trim();
        const ua = String(els.inputUA.value || "").trim() || DEFAULT_UA;

        if (!title) {
          setStatus("请先填写机场名");
          showToast("机场名不能为空");
          return;
        }
        if (!url) {
          setStatus("请先填写订阅地址");
          showToast("订阅地址不能为空");
          return;
        }

        let item = state.accounts.find(a => a.id === state.activeId);
        if (!item) {
          item = { id: newId(), title, url, ua };
          state.accounts.unshift(item);
          state.activeId = item.id;
        } else {
          item.title = title;
          item.url = url;
          item.ua = ua;
        }

        render();
        persist();
        setStatus(`已保存:${title}`);
        showToast("已保存");
      }

      function deleteAccount(id) {
        const target = state.accounts.find(a => a.id === id);
        if (!target) return;
        state.accounts = state.accounts.filter(a => a.id !== id);
        if (!state.accounts.length) {
          state.accounts = deepClone(DEFAULT_STATE.accounts);
        }
        if (!state.accounts.some(a => a.id === state.activeId)) {
          state.activeId = state.accounts[0].id;
        }
        render();
        persist();
        setStatus(`已删除:${target.title || "未命名"}`);
        showToast("已删除");
      }

      function buildHeaders(ua) {
        const v = ua || DEFAULT_UA;
        return {
          "User-Agent": v,
          "user-agent": v,
          "Accept": "*/*",
          "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
          "Cache-Control": "no-cache",
          "Pragma": "no-cache"
        };
      }

      function parseUserInfo(headerValue) {
        const s = String(headerValue || "");
        const get = (name) => {
          const m = s.match(new RegExp(`${name}=([0-9]+)`, "i"));
          return m ? Number(m[1]) : 0;
        };
        const upload = get("upload");
        const download = get("download");
        const total = get("total");
        const used = upload + download;
        const rest = Math.max(total - used, 0);
        return { upload, download, total, used, rest };
      }

      function normalizeResponse(res) {
        if (!res) return null;
        if (typeof res === "string") return { ok: true, status: 200, text: res, headers: {} };
        return res;
      }

      function buildDebug(res, meta, parsed) {
        const lines = [];
        lines.push(`URL: ${meta.url}`);
        lines.push(`UA: ${meta.ua || "(default)"}`);
        lines.push(`Status: ${res && res.status != null ? res.status : "unknown"}`);
        lines.push(`ok: ${res && typeof res.ok !== "undefined" ? String(res.ok) : "unknown"}`);
        lines.push(`finalUrl: ${res && res.finalUrl ? res.finalUrl : "(none)"}`);
        if (res && res.contentType) lines.push(`contentType: ${res.contentType}`);
        if (res && res.truncated != null) lines.push(`truncated: ${String(res.truncated)}`);
        if (res && res.headers) {
          const keys = Object.keys(res.headers).slice(0, 12);
          if (keys.length) {
            lines.push("headers:");
            keys.forEach(k => lines.push(`  ${k}: ${String(res.headers[k])}`));
          }
        }
        if (parsed) {
          lines.push(`upload: ${parsed.upload}`);
          lines.push(`download: ${parsed.download}`);
          lines.push(`total: ${parsed.total}`);
          lines.push(`used: ${parsed.used}`);
          lines.push(`rest: ${parsed.rest}`);
        }
        return lines.join("\n");
      }

      async function invokeHost(method, args) {
        if (!(window.yanm && typeof window.yanm.invoke === "function")) {
          throw new Error("host unavailable");
        }
        return await window.yanm.invoke(method, args);
      }

      async function fetchSubscription() {
        const seq = ++requestSeq;
        const a = activeAccount();
        if (!a || !a.url) {
          const msg = "未填写订阅地址";
          setStatus(msg);
          setDebug("空地址", msg);
          showToast("请先填写订阅地址");
          return;
        }

        const url = String(a.url || "").trim();
        const ua = String(a.ua || "").trim() || DEFAULT_UA;

        setStatus("正在拉取订阅信息…");
        setDebug("请求中", `准备请求:${url}`);
        showToast("正在请求订阅");

        try {
          const resRaw = await invokeHost("http.request", {
            url,
            method: "GET",
            headers: buildHeaders(ua),
            timeoutMs: 12000
          });

          if (seq !== requestSeq) return;

          const res = normalizeResponse(resRaw);
          const headers = (res && res.headers) || {};
          const userInfo = headers["subscription-userinfo"] || headers["Subscription-Userinfo"] || headers["subscription-userinfo".toLowerCase()] || "";
          const parsed = parseUserInfo(userInfo);

          if (res && res.ok === false) {
            const dbg = buildDebug(res, { url, ua }, parsed);
            state.lastDebug = dbg;
            setDebug("请求失败", dbg);
            renderHeader();
            persist();
            if (res.status === 404) {
              setStatus("HTTP 404:订阅链接无效");
              showToast("404:请核对订阅链接");
              return;
            }
            throw new Error(`HTTP ${res.status || "error"}`);
          }

          if (!parsed.total) {
            const dbg = buildDebug(res, { url, ua }, parsed);
            state.lastDebug = dbg;
            setDebug("缺少头信息", dbg);
            renderHeader();
            persist();
            throw new Error("缺少 subscription-userinfo");
          }

          state.metrics = {
            upload: parsed.upload,
            download: parsed.download,
            total: parsed.total,
            used: parsed.used,
            rest: parsed.rest,
            today: parsed.upload
          };
          state.lastUpdate = now();
          state.status = `已更新:${a.title || "未命名"}`;
          state.lastDebug = buildDebug(res, { url, ua }, parsed);

          render();
          persist();
          setStatus(`数据已更新:${a.title || "未命名"}`);
          setDebug("更新成功", state.lastDebug);
          showToast(`更新成功 · 剩余 ${fmtBytes(parsed.rest)}`);
        } catch (err) {
          const msg = err && err.message ? err.message : "未知错误";
          const dbg = [
            `URL: ${a.url || ""}`,
            `UA: ${a.ua || ""}`,
            `Error: ${msg}`
          ].join("\n");
          state.lastDebug = dbg;
          setDebug("异常", dbg);
          setStatus(`更新失败:${msg}`);
          showToast("请求失败");
        }
      }

      function copyDebugAll() {
        const a = activeAccount();
        const text = [
          `状态: ${state.status || ""}`,
          `最后更新时间: ${state.lastUpdate ? new Date(state.lastUpdate).toLocaleString("zh-CN") : "未更新"}`,
          `活动订阅: ${a ? (a.title || "") : ""}`,
          `账户数量: ${state.accounts.length}`,
          `--- 当前调试信息 ---`,
          state.lastDebug || "暂无调试信息"
        ].join("\n");

        const done = () => {
          showToast("调试信息已复制");
          setStatus("已复制调试信息");
        };

        if (window.yanm && typeof window.yanm.invoke === "function") {
          window.yanm.invoke("clipboard.write", { text })
            .then(done)
            .catch(async () => {
              try {
                if (navigator.clipboard && navigator.clipboard.writeText) {
                  await navigator.clipboard.writeText(text);
                  done();
                } else {
                  throw new Error("clipboard unavailable");
                }
              } catch (e) {
                setStatus("复制失败");
                showToast("复制失败");
              }
            });
          return;
        }

        try {
          const ta = document.createElement("textarea");
          ta.value = text;
          ta.style.position = "fixed";
          ta.style.left = "-9999px";
          document.body.appendChild(ta);
          ta.focus();
          ta.select();
          document.execCommand("copy");
          document.body.removeChild(ta);
          done();
        } catch (e) {
          setStatus("复制失败");
          showToast("复制失败");
        }
      }

      function clearDebug() {
        state.lastDebug = "暂无调试信息";
        setDebug("已清空", state.lastDebug);
        persist();
        showToast("已清空调试");
      }

      function showMenu() {
        els.menuMask.classList.add("show");
        els.body.classList.add("menu-open");
      }

      function hideMenu() {
        els.menuMask.classList.remove("show");
        els.body.classList.remove("menu-open");
      }

      function setTab(name) {
        activeTab = name;
        ["edit", "list", "debug"].forEach(t => {
          const btn = document.querySelector(`.tab[data-tab="${t}"]`);
          const sec = $("sec-" + t);
          if (btn) btn.classList.toggle("active", t === name);
          if (sec) sec.classList.toggle("active", t === name);
        });
      }

      function bind() {
        els.btnMenu.addEventListener("click", showMenu);
        els.btnRefresh.addEventListener("click", fetchSubscription);
        els.btnCloseMenu.addEventListener("click", hideMenu);
        els.menuMask.addEventListener("click", (e) => {
          if (e.target === els.menuMask) hideMenu();
        });

        els.btnSave.addEventListener("click", upsertCurrent);
        els.btnTest.addEventListener("click", fetchSubscription);
        els.btnAdd.addEventListener("click", addBlankAccount);
        els.btnDelete.addEventListener("click", () => {
          const a = activeAccount();
          if (a) deleteAccount(a.id);
        });

        els.btnCopyDebug.addEventListener("click", copyDebugAll);
        els.btnClearDebug.addEventListener("click", clearDebug);

        document.querySelectorAll(".tab").forEach(btn => {
          btn.addEventListener("click", () => setTab(btn.getAttribute("data-tab")));
        });

        [els.inputTitle, els.inputUrl, els.inputUA].forEach(input => {
          input.addEventListener("keydown", (e) => {
            if (e.key === "Enter") {
              e.preventDefault();
              upsertCurrent();
            }
          });
        });

        els.inputTitle.addEventListener("input", () => {
          const a = activeAccount();
          if (!a) return;
          a.title = els.inputTitle.value;
          renderHeader();
          renderList();
        });

        els.inputUrl.addEventListener("input", () => {
          const a = activeAccount();
          if (!a) return;
          a.url = els.inputUrl.value;
          renderHeader();
          renderList();
        });

        els.inputUA.addEventListener("input", () => {
          const a = activeAccount();
          if (!a) return;
          a.ua = els.inputUA.value;
          renderList();
        });

        window.addEventListener("storage", (e) => {
          if (e.key !== KEY) return;
          try {
            if (e.newValue) {
              state = normalizeState(JSON.parse(e.newValue));
              render();
            }
          } catch (err) {}
        });
      }

      async function init() {
        try {
          const local = readLocal();
          if (local) applyState(local, "本地状态已恢复");
          else render();

          const host = await readHost();
          if (host) applyState(host, "宿主状态已同步");
          else setStatus("宿主状态不可用,使用本地兜底");

          const a = activeAccount();
          if (a) {
            els.inputTitle.value = a.title || "";
            els.inputUrl.value = a.url || "";
            els.inputUA.value = a.ua || DEFAULT_UA;
          }

          render();

          if (a && a.url && String(a.url).trim()) {
            setTimeout(fetchSubscription, 240);
          } else {
            setDebug("等待输入", "未发现有效订阅地址,暂不发起请求。");
          }
        } catch (e) {
          setStatus("初始化失败,继续使用本地状态");
          render();
        }
      }

      function boot() {
        if (window.yanm && typeof window.yanm.invoke === "function") {
          bind();
          init();
        } else {
          setTimeout(boot, 180);
        }
      }

      boot();
    })();
  </script>
</body>
</html>

我的梦想捐钱修路建学校 最后更新于 2026/5/19

回复内容
暂无回复
回复主贴