| <!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>Nine7频道助手</title> |
| <script src="https://telegram.org/js/telegram-web-app.js"></script> |
| <style> |
| :root { |
| --bg: var(--tg-theme-bg-color, #1a1a2e); |
| --card: var(--tg-theme-secondary-bg-color, #16213e); |
| --text: var(--tg-theme-text-color, #eaeaea); |
| --hint: var(--tg-theme-hint-color, #8a8a9a); |
| --accent: var(--tg-theme-button-color, #0f3460); |
| --btn-text: var(--tg-theme-button-text-color, #ffffff); |
| --link: var(--tg-theme-link-color, #e94560); |
| --danger: #e94560; |
| --success: #27ae60; |
| --radius: 14px; |
| --shadow: 0 2px 16px rgba(0,0,0,.18); |
| } |
| * { margin:0; padding:0; box-sizing:border-box; } |
| body { |
| font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; |
| background: var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| padding-bottom: 80px; |
| overflow-x: hidden; |
| } |
| |
| .header { |
| background: linear-gradient(135deg, #0f3460 0%, #533483 50%, #e94560 100%); |
| padding: 20px 20px 24px; |
| text-align: center; |
| position: relative; |
| overflow: hidden; |
| } |
| .header::after { |
| content: ''; |
| position: absolute; |
| bottom: -20px; left: -10%; right: -10%; |
| height: 40px; |
| background: var(--bg); |
| border-radius: 50% 50% 0 0; |
| } |
| .header h1 { font-size: 20px; color: #fff; font-weight: 700; letter-spacing: 1px; } |
| .header p { font-size: 12px; color: rgba(255,255,255,.7); margin-top: 4px; } |
| .header-content { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 14px; |
| } |
| .avatar { |
| width: 48px; height: 48px; |
| border-radius: 50%; |
| border: 2px solid rgba(255,255,255,.3); |
| object-fit: cover; |
| box-shadow: 0 2px 12px rgba(0,0,0,.3); |
| flex-shrink: 0; |
| } |
| .avatar-placeholder { |
| width: 48px; height: 48px; |
| border-radius: 50%; |
| border: 2px solid rgba(255,255,255,.3); |
| background: rgba(255,255,255,.15); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 22px; |
| flex-shrink: 0; |
| } |
| .header-text { text-align: left; } |
| .header-text h1 { font-size: 18px; } |
| .header-text p { font-size: 11px; margin-top: 2px; } |
| .header-greeting { |
| font-size: 13px; |
| color: rgba(255,255,255,.85); |
| margin-top: 2px; |
| font-weight: 500; |
| } |
| |
| .bottom-nav { |
| position: fixed; bottom: 0; left: 0; right: 0; |
| display: flex; |
| background: var(--card); |
| border-top: 1px solid rgba(255,255,255,.06); |
| z-index: 100; |
| padding-bottom: env(safe-area-inset-bottom); |
| } |
| .nav-item { |
| flex: 1; |
| text-align: center; |
| padding: 8px 0 6px; |
| cursor: pointer; |
| transition: all .2s; |
| -webkit-tap-highlight-color: transparent; |
| } |
| .nav-item .icon { font-size: 20px; } |
| .nav-item .label { font-size: 10px; color: var(--hint); margin-top: 2px; } |
| .nav-item.active .label { color: var(--link); font-weight: 600; } |
| .nav-item.active .icon { transform: scale(1.15); } |
| |
| |
| .page { display: none; padding: 16px; animation: fadeUp .3s; } |
| .page.active { display: block; } |
| @keyframes fadeUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } } |
| |
| |
| .card { |
| background: var(--card); |
| border-radius: var(--radius); |
| padding: 16px; |
| margin-bottom: 12px; |
| box-shadow: var(--shadow); |
| position: relative; |
| border: 1px solid rgba(255,255,255,.04); |
| } |
| .card-title { |
| font-size: 14px; font-weight: 600; |
| margin-bottom: 8px; |
| display: flex; align-items: center; gap: 8px; |
| } |
| .card-body { font-size: 13px; color: var(--hint); line-height: 1.7; } |
| .card-body code { |
| background: rgba(255,255,255,.08); |
| padding: 2px 6px; |
| border-radius: 4px; |
| font-size: 12px; |
| color: var(--link); |
| } |
| .card-actions { |
| display: flex; gap: 8px; |
| margin-top: 12px; |
| flex-wrap: wrap; |
| } |
| .stat-row { |
| display: flex; justify-content: space-between; |
| padding: 8px 0; |
| border-bottom: 1px solid rgba(255,255,255,.04); |
| } |
| .stat-row:last-child { border: none; } |
| .stat-val { color: var(--link); font-weight: 600; } |
| |
| |
| .btn { |
| display: inline-flex; align-items: center; gap: 6px; |
| padding: 10px 18px; |
| border-radius: 10px; |
| border: none; |
| font-size: 13px; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all .2s; |
| -webkit-tap-highlight-color: transparent; |
| } |
| .btn:active { transform: scale(.96); } |
| .btn-primary { |
| background: linear-gradient(135deg, #0f3460, #533483); |
| color: #fff; |
| } |
| .btn-danger { background: rgba(233,69,96,.15); color: var(--danger); } |
| .btn-sm { padding: 6px 12px; font-size: 12px; border-radius: 8px; } |
| .btn-ghost { background: rgba(255,255,255,.06); color: var(--text); } |
| .btn-success { background: rgba(39,174,96,.15); color: var(--success); } |
| .btn-block { width: 100%; justify-content: center; } |
| |
| |
| .fab { |
| position: fixed; |
| bottom: 80px; right: 20px; |
| width: 52px; height: 52px; |
| border-radius: 50%; |
| background: linear-gradient(135deg, #e94560, #533483); |
| color: #fff; |
| font-size: 24px; |
| display: flex; align-items: center; justify-content: center; |
| box-shadow: 0 4px 20px rgba(233,69,96,.4); |
| cursor: pointer; |
| z-index: 50; |
| transition: all .2s; |
| border: none; |
| } |
| .fab:active { transform: scale(.9); } |
| |
| |
| .tool-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 12px; |
| } |
| .tool-card { |
| background: var(--card); |
| border-radius: var(--radius); |
| padding: 20px 14px; |
| text-align: center; |
| cursor: pointer; |
| transition: all .2s; |
| border: 1px solid rgba(255,255,255,.04); |
| box-shadow: var(--shadow); |
| } |
| .tool-card:active { transform: scale(.96); } |
| .tool-card .t-icon { font-size: 28px; margin-bottom: 8px; } |
| .tool-card .t-name { font-size: 13px; font-weight: 600; } |
| .tool-card .t-desc { font-size: 11px; color: var(--hint); margin-top: 4px; } |
| |
| |
| .modal-overlay { |
| position: fixed; inset: 0; |
| background: rgba(0,0,0,.65); |
| z-index: 200; |
| display: none; |
| align-items: flex-end; |
| justify-content: center; |
| animation: fadeIn .2s; |
| } |
| .modal-overlay.open { display: flex; } |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } |
| @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } } |
| |
| .modal { |
| background: var(--bg); |
| width: 100%; |
| max-width: 500px; |
| max-height: 88vh; |
| border-radius: 20px 20px 0 0; |
| padding: 20px; |
| overflow-y: auto; |
| animation: slideUp .3s; |
| } |
| .modal-title { |
| font-size: 17px; |
| font-weight: 700; |
| margin-bottom: 16px; |
| display: flex; align-items: center; gap: 8px; |
| } |
| .modal-close { |
| margin-left: auto; |
| background: rgba(255,255,255,.08); |
| border: none; |
| color: var(--hint); |
| width: 30px; height: 30px; |
| border-radius: 50%; |
| font-size: 16px; |
| cursor: pointer; |
| } |
| |
| |
| .form-group { margin-bottom: 14px; } |
| .form-label { |
| font-size: 12px; |
| color: var(--hint); |
| margin-bottom: 6px; |
| display: block; |
| font-weight: 500; |
| } |
| .form-input, .form-select { |
| width: 100%; |
| padding: 12px 14px; |
| background: var(--card); |
| border: 1px solid rgba(255,255,255,.08); |
| border-radius: 10px; |
| color: var(--text); |
| font-size: 14px; |
| outline: none; |
| transition: border .2s; |
| } |
| .form-input:focus { border-color: var(--link); } |
| .form-input::placeholder { color: var(--hint); } |
| .form-hint { font-size: 11px; color: var(--hint); margin-top: 4px; } |
| .channel-picker { |
| display: flex; flex-wrap: wrap; gap: 8px; |
| margin-top: 8px; |
| } |
| .ch-chip { |
| padding: 6px 12px; |
| background: rgba(255,255,255,.06); |
| border-radius: 20px; |
| font-size: 12px; |
| cursor: pointer; |
| transition: all .2s; |
| border: 1px solid transparent; |
| } |
| .ch-chip:active, .ch-chip.picked { border-color: var(--link); background: rgba(233,69,96,.1); } |
| |
| |
| .segment { |
| display: flex; |
| background: var(--card); |
| border-radius: 10px; |
| padding: 3px; |
| margin-bottom: 16px; |
| } |
| .seg-btn { |
| flex: 1; |
| text-align: center; |
| padding: 8px; |
| font-size: 13px; |
| border-radius: 8px; |
| cursor: pointer; |
| transition: all .2s; |
| color: var(--hint); |
| border: none; |
| background: transparent; |
| } |
| .seg-btn.active { |
| background: linear-gradient(135deg, #0f3460, #533483); |
| color: #fff; |
| font-weight: 600; |
| } |
| |
| |
| .toast { |
| position: fixed; |
| top: 20px; left: 50%; |
| transform: translateX(-50%) translateY(-80px); |
| background: var(--card); |
| color: var(--text); |
| padding: 12px 24px; |
| border-radius: 12px; |
| font-size: 13px; |
| z-index: 999; |
| box-shadow: 0 4px 24px rgba(0,0,0,.3); |
| transition: transform .3s; |
| border: 1px solid rgba(255,255,255,.06); |
| } |
| .toast.show { transform: translateX(-50%) translateY(0); } |
| .toast.success { border-left: 3px solid var(--success); } |
| .toast.error { border-left: 3px solid var(--danger); } |
| |
| |
| .empty { |
| text-align: center; |
| padding: 40px 20px; |
| color: var(--hint); |
| } |
| .empty .e-icon { font-size: 48px; margin-bottom: 12px; } |
| .empty .e-text { font-size: 14px; } |
| |
| |
| .badge { |
| display: inline-block; |
| padding: 2px 8px; |
| border-radius: 6px; |
| font-size: 11px; |
| font-weight: 600; |
| } |
| .badge-info { background: rgba(15,52,96,.4); color: #5dade2; } |
| .badge-warn { background: rgba(233,69,96,.15); color: var(--danger); } |
| |
| |
| .spinner { |
| width: 24px; height: 24px; |
| border: 3px solid rgba(255,255,255,.1); |
| border-top-color: var(--link); |
| border-radius: 50%; |
| animation: spin .6s linear infinite; |
| margin: 20px auto; |
| } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| .loading-card { text-align: center; padding: 40px; } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div class="header"> |
| <div class="header-content"> |
| <div id="avatarContainer"> |
| <div class="avatar-placeholder">👤</div> |
| </div> |
| <div class="header-text"> |
| <h1> Nine7<img src="https://lin7zhi.pages.dev/file/1777856634364_nine7.png" |
| style="width:22px;height:22px;vertical-align:middle;margin-right:6px;border-radius:6px;filter:drop-shadow(0 0 6px rgba(233,69,96,.4))" |
| onerror="this.style.display='none'">频道助手</h1> |
| <p class="header-greeting" id="headerGreeting">你好!</p> |
| <p id="headerSub">加载中...</p> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="toast" id="toast"></div> |
|
|
| |
| <div class="page active" id="page-home"> |
| <div class="card"> |
| <div class="card-title">⚙️ 系统状态</div> |
| <div class="card-body" id="statusInfo"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| <div class="card"> |
| <div class="card-title">📊 概览</div> |
| <div class="card-body" id="overview"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="page" id="page-sync"> |
| <div id="syncList"></div> |
| <button class="fab" onclick="openModal('modalAddSync')">+</button> |
| </div> |
|
|
| |
| <div class="page" id="page-tools"> |
| <div class="tool-grid"> |
| <div class="tool-card" onclick="openModal('modalBtnNew')"> |
| <div class="t-icon">🆕</div> |
| <div class="t-name">发按钮消息</div> |
| <div class="t-desc">发送带按钮的新消息</div> |
| </div> |
| <div class="tool-card" onclick="openModal('modalBtnOld')"> |
| <div class="t-icon">🔘</div> |
| <div class="t-name">改旧按钮</div> |
| <div class="t-desc">给旧消息加/改/删按钮</div> |
| </div> |
| <div class="tool-card" onclick="openModal('modalGenDir')"> |
| <div class="t-icon">🗂️</div> |
| <div class="t-name">手动目录</div> |
| <div class="t-desc">扫描频道生成标签目录</div> |
| </div> |
| <div class="tool-card" onclick="openModal('modalReplace')"> |
| <div class="t-icon">🔄</div> |
| <div class="t-name">替换标签</div> |
| <div class="t-desc">批量替换/删除标签</div> |
| </div> |
| <div class="tool-card" onclick="openModal('modalBackup')"> |
| <div class="t-icon">🚀</div> |
| <div class="t-name">智能备份</div> |
| <div class="t-desc">跨频道全量搬运</div> |
| </div> |
| <div class="tool-card" onclick="openModal('modalBtnMulti')"> |
| <div class="t-icon">🔗</div> |
| <div class="t-name">多按钮消息</div> |
| <div class="t-desc">添加多个按钮行</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="page" id="page-tasks"> |
| <div class="segment"> |
| <button class="seg-btn active" onclick="switchTaskTab(this,'statsList')">📈 统计任务</button> |
| <button class="seg-btn" onclick="switchTaskTab(this,'dirsList')">📂 目录任务</button> |
| </div> |
| <div id="statsList"></div> |
| <div id="dirsList" style="display:none"></div> |
| <button class="fab" id="fabTask" onclick="openAddTask()">+</button> |
| </div> |
|
|
| |
| <div class="page" id="page-addr"> |
| <div id="addrList"></div> |
| <button class="fab" onclick="openModal('modalAddAddr')">+</button> |
| </div> |
|
|
| |
| <div class="bottom-nav"> |
| <div class="nav-item active" onclick="switchPage('home',this)"> |
| <div class="icon">🏠</div><div class="label">首页</div> |
| </div> |
| <div class="nav-item" onclick="switchPage('sync',this)"> |
| <div class="icon">🔄</div><div class="label">同步</div> |
| </div> |
| <div class="nav-item" onclick="switchPage('tools',this)"> |
| <div class="icon">🧰</div><div class="label">工具箱</div> |
| </div> |
| <div class="nav-item" onclick="switchPage('tasks',this)"> |
| <div class="icon">📊</div><div class="label">任务</div> |
| </div> |
| <div class="nav-item" onclick="switchPage('addr',this)"> |
| <div class="icon">📔</div><div class="label">地址簿</div> |
| </div> |
| </div> |
|
|
| |
|
|
| |
| <div class="modal-overlay" id="modalAddSync"> |
| <div class="modal"> |
| <div class="modal-title">➕ 添加同步组 <button class="modal-close" onclick="closeModal('modalAddSync')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">源频道 ID</label> |
| <input class="form-input" id="syncSrc" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerSyncSrc"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">目标频道 ID</label> |
| <input class="form-input" id="syncTgt" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerSyncTgt"></div> |
| </div> |
| <button class="btn btn-primary btn-block" onclick="addSyncGroup()">✅ 创建同步组</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalAddAddr"> |
| <div class="modal"> |
| <div class="modal-title">📔 添加频道 <button class="modal-close" onclick="closeModal('modalAddAddr')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">频道 ID</label> |
| <input class="form-input" id="addrId" placeholder="-100xxxxxxxxxx"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">备注名称</label> |
| <input class="form-input" id="addrName" placeholder="如: 主频道"> |
| </div> |
| <button class="btn btn-primary btn-block" onclick="addChannel()">✅ 保存</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalBtnNew"> |
| <div class="modal"> |
| <div class="modal-title">🆕 发送带按钮的新消息 <button class="modal-close" onclick="closeModal('modalBtnNew')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">频道 ID</label> |
| <input class="form-input" id="bnCh" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerBnCh"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">消息正文 (支持 HTML)</label> |
| <textarea class="form-input" id="bnText" rows="4" placeholder="输入消息内容..."></textarea> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">按钮文字</label> |
| <input class="form-input" id="bnBtnText" placeholder="🔗 点击进入主群"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">跳转链接</label> |
| <input class="form-input" id="bnUrl" placeholder="https://..." type="url"> |
| </div> |
| <button class="btn btn-primary btn-block" onclick="sendBtnNew()">🚀 发送</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalBtnOld"> |
| <div class="modal"> |
| <div class="modal-title">🔘 给旧消息加/改按钮 <button class="modal-close" onclick="closeModal('modalBtnOld')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">频道 ID</label> |
| <input class="form-input" id="boCh" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerBoCh"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">消息 ID 或链接</label> |
| <input class="form-input" id="boMsg" placeholder="消息 ID 或链接"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">按钮文字 (输入"删除"以移除按钮)</label> |
| <input class="form-input" id="boBtnText" placeholder="按钮文字 或 删除"> |
| </div> |
| <div class="form-group" id="boUrlGroup"> |
| <label class="form-label">跳转链接</label> |
| <input class="form-input" id="boUrl" placeholder="https://..." type="url"> |
| </div> |
| <button class="btn btn-primary btn-block" onclick="sendBtnOld()">✅ 提交</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalBtnMulti"> |
| <div class="modal"> |
| <div class="modal-title">🔗 多按钮消息 <button class="modal-close" onclick="closeModal('modalBtnMulti')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">频道 ID</label> |
| <input class="form-input" id="bmCh" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerBmCh"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">消息正文 (支持 HTML)</label> |
| <textarea class="form-input" id="bmText" rows="3" placeholder="消息内容..."></textarea> |
| </div> |
| <div id="bmBtnsContainer"> |
| <div class="form-group" style="display:flex;gap:8px"> |
| <input class="form-input" placeholder="按钮文字" style="flex:1"> |
| <input class="form-input" placeholder="https://链接" style="flex:1"> |
| </div> |
| </div> |
| <button class="btn btn-ghost btn-sm" onclick="addBtnRow()" style="margin-bottom:12px">➕ 再加一行按钮</button> |
| <button class="btn btn-primary btn-block" onclick="sendBtnMulti()">🚀 发送</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalGenDir"> |
| <div class="modal"> |
| <div class="modal-title">🗂️ 生成手动目录 <button class="modal-close" onclick="closeModal('modalGenDir')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">频道 ID</label> |
| <input class="form-input" id="gdCh" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerGdCh"></div> |
| </div> |
| <p class="form-hint">⚠️ 扫描可能需要较长时间,结果将发送到机器人对话中</p> |
| <button class="btn btn-primary btn-block" style="margin-top:12px" onclick="genDir()">🔍 开始扫描</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalReplace"> |
| <div class="modal"> |
| <div class="modal-title">🔄 批量替换标签 <button class="modal-close" onclick="closeModal('modalReplace')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">频道 ID</label> |
| <input class="form-input" id="rpCh" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerRpCh"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">旧标签 (带 #)</label> |
| <input class="form-input" id="rpOld" placeholder="#旧标签"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">新标签 (输入"删除"则彻底移除)</label> |
| <input class="form-input" id="rpNew" placeholder="#新标签 或 删除"> |
| </div> |
| <button class="btn btn-primary btn-block" onclick="replaceTag()">🚀 开始替换</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalBackup"> |
| <div class="modal"> |
| <div class="modal-title">🚀 智能备份 <button class="modal-close" onclick="closeModal('modalBackup')">✕</button></div> |
| <div id="backupGroupBtns" style="margin-bottom:12px"></div> |
| <hr style="border-color:rgba(255,255,255,.06);margin:12px 0"> |
| <p class="form-label" style="margin-bottom:8px">✏️ 手动指定通道:</p> |
| <div class="form-group"> |
| <label class="form-label">源频道 ID</label> |
| <input class="form-input" id="bkSrc" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerBkSrc"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">目标频道 ID</label> |
| <input class="form-input" id="bkTgt" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerBkTgt"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">源频道最新消息链接</label> |
| <input class="form-input" id="bkLink" placeholder="https://t.me/..."> |
| </div> |
| <button class="btn btn-primary btn-block" onclick="startBackup()">🚀 开始备份</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalAddStat"> |
| <div class="modal"> |
| <div class="modal-title">📈 创建统计任务 <button class="modal-close" onclick="closeModal('modalAddStat')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">任务名称</label> |
| <input class="form-input" id="stName" placeholder="日常早报统计"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">频道 ID</label> |
| <input class="form-input" id="stCh" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerStCh"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">消息 ID 或链接</label> |
| <input class="form-input" id="stMsg" placeholder="311"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">统计表标题</label> |
| <input class="form-input" id="stTitle" placeholder="🔥 霸榜热评区:"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">显示前几名</label> |
| <input class="form-input" id="stTopN" placeholder="10" type="number"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">触发标签 (带 #)</label> |
| <input class="form-input" id="stTrig" placeholder="#更新"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">更新频率 (分钟)</label> |
| <input class="form-input" id="stIntv" placeholder="15" type="number"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">寿命期限 (天)</label> |
| <input class="form-input" id="stDura" placeholder="7" type="number"> |
| </div> |
| |
| <div class="form-group"> |
| <label class="form-label">屏蔽名单 (空格隔开,无则留空)</label> |
| <input class="form-input" id="stBlack" placeholder="名字A 名字B 名字C"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">屏蔽区标题 (留空则默认)</label> |
| <input class="form-input" id="stBlTitle" placeholder="🚫本月轮换限制:"> |
| </div> |
| <button class="btn btn-primary btn-block" onclick="addStatTask()">✅ 创建任务</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalAddDir"> |
| <div class="modal"> |
| <div class="modal-title">🗂️ 创建自动目录 <button class="modal-close" onclick="closeModal('modalAddDir')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label">任务名称</label> |
| <input class="form-input" id="drName" placeholder="主频道自动目录"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">频道 ID</label> |
| <input class="form-input" id="drCh" placeholder="-100xxxxxxxxxx"> |
| <div class="channel-picker" id="pickerDrCh"></div> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">承载目录的消息 ID</label> |
| <input class="form-input" id="drMsg" placeholder="消息ID"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">屏蔽的标签 (空格隔开,无则留空)</label> |
| <input class="form-input" id="drBlack" placeholder="#通知 #避雷"> |
| </div> |
| <button class="btn btn-primary btn-block" onclick="addDirTask()">✅ 创建</button> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalEdit"> |
| <div class="modal"> |
| <div class="modal-title" id="editTitle">✏️ 编辑 <button class="modal-close" onclick="closeModal('modalEdit')">✕</button></div> |
| <div class="form-group"> |
| <label class="form-label" id="editLabel">新值</label> |
| <input class="form-input" id="editVal"> |
| </div> |
| <button class="btn btn-primary btn-block" id="editSubmitBtn" onclick="submitEdit()">✅ 确认修改</button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const tg = window.Telegram.WebApp; |
| tg.ready(); |
| tg.expand(); |
| tg.enableClosingConfirmation(); |
| |
| const API = '/api'; |
| const initData = tg.initData || ''; |
| const user = tg.initDataUnsafe?.user; |
| |
| let userData = null; |
| let currentTaskTab = 'stats'; |
| let editCtx = {}; |
| |
| |
| if (user) { |
| |
| const avatarContainer = document.getElementById('avatarContainer'); |
| if (user.photo_url) { |
| avatarContainer.innerHTML = `<img class="avatar" src="${user.photo_url}" alt="avatar" onerror="this.outerHTML='<div class=\\'avatar-placeholder\\'>${user.first_name?user.first_name[0]:'👤'}</div>'">`; |
| } else { |
| const initial = user.first_name ? user.first_name[0] : '👤'; |
| avatarContainer.innerHTML = `<div class="avatar-placeholder">${initial}</div>`; |
| } |
| |
| |
| const hour = new Date().getHours(); |
| let greeting = '🌙 晚上好'; |
| if (hour >= 5 && hour < 12) greeting = '🌅 早上好'; |
| else if (hour >= 12 && hour < 14) greeting = '☀️ 中午好'; |
| else if (hour >= 14 && hour < 18) greeting = '🌤 下午好'; |
| |
| document.getElementById('headerGreeting').textContent = `${greeting},${user.first_name}!`; |
| document.getElementById('headerSub').textContent = `UID: ${user.id}`; |
| } else { |
| document.getElementById('headerGreeting').textContent = ''; |
| document.getElementById('headerSub').textContent = '未检测到用户'; |
| } |
| |
| |
| function toast(msg, type = 'success') { |
| const t = document.getElementById('toast'); |
| t.textContent = msg; |
| t.className = `toast show ${type}`; |
| setTimeout(() => t.className = 'toast', 2800); |
| } |
| |
| async function api(path, method = 'GET', body = null) { |
| const opts = { |
| method, |
| headers: { 'Content-Type': 'application/json', 'X-Init-Data': initData } |
| }; |
| if (body) opts.body = JSON.stringify(body); |
| const res = await fetch(API + path, opts); |
| return res.json(); |
| } |
| |
| function switchPage(name, el) { |
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); |
| document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); |
| document.getElementById('page-' + name).classList.add('active'); |
| el.classList.add('active'); |
| if (name === 'sync') renderSync(); |
| if (name === 'tasks') renderTasks(); |
| if (name === 'addr') renderAddr(); |
| if (name === 'home') loadHome(); |
| } |
| |
| function openModal(id) { |
| document.getElementById(id).classList.add('open'); |
| renderChannelPickers(); |
| if (id === 'modalBackup') renderBackupGroups(); |
| } |
| function closeModal(id) { document.getElementById(id).classList.remove('open'); } |
| |
| |
| document.querySelectorAll('.modal-overlay').forEach(o => { |
| o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); |
| }); |
| |
| |
| function renderChannelPickers() { |
| if (!userData) return; |
| const ab = userData.address_book || {}; |
| document.querySelectorAll('.channel-picker').forEach(picker => { |
| picker.innerHTML = ''; |
| Object.entries(ab).forEach(([cid, name]) => { |
| const chip = document.createElement('div'); |
| chip.className = 'ch-chip'; |
| chip.textContent = `📔 ${name}`; |
| chip.onclick = () => { |
| const inp = picker.previousElementSibling; |
| if (inp && inp.classList.contains('form-input')) inp.value = cid; |
| chip.classList.add('picked'); |
| setTimeout(() => chip.classList.remove('picked'), 600); |
| tg.HapticFeedback.impactOccurred('light'); |
| }; |
| picker.appendChild(chip); |
| }); |
| }); |
| } |
| |
| |
| async function loadHome() { |
| const data = await api('/data'); |
| if (!data.ok) return; |
| userData = data.user; |
| const d = data; |
| |
| document.getElementById('statusInfo').innerHTML = ` |
| <div class="stat-row"><span>☁️ 云端路径</span><span class="stat-val" style="font-size:11px;max-width:160px;overflow:hidden;text-overflow:ellipsis">${d.webdav_url || '未设置'}</span></div> |
| <div class="stat-row"><span>🗺️ 消息映射</span><span class="stat-val">${d.msg_count} 条</span></div> |
| <div class="stat-row"><span>🤖 Userbot</span><span class="stat-val">${d.userbot ? '✅ 在线' : '❌ 离线'}</span></div> |
| `; |
| |
| const groups = userData.groups || []; |
| const stats = userData.stats_tasks || []; |
| const dirs = userData.dir_tasks || []; |
| const ab = userData.address_book || {}; |
| |
| document.getElementById('overview').innerHTML = ` |
| <div class="stat-row"><span>🔄 同步组</span><span class="stat-val">${groups.length}</span></div> |
| <div class="stat-row"><span>📈 统计任务</span><span class="stat-val">${stats.length}</span></div> |
| <div class="stat-row"><span>📂 目录任务</span><span class="stat-val">${dirs.length}</span></div> |
| <div class="stat-row"><span>📔 地址簿</span><span class="stat-val">${Object.keys(ab).length} 个频道</span></div> |
| `; |
| } |
| |
| |
| function renderSync() { |
| const list = document.getElementById('syncList'); |
| const groups = userData?.groups || []; |
| if (!groups.length) { |
| list.innerHTML = '<div class="empty"><div class="e-icon">🔄</div><div class="e-text">暂无同步任务<br>点击右下角 + 创建</div></div>'; |
| return; |
| } |
| list.innerHTML = groups.map((g, i) => ` |
| <div class="card"> |
| <div class="card-title">📦 同步组 ${i + 1}</div> |
| <div class="card-body"> |
| 源频道: <code>${g.src}</code><br> |
| 目标频道: <code>${g.tgt}</code> |
| </div> |
| <div class="card-actions"> |
| <button class="btn btn-danger btn-sm" onclick="delSync(${i})">🗑 删除</button> |
| </div> |
| </div> |
| `).join(''); |
| } |
| |
| async function addSyncGroup() { |
| const src = document.getElementById('syncSrc').value.trim(); |
| const tgt = document.getElementById('syncTgt').value.trim(); |
| if (!src || !tgt) return toast('请填写完整', 'error'); |
| const res = await api('/groups', 'POST', { src, tgt }); |
| if (res.ok) { |
| toast('✅ 同步组已创建'); |
| closeModal('modalAddSync'); |
| userData = res.user; |
| renderSync(); |
| } else toast(res.msg || '失败', 'error'); |
| } |
| |
| async function delSync(idx) { |
| tg.showConfirm('确认删除该同步组?', async (ok) => { |
| if (!ok) return; |
| const res = await api(`/groups/${idx}`, 'DELETE'); |
| if (res.ok) { toast('已删除'); userData = res.user; renderSync(); } |
| else toast(res.msg || '失败', 'error'); |
| }); |
| } |
| |
| |
| function renderAddr() { |
| const list = document.getElementById('addrList'); |
| const ab = userData?.address_book || {}; |
| const entries = Object.entries(ab); |
| if (!entries.length) { |
| list.innerHTML = '<div class="empty"><div class="e-icon">📔</div><div class="e-text">暂无保存的频道<br>点击右下角 + 添加</div></div>'; |
| return; |
| } |
| list.innerHTML = entries.map(([cid, name]) => ` |
| <div class="card"> |
| <div class="card-title">📔 ${name}</div> |
| <div class="card-body"><code>${cid}</code></div> |
| <div class="card-actions"> |
| <button class="btn btn-danger btn-sm" onclick="delAddr('${cid}')">🗑 删除</button> |
| </div> |
| </div> |
| `).join(''); |
| } |
| |
| async function addChannel() { |
| const id = document.getElementById('addrId').value.trim(); |
| const name = document.getElementById('addrName').value.trim(); |
| if (!id || !name) return toast('请填写完整', 'error'); |
| const res = await api('/channels', 'POST', { id: id, name }); |
| if (res.ok) { toast('✅ 已添加'); closeModal('modalAddAddr'); userData = res.user; renderAddr(); renderChannelPickers(); } |
| else toast(res.msg || '失败', 'error'); |
| } |
| |
| async function delAddr(cid) { |
| tg.showConfirm('确认删除?', async (ok) => { |
| if (!ok) return; |
| const res = await api(`/channels/${encodeURIComponent(cid)}`, 'DELETE'); |
| if (res.ok) { toast('已删除'); userData = res.user; renderAddr(); renderChannelPickers(); } |
| }); |
| } |
| |
| |
| function switchTaskTab(el, listId) { |
| document.querySelectorAll('.seg-btn').forEach(b => b.classList.remove('active')); |
| el.classList.add('active'); |
| currentTaskTab = listId === 'statsList' ? 'stats' : 'dirs'; |
| document.getElementById('statsList').style.display = listId === 'statsList' ? '' : 'none'; |
| document.getElementById('dirsList').style.display = listId === 'dirsList' ? '' : 'none'; |
| } |
| |
| function openAddTask() { |
| openModal(currentTaskTab === 'stats' ? 'modalAddStat' : 'modalAddDir'); |
| } |
| |
| function renderTasks() { |
| renderStatsTasks(); |
| renderDirsTasks(); |
| } |
| |
| function renderStatsTasks() { |
| const list = document.getElementById('statsList'); |
| const tasks = userData?.stats_tasks || []; |
| if (!tasks.length) { |
| list.innerHTML = '<div class="empty"><div class="e-icon">📈</div><div class="e-text">暂无统计任务</div></div>'; |
| return; |
| } |
| list.innerHTML = tasks.map((t, i) => ` |
| <div class="card"> |
| <div class="card-title">🎯 ${t.task_name || '未命名'} <span class="badge badge-info">每${t.interval||60}分钟</span></div> |
| <div class="card-body"> |
| 频道: <code>${t.channel_id}</code> | 消息: <code>${t.msg_id}</code><br> |
| 表头: <code>${t.table_title || ''}</code><br> |
| 触发: <code>${t.trigger_tag || ''}</code> | 前 <code>${t.top_n||10}</code> 名 | 寿命 <code>${t.duration||7}</code> 天<br> |
| 🚫 屏蔽: <code>${(t.stats_blacklist||[]).join(', ') || '无'}</code><br> |
| 📝 屏蔽标题: <code>${t.blacklist_title || '默认'}</code> |
| </div> |
| <div class="card-actions"> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'task_name','任务名称')">✏️名称</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'channel_id','频道ID')">✏️频道</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'msg_id','消息ID')">✏️消息</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'table_title','表头标题')">✏️表头</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'trigger_tag','触发标签')">✏️标签</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'top_n','上榜名额')">🏆名额</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'interval','更新频率(分钟)')">⏱频率</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'duration','寿命(天)')">⏳寿命</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'add_stats_bl','添加屏蔽(空格隔开)')">🚫加屏蔽</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'rm_stats_bl','解除屏蔽(空格隔开)')">✅删屏蔽</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('stats',${i},'blacklist_title','屏蔽区标题')">📝屏蔽标题</button> |
| <button class="btn btn-danger btn-sm" onclick="delTask('stats',${i})">🗑 删除</button> |
| </div> |
| </div> |
| `).join(''); |
| } |
| |
| function renderDirsTasks() { |
| const list = document.getElementById('dirsList'); |
| const tasks = userData?.dir_tasks || []; |
| if (!tasks.length) { |
| list.innerHTML = '<div class="empty"><div class="e-icon">📂</div><div class="e-text">暂无目录任务</div></div>'; |
| return; |
| } |
| list.innerHTML = tasks.map((t, i) => ` |
| <div class="card"> |
| <div class="card-title">🗂️ ${t.task_name || '未命名'} <span class="badge badge-info">每${t.interval||15}分钟</span></div> |
| <div class="card-body"> |
| 频道: <code>${t.channel_id}</code> | 消息: <code>${t.msg_id}</code><br> |
| 屏蔽: <code>${(t.blacklist||[]).join(', ') || '无'}</code><br> |
| 已收录: <code>${(t.tags_cache||[]).length}</code> 个标签 |
| </div> |
| <div class="card-actions"> |
| <button class="btn btn-ghost btn-sm" onclick="editField('dirs',${i},'add_blacklist','追加屏蔽标签(空格隔开)')">➕屏蔽</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('dirs',${i},'rm_blacklist','移除屏蔽标签(空格隔开)')">➖屏蔽</button> |
| <button class="btn btn-ghost btn-sm" onclick="editField('dirs',${i},'interval','扫描频率(分钟)')">⏱频率</button> |
| <button class="btn btn-danger btn-sm" onclick="delTask('dirs',${i})">🗑 删除</button> |
| </div> |
| </div> |
| `).join(''); |
| } |
| |
| async function addStatTask() { |
| const body = { |
| task_name: document.getElementById('stName').value.trim(), |
| channel_id: document.getElementById('stCh').value.trim(), |
| msg_id: document.getElementById('stMsg').value.trim(), |
| table_title: document.getElementById('stTitle').value.trim(), |
| top_n: parseInt(document.getElementById('stTopN').value) || 10, |
| trigger_tag: document.getElementById('stTrig').value.trim(), |
| interval: parseInt(document.getElementById('stIntv').value) || 15, |
| duration: parseInt(document.getElementById('stDura').value) || 7, |
| |
| stats_blacklist: document.getElementById('stBlack').value.trim(), |
| blacklist_title: document.getElementById('stBlTitle').value.trim() || '🚫本月轮换限制:' |
| }; |
| |
| if (!body.task_name || !body.channel_id || !body.msg_id) return toast('请填写必填项', 'error'); |
| if (!body.trigger_tag.startsWith('#')) return toast('触发标签必须以#开头', 'error'); |
| const res = await api('/stats', 'POST', body); |
| if (res.ok) { toast('✅ 统计任务已创建'); closeModal('modalAddStat'); userData = res.user; renderStatsTasks(); } |
| else toast(res.msg || '创建失败', 'error'); |
| } |
| |
| async function addDirTask() { |
| const bl = document.getElementById('drBlack').value.trim(); |
| const body = { |
| task_name: document.getElementById('drName').value.trim(), |
| channel_id: document.getElementById('drCh').value.trim(), |
| msg_id: document.getElementById('drMsg').value.trim(), |
| blacklist: bl ? bl.split(/\s+/) : [] |
| }; |
| if (!body.task_name || !body.channel_id || !body.msg_id) return toast('请填写必填项', 'error'); |
| const res = await api('/dirs', 'POST', body); |
| if (res.ok) { toast('✅ 目录任务已创建'); closeModal('modalAddDir'); userData = res.user; renderDirsTasks(); } |
| else toast(res.msg || '创建失败', 'error'); |
| } |
| |
| async function delTask(type, idx) { |
| tg.showConfirm('确认删除此任务?', async (ok) => { |
| if (!ok) return; |
| const path = type === 'stats' ? `/stats/${idx}` : `/dirs/${idx}`; |
| const res = await api(path, 'DELETE'); |
| if (res.ok) { toast('已删除'); userData = res.user; renderTasks(); } |
| }); |
| } |
| |
| function editField(type, idx, field, label) { |
| editCtx = { type, idx, field }; |
| document.getElementById('editTitle').innerHTML = `✏️ 编辑 - ${label} <button class="modal-close" onclick="closeModal('modalEdit')">✕</button>`; |
| document.getElementById('editLabel').textContent = label; |
| document.getElementById('editVal').value = ''; |
| document.getElementById('editVal').placeholder = `请输入新的${label}`; |
| openModal('modalEdit'); |
| } |
| |
| async function submitEdit() { |
| const val = document.getElementById('editVal').value.trim(); |
| if (!val) return toast('不能为空', 'error'); |
| const { type, idx, field } = editCtx; |
| const path = type === 'stats' ? `/stats/${idx}` : `/dirs/${idx}`; |
| const res = await api(path, 'PUT', { field, value: val }); |
| if (res.ok) { toast('✅ 已修改'); closeModal('modalEdit'); userData = res.user; renderTasks(); } |
| else toast(res.msg || '修改失败', 'error'); |
| } |
| |
| |
| async function sendBtnNew() { |
| const body = { |
| ch_id: document.getElementById('bnCh').value.trim(), |
| text: document.getElementById('bnText').value, |
| btn_text: document.getElementById('bnBtnText').value.trim(), |
| url: document.getElementById('bnUrl').value.trim() |
| }; |
| if (!body.ch_id || !body.text || !body.btn_text || !body.url) return toast('请填写完整', 'error'); |
| const res = await api('/btn_new', 'POST', body); |
| if (res.ok) { toast('✅ 消息已发送'); closeModal('modalBtnNew'); } |
| else toast(res.msg || '发送失败', 'error'); |
| } |
| |
| async function sendBtnOld() { |
| const body = { |
| ch_id: document.getElementById('boCh').value.trim(), |
| msg_id: document.getElementById('boMsg').value.trim(), |
| btn_text: document.getElementById('boBtnText').value.trim(), |
| url: document.getElementById('boUrl').value.trim() |
| }; |
| if (!body.ch_id || !body.msg_id || !body.btn_text) return toast('请填写必填项', 'error'); |
| const res = await api('/btn_old', 'POST', body); |
| if (res.ok) { toast('✅ 操作成功'); closeModal('modalBtnOld'); } |
| else toast(res.msg || '操作失败', 'error'); |
| } |
| |
| function addBtnRow() { |
| const c = document.getElementById('bmBtnsContainer'); |
| const div = document.createElement('div'); |
| div.className = 'form-group'; |
| div.style.cssText = 'display:flex;gap:8px'; |
| div.innerHTML = `<input class="form-input" placeholder="按钮文字" style="flex:1"><input class="form-input" placeholder="https://链接" style="flex:1">`; |
| c.appendChild(div); |
| } |
| |
| async function sendBtnMulti() { |
| const ch = document.getElementById('bmCh').value.trim(); |
| const text = document.getElementById('bmText').value; |
| const rows = document.querySelectorAll('#bmBtnsContainer .form-group'); |
| const buttons = []; |
| rows.forEach(r => { |
| const inputs = r.querySelectorAll('input'); |
| if (inputs[0].value.trim() && inputs[1].value.trim()) |
| buttons.push({ text: inputs[0].value.trim(), url: inputs[1].value.trim() }); |
| }); |
| if (!ch || !text || !buttons.length) return toast('请填写完整', 'error'); |
| const res = await api('/btn_multi', 'POST', { ch_id: ch, text, buttons }); |
| if (res.ok) { toast('✅ 已发送'); closeModal('modalBtnMulti'); } |
| else toast(res.msg || '失败', 'error'); |
| } |
| |
| async function genDir() { |
| const ch = document.getElementById('gdCh').value.trim(); |
| if (!ch) return toast('请输入频道ID', 'error'); |
| const res = await api('/gen_dir', 'POST', { ch_id: ch }); |
| toast(res.ok ? '✅ 扫描已启动,请在 Bot 对话中查看结果' : (res.msg || '失败'), res.ok ? 'success' : 'error'); |
| if (res.ok) closeModal('modalGenDir'); |
| } |
| |
| async function replaceTag() { |
| const body = { |
| ch_id: document.getElementById('rpCh').value.trim(), |
| old_tag: document.getElementById('rpOld').value.trim(), |
| new_tag: document.getElementById('rpNew').value.trim() |
| }; |
| if (!body.ch_id || !body.old_tag) return toast('请填写必填项', 'error'); |
| if (!body.old_tag.startsWith('#')) return toast('旧标签需以#开头', 'error'); |
| const res = await api('/replace_tag', 'POST', body); |
| toast(res.ok ? '✅ 替换任务已启动,请在 Bot 对话中查看进度' : (res.msg || '失败'), res.ok ? 'success' : 'error'); |
| if (res.ok) closeModal('modalReplace'); |
| } |
| |
| function renderBackupGroups() { |
| const c = document.getElementById('backupGroupBtns'); |
| const groups = userData?.groups || []; |
| if (!groups.length) { c.innerHTML = '<p style="color:var(--hint);font-size:13px">暂无同步组可快速备份</p>'; return; } |
| c.innerHTML = groups.map((g, i) => ` |
| <button class="btn btn-ghost btn-sm" style="margin:4px" onclick="quickBackup(${i})">📦 组${i+1}: ${g.src}→${g.tgt}</button> |
| `).join(''); |
| } |
| |
| async function quickBackup(idx) { |
| tg.showPopup({ |
| title: '输入最新消息链接', |
| message: '请粘贴源频道最新消息的链接', |
| buttons: [{ type: 'default', text: '确定', id: 'ok' }, { type: 'cancel' }] |
| }, async (id) => { |
| if (id !== 'ok') return; |
| |
| toast('请在 Bot 对话中使用 /backup 命令', 'error'); |
| }); |
| } |
| |
| async function startBackup() { |
| const body = { |
| src: document.getElementById('bkSrc').value.trim(), |
| tgt: document.getElementById('bkTgt').value.trim(), |
| link: document.getElementById('bkLink').value.trim() |
| }; |
| if (!body.src || !body.tgt || !body.link) return toast('请填写完整', 'error'); |
| const res = await api('/backup', 'POST', body); |
| toast(res.ok ? '✅ 备份已启动,请在 Bot 对话中查看进度' : (res.msg || '失败'), res.ok ? 'success' : 'error'); |
| if (res.ok) closeModal('modalBackup'); |
| } |
| |
| loadHome(); |
| </script> |
| </body> |
| </html> |
|
|