Greasy Fork

来自缓存

Greasy Fork is available in English.

千川广告计划 ROI 倒计时(多账号独立+自修复)

每个账号独立保存倒计时,支持刷新恢复,自动检测并修复丢失的倒计时显示,带管理面板。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         千川广告计划 ROI 倒计时(多账号独立+自修复)
// @namespace    http://greasyfork.icu/zh-CN/users/your-name
// @version      3.1.0
// @description  每个账号独立保存倒计时,支持刷新恢复,自动检测并修复丢失的倒计时显示,带管理面板。
// @author       你
// @match        *://qianchuan.jinritemai.com/uni-prom*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
  'use strict';

  const DURATION_MS = 61 * 60 * 1000; // 1小时1分钟
  const aavid = new URL(location.href).searchParams.get('aavid') || 'default';
  const STORAGE_KEY = `timers_${aavid}`;
  console.log(`👤 当前账号ID: ${aavid}`);

  let timers = GM_getValue(STORAGE_KEY, {}); // { planId: startTimestamp }
  let lastEdit = null;
  const countdownMap = new Map();

  // ✅ 页面加载后延迟恢复倒计时
  restoreCountdowns();
  createManagerButton();

  // 🩺 每秒检测倒计时显示是否存在(自修复)
  setInterval(() => {
    Object.keys(timers).forEach(planId => {
      const row = findRowByPlanId(planId);
      if (!row) return;
      const exist = row.querySelector('.tm-countdown');
      if (!exist) {
        console.warn(`⚙️ 倒计时缺失,自动修复计划 ${planId}`);
        createCountdown(planId, timers[planId], row);
      }
    });
  }, 1000);

  // 延迟恢复逻辑
  function restoreCountdowns() {
    const planIds = Object.keys(timers);
    if (planIds.length === 0) return;
    let retry = 0;
    const checkInterval = setInterval(() => {
      let allRestored = true;
      for (const planId of planIds) {
        const row = findRowByPlanId(planId);
        if (row) {
          if (!row.querySelector('.tm-countdown')) {
            createCountdown(planId, timers[planId], row);
            console.log(`♻️ 已恢复倒计时: 计划 ${planId}`);
          }
        } else {
          allRestored = false;
        }
      }
      retry++;
      if (allRestored || retry > 10) clearInterval(checkInterval);
    }, 3000);
  }

  // ROI编辑检测
  document.addEventListener('click', (e) => {
    const btn = e.target.closest('.roi-bid .edit-icon');
    if (!btn) return;
    const row = btn.closest('tr');
    const planId = row?.querySelector('.oc-typography-value-slot')?.innerText.match(/\d+/)?.[0];
    const roiValue = row?.querySelector('.roi-bid .bold')?.innerText?.trim();
    if (!planId || !roiValue) return;
    lastEdit = { planId, oldValue: roiValue, row };
    console.log(`🟦 [编辑检测] ROI编辑点击 | ID=${planId} | 当前ROI=${roiValue}`);
  });

  // 确定按钮检测ROI变化
  document.addEventListener('click', (e) => {
    const btn = e.target.closest('button.ovui-button');
    if (!btn) return;
    if (btn.textContent.trim() !== '确定') return;
    if (!lastEdit) return;

    const { planId, oldValue, row } = lastEdit;
    const roiNode = row.querySelector('.roi-bid .bold');
    if (!roiNode) return;

    console.log(`🟧 [确认检测] 点击确定 | 等待ROI更新… | ID=${planId} | 旧值=${oldValue}`);

    const observer = new MutationObserver(() => {
      const newValue = roiNode.innerText.trim();
      if (newValue !== oldValue) {
        observer.disconnect();
        console.log(`🟩 [ROI变化检测] ${planId}: ${oldValue} → ${newValue}`);
        startCountdown(planId, row);
      }
    });

    observer.observe(roiNode, { characterData: true, subtree: true, childList: true });
    setTimeout(() => observer.disconnect(), 5000);
    lastEdit = null;
  });

  // 启动倒计时
  function startCountdown(planId, row) {
    const start = Date.now();
    timers[planId] = start;
    GM_setValue(STORAGE_KEY, timers);
    createCountdown(planId, start, row);
    updateManagerPanel();
  }

  // 创建倒计时
  function createCountdown(planId, start, row) {
    if (!row) row = findRowByPlanId(planId);
    if (!row) return;

    const roiNode = row.querySelector('.roi-bid .bold');
    if (!roiNode) return;

    roiNode.parentNode.querySelector('.tm-countdown')?.remove();

    const div = document.createElement('span');
    div.className = 'tm-countdown';
    div.style.cssText = `
      color: #2a55e5;
      font-weight: 500;
      margin-left: 6px;
      font-size: 12px;
    `;
    roiNode.after(div);

    if (countdownMap.has(planId)) clearInterval(countdownMap.get(planId));

    const timer = setInterval(() => {
      const remain = DURATION_MS - (Date.now() - start);
      if (remain <= 0) {
        clearInterval(timer);
        div.remove();
        countdownMap.delete(planId);
        delete timers[planId];
        GM_setValue(STORAGE_KEY, timers);
        updateManagerPanel();
        return;
      }
      const m = Math.floor(remain / 60000);
      const s = Math.floor((remain % 60000) / 1000);
      div.textContent = `剩余 ${m}分${s.toString().padStart(2, '0')}秒`;
      updateManagerPanel();
    }, 1000);

    countdownMap.set(planId, timer);
  }

  // 查找计划行
  function findRowByPlanId(id) {
    return [...document.querySelectorAll('tr')].find((tr) => tr.innerText.includes(id));
  }

  // 创建右上角管理按钮
  function createManagerButton() {
    const btn = document.createElement('button');
    btn.textContent = '🕒 倒计时管理';
    btn.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      z-index: 999999;
      background: #2a55e5;
      color: #fff;
      border: none;
      border-radius: 6px;
      padding: 6px 10px;
      font-size: 13px;
      cursor: pointer;
    `;
    btn.onclick = togglePanel;
    document.body.appendChild(btn);
  }

  // 管理面板
  let panel = null;
  function togglePanel() {
    if (panel) {
      panel.remove();
      panel = null;
      return;
    }

    panel = document.createElement('div');
    panel.style.cssText = `
      position: fixed;
      top: 60px;
      right: 20px;
      background: #fff;
      border: 1px solid #ccc;
      border-radius: 8px;
      padding: 10px;
      width: 320px;
      z-index: 999999;
      font-size: 13px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.2);
    `;
    panel.innerHTML = `<b>🕒 当前倒计时列表(账号:${aavid})</b>
      <button id="clearAll" style="float:right;background:#e33;color:#fff;border:none;border-radius:4px;padding:2px 6px;cursor:pointer;">清除全部</button>
      <hr style="margin:6px 0;">
      <div id="timerList">加载中...</div>`;
    document.body.appendChild(panel);
    updateManagerPanel();

    panel.querySelector('#clearAll').onclick = () => {
      if (confirm('确认清除所有倒计时吗?')) {
        for (const id of Object.keys(timers)) removeCountdown(id);
      }
    };
  }

  // 更新面板
  function updateManagerPanel() {
    if (!panel) return;
    const list = panel.querySelector('#timerList');
    if (!list) return;

    const ids = Object.keys(timers);
    if (ids.length === 0) {
      list.innerHTML = '<i>暂无正在运行的倒计时</i>';
      return;
    }

    let html = '';
    ids.forEach((id) => {
      const remain = DURATION_MS - (Date.now() - timers[id]);
      const m = Math.floor(remain / 60000);
      const s = Math.floor((remain % 60000) / 1000);
      html += `<div style="margin:4px 0;">
        计划ID:<b>${id}</b> |
        剩余 ${m}分${s.toString().padStart(2, '0')}秒
        <button data-id="${id}" style="margin-left:6px;background:#e33;color:#fff;border:none;border-radius:4px;padding:1px 4px;cursor:pointer;">❌清除</button>
      </div>`;
    });

    list.innerHTML = html;
    list.querySelectorAll('button[data-id]').forEach((btn) => {
      btn.onclick = () => removeCountdown(btn.dataset.id);
    });
  }

  // 删除倒计时
  function removeCountdown(planId) {
    const row = findRowByPlanId(planId);
    if (row) row.querySelector('.tm-countdown')?.remove();
    if (countdownMap.has(planId)) clearInterval(countdownMap.get(planId));

    delete timers[planId];
    countdownMap.delete(planId);
    GM_setValue(STORAGE_KEY, timers);
    console.log(`🧹 已清除倒计时 | 计划 ${planId}`);
    updateManagerPanel();
  }
})();