<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>夸克自动转存</title>
  <!-- CSS -->
  <link rel="stylesheet" href="./static/css/bootstrap.min.css">
  <link rel="stylesheet" href="./static/css/bootstrap-icons.min.css">
  <link rel="stylesheet" href="./static/css/main.css">
  <!-- Bootstrap JS -->
  <script src="./static/js/jquery-3.5.1.slim.min.js"></script>
  <script src="./static/js/bootstrap.bundle.min.js"></script>
  <!-- Vue.js -->
  <script src="./static/js/vue@2.js"></script>
  <script src="./static/js/axios.min.js"></script>
  <script src="./static/js/v-jsoneditor.min.js"></script>
  <script src="./static/js/sort_file_by_name.js"></script>
  <script src="./static/js/pinyin-pro.min.js"></script>
  <script>
  // 添加检测文本溢出的自定义指令
  Vue.directive('check-overflow', {
    inserted: function(el, binding, vnode) {
      // 检查元素是否溢出
      const isOverflowing = el.scrollWidth > el.clientWidth;
      
      // 如果绑定了值，则绑定到该值对应的记录属性上
      if (binding.value) {
        const indexAndField = binding.value.split('|');
        const index = parseInt(indexAndField[0]);
        const field = indexAndField[1];
        
        // 设置记录的_isOverflowing属性
        // 获取记录数组
        const records = vnode.context.filteredHistoryRecords;
        if (records && records[index]) {
          // 初始化_isOverflowing属性（如果不存在）
          if (!records[index]._isOverflowing) {
            vnode.context.$set(records[index], '_isOverflowing', {});
          }
          // 设置对应字段的溢出状态
          vnode.context.$set(records[index]._isOverflowing, field, isOverflowing);
        }
      }
    },
    // 添加update钩子，在DOM更新时重新检测溢出状态
    update: function(el, binding, vnode) {
      // 检查元素是否溢出
      const isOverflowing = el.scrollWidth > el.clientWidth;
      
      // 如果绑定了值，则绑定到该值对应的记录属性上
      if (binding.value) {
        const indexAndField = binding.value.split('|');
        const index = parseInt(indexAndField[0]);
        const field = indexAndField[1];
        
        // 设置记录的_isOverflowing属性
        const records = vnode.context.filteredHistoryRecords;
        if (records && records[index]) {
          // 初始化_isOverflowing属性（如果不存在）
          if (!records[index]._isOverflowing) {
            vnode.context.$set(records[index], '_isOverflowing', {});
          }
          // 设置对应字段的溢出状态
          vnode.context.$set(records[index]._isOverflowing, field, isOverflowing);
        }
      }
    }
  });
  
  // 根据文件扩展名获取对应的Bootstrap图标类名
  function getFileIconClass(fileName, isDir = false) {
    // 如果是文件夹，返回文件夹图标
    if (isDir) {
      return 'bi-folder-fill';
    }

    // 获取文件扩展名（转为小写）
    const ext = fileName.toLowerCase().split('.').pop();

    // 视频文件
    const videoExts = ['mp4', 'mkv', 'avi', 'mov', 'rmvb', 'flv', 'wmv', 'm4v', 'ts', 'webm', '3gp', 'f4v'];
    if (videoExts.includes(ext)) {
      return 'bi-file-earmark-play';
    }

    // 音频文件
    const audioExts = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a', 'wma', 'ape', 'ac3', 'dts'];
    if (audioExts.includes(ext)) {
      return 'bi-file-earmark-music';
    }

    // 图片文件
    const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'ico', 'raw'];
    if (imageExts.includes(ext)) {
      return 'bi-file-earmark-image';
    }

    // 文本文件（包括歌词文件和字幕文件）
    const textExts = ['txt', 'md', 'rtf', 'log', 'ini', 'cfg', 'conf', 'lrc', 'srt', 'ass', 'ssa', 'vtt', 'sup'];
    if (textExts.includes(ext)) {
      return 'bi-file-earmark-text';
    }

    // 富文本文件
    const richtextExts = ['rtf', 'odt'];
    if (richtextExts.includes(ext)) {
      return 'bi-file-earmark-richtext';
    }

    // 压缩文件
    const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'lzma', 'cab', 'iso'];
    if (archiveExts.includes(ext)) {
      return 'bi-file-earmark-zip';
    }

    // 字体文件
    const fontExts = ['ttf', 'otf', 'woff', 'woff2', 'eot'];
    if (fontExts.includes(ext)) {
      return 'bi-file-earmark-font';
    }

    // 代码文件
    const codeExts = ['js', 'html', 'css', 'py', 'java', 'c', 'cpp', 'php', 'go', 'json', 'xml', 'yml', 'yaml', 'sql', 'sh', 'bat', 'ps1', 'rb', 'swift', 'kt', 'ts', 'jsx', 'tsx', 'vue', 'scss', 'sass', 'less'];
    if (codeExts.includes(ext)) {
      return 'bi-file-earmark-code';
    }

    // PDF文件
    if (ext === 'pdf') {
      return 'bi-file-earmark-pdf';
    }

    // Word文档
    const wordExts = ['doc', 'docx'];
    if (wordExts.includes(ext)) {
      return 'bi-file-earmark-word';
    }

    // Excel文档
    const excelExts = ['xls', 'xlsx', 'csv'];
    if (excelExts.includes(ext)) {
      return 'bi-file-earmark-excel';
    }

    // PowerPoint文档
    const pptExts = ['ppt', 'pptx'];
    if (pptExts.includes(ext)) {
      return 'bi-file-earmark-ppt';
    }

    // 医疗/健康相关文件
    const medicalExts = ['dcm', 'dicom', 'hl7'];
    if (medicalExts.includes(ext)) {
      return 'bi-file-earmark-medical';
    }

    // 默认文件图标
    return 'bi-file-earmark';
  }

  // 添加检测文件整理页面文件名溢出的自定义指令
  Vue.directive('check-file-overflow', {
    inserted: function(el, binding, vnode) {
      // 检查元素是否溢出
      const isOverflowing = el.scrollWidth > el.clientWidth;

      // 如果绑定了值，则绑定到该值对应的文件属性上
      if (binding.value) {
        const indexAndField = binding.value.split('|');
        const index = parseInt(indexAndField[0]);
        const field = indexAndField[1];

        // 设置文件的_isOverflowing属性
        const files = vnode.context.fileManager.fileList;
        if (files && files[index]) {
          // 初始化_isOverflowing属性（如果不存在）
          if (!files[index]._isOverflowing) {
            vnode.context.$set(files[index], '_isOverflowing', {});
          }
          // 设置对应字段的溢出状态
          vnode.context.$set(files[index]._isOverflowing, field, isOverflowing);
        }
      }
    },
    // 添加update钩子，在DOM更新时重新检测溢出状态
    update: function(el, binding, vnode) {
      // 检查元素是否溢出
      const isOverflowing = el.scrollWidth > el.clientWidth;

      // 如果绑定了值，则绑定到该值对应的文件属性上
      if (binding.value) {
        const indexAndField = binding.value.split('|');
        const index = parseInt(indexAndField[0]);
        const field = indexAndField[1];

        // 设置文件的_isOverflowing属性
        const files = vnode.context.fileManager.fileList;
        if (files && files[index]) {
          // 初始化_isOverflowing属性（如果不存在）
          if (!files[index]._isOverflowing) {
            vnode.context.$set(files[index], '_isOverflowing', {});
          }
          // 设置对应字段的溢出状态
          vnode.context.$set(files[index]._isOverflowing, field, isOverflowing);
        }
      }
    }
  });
  
  // 添加检测模态框表格文本溢出的自定义指令
  Vue.directive('check-modal-overflow', {
    inserted: function(el, binding, vnode) {
      // 检查元素是否溢出
      const isOverflowing = el.scrollWidth > el.clientWidth;
      
      // 如果绑定了值，则绑定到该值对应的文件属性上
      if (binding.value) {
        const indexAndField = binding.value.split('|');
        const index = parseInt(indexAndField[0]);
        const field = indexAndField[1];
        
        // 设置文件的_isOverflowing属性
        const files = vnode.context.fileSelect.fileList;
        if (files && files[index]) {
          // 初始化_isOverflowing属性（如果不存在）
          if (!files[index]._isOverflowing) {
            vnode.context.$set(files[index], '_isOverflowing', {});
          }
          // 设置对应字段的溢出状态
          vnode.context.$set(files[index]._isOverflowing, field, isOverflowing);
        }
      }
    },
    // 添加update钩子，在DOM更新时重新检测溢出状态
    update: function(el, binding, vnode) {
      // 检查元素是否溢出
      const isOverflowing = el.scrollWidth > el.clientWidth;
      
      // 如果绑定了值，则绑定到该值对应的文件属性上
      if (binding.value) {
        const indexAndField = binding.value.split('|');
        const index = parseInt(indexAndField[0]);
        const field = indexAndField[1];
        
        // 设置文件的_isOverflowing属性
        const files = vnode.context.fileSelect.fileList;
        if (files && files[index]) {
          // 初始化_isOverflowing属性（如果不存在）
          if (!files[index]._isOverflowing) {
            vnode.context.$set(files[index], '_isOverflowing', {});
          }
          // 设置对应字段的溢出状态
          vnode.context.$set(files[index]._isOverflowing, field, isOverflowing);
        }
      }
    }
  });
  
  // 添加中文数字转阿拉伯数字的函数
  function chineseToArabic(chinese) {
    if (!chinese) {
      return null;
    }
    
    // 数字映射
    const digitMap = {
      '零': 0, '一': 1, '二': 2, '三': 3, '四': 4, 
      '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, 
      '两': 2
    };
    
    // 单位映射
    const unitMap = {
      '十': 10, 
      '百': 100, 
      '千': 1000, 
      '万': 10000
    };
    
    // 如果是单个字符，直接返回对应数字
    if (chinese.length === 1) {
      if (chinese === '十') return 10;
      return digitMap[chinese];
    }
    
    let result = 0;
    let section = 0;
    let number = 0;
    
    // 从左向右处理
    for (let i = 0; i < chinese.length; i++) {
      const char = chinese[i];
      
      if (char in digitMap) {
        number = digitMap[char];
      } else if (char in unitMap) {
        const unit = unitMap[char];
        // 如果前面没有数字，默认为1，例如"十"表示1*10=10
        section += (number || 1) * unit;
        number = 0;
        
        // 如果是万级单位，累加到结果并重置section
        if (unit === 10000) {
          result += section;
          section = 0;
        }
      } else {
        // 非法字符
        return null;
      }
    }
    
    // 加上最后的数字和小节
    result += section + number;
    
    return result;
  }
  </script>
</head>

<body>
  <div id="app">
    <!-- 添加通知组件 -->
    <div class="toast-container toast-container-center">
      <div class="toast toast-custom" ref="toast" data-delay="3000">
        <div class="toast-body toast-body-custom">
          {{ toastMessage }}
        </div>
      </div>
    </div>

    <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
      <a class="navbar-brand col-md-2 col-lg-2 mr-0 px-3" href="#" @click.prevent="window.innerWidth > 767.98 && toggleSidebar()" :class="{'sidebar-collapsed-navbar-brand': sidebarCollapsed}" :title="sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'">
        <i class="bi bi-clouds"></i> <span class="navbar-title">夸克自动转存</span>
      </a>
      <!-- 顶部操作按钮组 -->
      <div class="navbar-actions">
        <button type="button" class="navbar-action-btn" title="保存（CTRL+S）" @click="saveConfig()">
          <i class="bi bi-check2"></i>
        </button>
        <button type="button" class="navbar-action-btn" title="运行全部任务（CTRL+R）" @click="runScriptNow()">
          <i class="bi bi-caret-right"></i>
        </button>
        <button type="button" class="navbar-action-btn" title="单击回到顶部/双击回到底部" @click="scrollToX(0)" @dblclick="scrollToX()">
          <i class="bi bi-chevron-expand"></i>
        </button>
        <button type="button" class="navbar-action-btn d-none d-md-inline-block" :title="'页面宽度：' + (pageWidthMode === 'narrow' ? '窄' : pageWidthMode === 'medium' ? '中' : '宽')" @click="togglePageWidth()">
          <i class="bi bi-arrows"></i>
        </button>
      </div>
      <button class="navbar-toggler navbar-toggler-square position-absolute d-md-none collapsed" type="button" data-toggle="collapse" data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false">
        <i class="bi bi-list"></i>
      </button>
    </nav>

    <div class="container-fluid">
      <nav id="sidebarMenu" class="col-md-2 col-lg-2 d-md-block bg-light sidebar collapse" :class="{'sidebar-collapsed': sidebarCollapsed}">
        <div class="sidebar-sticky">
          <ul class="nav flex-column">
            <li class="nav-item">
              <a class="nav-link" href="#" :class="{active: activeTab === 'tasklist'}" @click="changeTab('tasklist')">
                <i class="bi bi-list-ul"></i> <span class="nav-text">任务列表</span>
              </a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#" :class="{active: activeTab === 'history'}" @click="changeTab('history')">
                <i class="bi bi-clock-history"></i> <span class="nav-text">转存记录</span>
              </a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#" :class="{active: activeTab === 'filemanager'}" @click="changeTab('filemanager')">
                <i class="bi bi-archive"></i> <span class="nav-text">文件整理</span>
              </a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#" :class="{active: activeTab === 'discovery'}" @click="changeTab('discovery')">
                <i class="bi bi-film"></i> <span class="nav-text">影视发现</span>
              </a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#" :class="{active: activeTab === 'calendar'}" @click="changeTab('calendar')">
                <i class="bi bi-calendar3-week"></i> <span class="nav-text">追剧日历</span>
              </a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#" :class="{active: activeTab === 'config'}" @click="changeTab('config')">
                <i class="bi bi-gear"></i> <span class="nav-text">系统配置</span>
              </a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="/logout">
                <i class="bi bi-power"></i> <span class="nav-text">退出</span>
              </a>
            </li>
          </ul>
          <div class="bottom-links">
            <ul class="nav flex-column">
              <li class="nav-item">
                <a class="nav-link" href="https://github.com/x1ao4/quark-auto-save-x/wiki" target="_blank">
                  <i class="bi bi-book"></i>
                  <span class="nav-text">Quark Auto Save X</span>
                </a>
              </li>
              <li class="nav-item">
                <a class="nav-link" href="https://github.com/x1ao4/quark-auto-save-x/releases" target="_blank">
                  <i class="bi bi-github"></i>
                  <span class="nav-text"><span v-html="versionTips"></span></span>
                </a>
              </li>
              <li class="nav-item">
                <a class="nav-link" href="https://github.com/x1ao4/quark-auto-save-x/wiki/赞赏与交流群" target="_blank">
                  <i class="bi bi-wechat"></i>
                  <span class="nav-text">WeChat Group</span>
                </a>
              </li>
            </ul>
          </div>
        </div>
      </nav>

      <main class="col-md-10 col-lg-10 ml-sm-auto">
        <form @submit.prevent="saveConfig" @keydown.enter.prevent>

          <div v-if="activeTab === 'config'">
            <div style="height: 0.5px;"></div>
            <div class="row title">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">账号设置</h2>
                <span class="badge badge-pill badge-light">
                  <a href="#" title="夸克自动转存WebUI账号设置"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div class="row mb-2">
              <div class="col webui-username-col">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">用户名</span>
                  </div>
                  <input type="text" v-model="formData.webui.username" class="form-control" placeholder="登录用户名">
                </div>
              </div>
              <div class="col webui-password-col">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">密码</span>
                  </div>
                  <input :type="showWebuiPassword ? 'text' : 'password'" v-model="formData.webui.password" class="form-control" placeholder="登录密码">
                  <div class="input-group-append">
                    <button type="button" class="btn btn-outline-secondary" @click="toggleWebuiPassword">
                      <i :class="['bi', showWebuiPassword ? 'bi-eye' : 'bi-eye-slash']"></i>
                    </button>
                  </div>
                </div>
              </div>
            </div>

            <div class="row title">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">Cookie</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://github.com/x1ao4/quark-auto-save-x/wiki/使用技巧集锦" target="_blank" title="夸克网盘账号的Cookie，用于转存、签到和文件整理，查阅Wiki了解详情"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div v-for="(value, index) in formData.cookie" :key="index" class="input-group mb-2">
              <div class="input-group-prepend" v-if="userInfoList[index]">
                <span class="input-group-text" 
                      :style="userInfoList[index].nickname ? (userInfoList[index].is_active ? 'color: var(--dark-text-color);' : 'color: red;') : 'color: var(--dark-text-color);'"
                      :title="getCookieStatusTooltip(userInfoList[index])">
                  {{ userInfoList[index].nickname || (userInfoList[index].has_mparam ? '仅签到' : '未登录') }}
                </span>
              </div>
              <div class="input-group-prepend" v-else>
                <span class="input-group-text" style="color: var(--dark-text-color);" title="账号信息未验证，请获取Cookie后自动验证">未验证</span>
              </div>
              <input type="text" v-model="formData.cookie[index]" class="form-control" placeholder="打开 pan.quark.com 按 F12 抓取" @change="fetchUserInfo" title="所有账号都会进行签到（纯签到只需填写移动端参数），只有第一个账号会进行转存，请自行确认账号顺序，所有填写了Cookie的账号均支持文件整理，如需签到请在Cookie后方添加签到参数">
              <div class="input-group-append">
                <button v-if="index === formData.cookie.length - 1" type="button" class="btn btn-outline-primary" @click="addCookie()"><i class="bi bi-plus-lg"></i></button>
                <button type="button" class="btn btn-outline-danger" @click="removeCookie(index)"><i class="bi bi-dash-lg"></i></button>
              </div>
            </div>

            <div class="row title">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">定时规则</h2>
                <span class="badge badge-pill badge-light">
                  <a target="_blank" href="https://github.com/x1ao4/quark-auto-save-x/wiki/定时规则" title="设置任务的定时、延迟规则与执行周期模式，查阅Wiki了解详情"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div class="row mb-2 crontab-setting-row">
              <div class="col-lg-4 col-md-6 col-12 mb-2 mb-lg-0">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">
                      <a href="https://tool.lu/crontab/" target="_blank" rel="noopener noreferrer" class="crontab-link" title="点击查看Crontab执行时间计算器">Crontab</a>
                    </span>
                  </div>
                  <input type="text" v-model="formData.crontab" class="form-control" placeholder="必填" title="支持标准Crontab表达式，例如 0 * * * * 表示在每个整点执行一次">
                </div>
              </div>
              <div class="col-lg-4 col-md-6 col-12 mb-2 mb-lg-0">
                <div class="input-group" title="添加随机延迟时间：定时任务将在0到设定秒数之间随机延迟执行。建议值：0–3600，0表示不延迟">
                  <div class="input-group-prepend">
                    <span class="input-group-text">延迟执行</span>
                  </div>
                  <input type="text" v-model="formData.crontab_delay" class="form-control no-spinner" placeholder="0-3600" @input="validateNumberInput($event, 'crontab_delay', 3600)">
                  <div class="input-group-append">
                    <span class="input-group-text square-append">秒</span>
                  </div>
                </div>
              </div>
              <div class="col-lg-4 col-md-6 col-12 mb-2 mb-lg-0">
                <div class="input-group" title="执行周期判断方式：按自选周期执行（自选）使用任务的执行周期和截止日期判断；按任务进度执行（自动）根据任务进度是否100%判断，100%则跳过">
                  <div class="input-group-prepend">
                    <span class="input-group-text">执行周期</span>
                  </div>
                  <select v-model="formData.execution_mode" class="form-control" @change="onExecutionModeChange">
                    <option value="manual">按自选周期执行（自选）</option>
                    <option value="auto">按任务进度执行（自动）</option>
                  </select>
                </div>
              </div>
            </div>

            <div class="row title" title="通知推送，支持多个渠道，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">通知设置</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://github.com/x1ao4/quark-auto-save-x/wiki/通知设置" target="_blank"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div class="input-group mb-2">
              <div class="input-group-prepend">
                <span class="input-group-text">推送通知</span>
              </div>
              <select v-model="formData.push_notify_type" class="form-control">
                <option value="full">推送完整信息（转存成功、转存失败、资源失效）</option>
                <option value="success_only">推送成功信息（转存成功）</option>
                <option value="exclude_invalid">排除失效信息（转存成功、转存失败）</option>
              </select>
            </div>
            <div v-for="(value, key, index) in formData.push_config" :key="key" class="input-group mb-2">
              <div class="input-group-prepend">
                <span class="input-group-text" v-html="key"></span>
              </div>
              <div class="input-group-prepend" v-if="(key == 'DEER_KEY' || key == 'PUSH_KEY')">
                <a type="button" class="btn btn-warning" target="_blank" href="https://sct.ftqq.com/r/13249" title="Server酱推荐计划"><i class="bi bi-award"></i></a>
              </div>
              <input type="text" v-model="formData.push_config[key]" class="form-control">
              <div class="input-group-append">
                <button v-if="index === Object.keys(formData.push_config).length - 1" type="button" class="btn btn-outline-primary" @click="addPush()"><i class="bi bi-plus-lg"></i></button>
                <button type="button" class="btn btn-outline-danger" @click="removePush(key)"><i class="bi bi-dash-lg"></i></button>
              </div>
            </div>

            <div class="row title" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length" title="插件设置，具体键值由插件定义，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">插件设置</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://github.com/x1ao4/quark-auto-save-x/wiki/插件设置" target="_blank"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div v-for="(plugin, pluginName) in getAvailablePlugins(formData.plugins)" :key="pluginName" style="margin-bottom: -8px;" :style="pluginName === Object.keys(getAvailablePlugins(formData.plugins))[0] ? 'margin-top: -8px;' : ''">
              <div class="form-group row mb-0" style="display:flex; align-items:center;">
                <div data-toggle="collapse" :data-target="'#collapse_'+pluginName" aria-expanded="true" :aria-controls="'collapse_'+pluginName">
                  <div class="btn btn-block text-left">
                    <i class="bi bi-caret-right-fill"></i> <span v-html="getPluginDisplayName(pluginName)"></span>
                  </div>
                </div>
              </div>
              <div class="collapse" :id="'collapse_'+pluginName" style="margin-left: 26px;">
                <div v-for="(value, key, keyIndex) in plugin" :key="key" class="input-group mb-2" :style="{ marginBottom: keyIndex === Object.keys(plugin).length - 1 && pluginName === Object.keys(getAvailablePlugins(formData.plugins)).pop() ? '8.5px !important' : '' }">
                  <div class="input-group-prepend">
                    <span class="input-group-text" v-html="key" :title="getPluginConfigHelp(pluginName, key)"></span>
                  </div>
                  <input type="text" v-model="formData.plugins[pluginName][key]" class="form-control"
                    :placeholder="getPluginConfigPlaceholder(pluginName, key)"
                    :title="getPluginConfigHelp(pluginName, key)">
                </div>
                <!-- 为特定插件添加全局配置选项 -->
                <div v-if="['aria2', 'alist_strm_gen', 'emby'].includes(pluginName)" class="input-group mb-2">
                  <div class="input-group-prepend">
                    <span class="input-group-text" :title="getPluginConfigModeHelp(pluginName)">插件配置模式</span>
                  </div>
                  <select class="form-control" v-model="formData.plugin_config_mode[pluginName]" @change="onPluginConfigModeChange(pluginName)" :title="getPluginConfigModeHelp(pluginName)">
                    <option value="independent">独立配置</option>
                    <option value="global">全局配置</option>
                  </select>
                </div>
                <!-- 全局插件配置选项 -->
                <div v-if="['aria2', 'alist_strm_gen', 'emby'].includes(pluginName) && formData.plugin_config_mode[pluginName] === 'global'">
                  <div v-for="(taskConfig, taskKey) in getPluginTaskConfig(pluginName)" :key="taskKey" class="input-group mb-2">
                    <div class="input-group-prepend">
                      <span class="input-group-text" v-html="taskKey" :title="getPluginTaskConfigHelp(pluginName, taskKey)"></span>
                    </div>
                    <select v-if="typeof formData.global_plugin_config[pluginName][taskKey] === 'boolean'" class="form-control" v-model="formData.global_plugin_config[pluginName][taskKey]" @change="onGlobalPluginConfigChange()" :title="getPluginTaskConfigHelp(pluginName, taskKey)">
                      <option :value="true">true</option>
                      <option :value="false">false</option>
                    </select>
                    <input v-else type="text" class="form-control" v-model="formData.global_plugin_config[pluginName][taskKey]"
                      :placeholder="getPluginTaskConfigPlaceholder(pluginName, taskKey)"
                      :title="getPluginTaskConfigHelp(pluginName, taskKey)"
                      @input="onGlobalPluginConfigChange()">
                  </div>
                </div>
              </div>
            </div>
            
            <div class="row title" title="资源搜索服务配置，用于任务名称智能搜索，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">搜索来源</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://github.com/x1ao4/quark-auto-save-x/wiki/资源搜索" target="_blank"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <!-- 按插件风格显示为可展开项 -->
            <div style="margin-bottom: -8px;">
              <!-- CloudSaver -->
              <div style="margin-bottom: -8px; margin-top: -8px;">
                <div class="form-group row mb-0" style="display:flex; align-items:center;">
                  <div data-toggle="collapse" data-target="#collapse_source_cloudsaver" aria-expanded="true" aria-controls="collapse_source_cloudsaver">
                    <div class="btn btn-block text-left">
                      <i class="bi bi-caret-right-fill"></i> CloudSaver
                    </div>
                  </div>
                </div>
                <div class="collapse" id="collapse_source_cloudsaver" style="margin-left: 26px;">
                  <div class="input-group mb-2">
                    <div class="input-group-prepend">
                      <span class="input-group-text">服务器</span>
                    </div>
                    <input type="text" v-model="formData.source.cloudsaver.server" class="form-control" placeholder="CloudSaver 服务器地址，如：http://192.168.1.100:8008">
                  </div>
                  <div class="input-group mb-2">
                    <div class="input-group-prepend">
                      <span class="input-group-text">用户名</span>
                    </div>
                    <input type="text" v-model="formData.source.cloudsaver.username" class="form-control" placeholder="CloudSaver 用户名">
                  </div>
                  <div class="input-group mb-2">
                    <div class="input-group-prepend">
                      <span class="input-group-text">密码</span>
                    </div>
                    <input :type="showCloudSaverPassword ? 'text' : 'password'" v-model="formData.source.cloudsaver.password" class="form-control" placeholder="CloudSaver 密码">
                    <div class="input-group-append">
                      <button type="button" class="btn btn-outline-secondary" @click="toggleCloudSaverPassword">
                        <i :class="['bi', showCloudSaverPassword ? 'bi-eye' : 'bi-eye-slash']"></i>
                      </button>
                    </div>
                  </div>
                </div>
              </div>

              <!-- PanSou -->
              <div style="margin-bottom: -9.5px;">
                <div class="form-group row mb-0" style="display:flex; align-items:center;">
                  <div data-toggle="collapse" data-target="#collapse_source_pansou" aria-expanded="false" aria-controls="collapse_source_pansou">
                    <div class="btn btn-block text-left">
                      <i class="bi bi-caret-right-fill"></i> PanSou
                    </div>
                  </div>
                </div>
                <div class="collapse" id="collapse_source_pansou" style="margin-left: 26px;">
                  <div class="input-group mb-2" style="margin-bottom: 10.5px !important;">
                    <div class="input-group-prepend">
                      <span class="input-group-text">服务器</span>
                    </div>
                    <input type="text" v-model="formData.source.pansou.server" class="form-control" placeholder="PanSou 服务器地址，如：http://192.168.1.100:80，默认地址 https://so.252035.xyz 为 PanSou 官方地址，建议自部署以获得更稳定的体验">
                  </div>
                </div>
              </div>
            </div>

            <div class="row title" title="预定义的正则匹配规则，在任务列表中可直接点击使用，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">魔法匹配</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://github.com/x1ao4/quark-auto-save-x/wiki/命名规则#21-魔法匹配" target="_blank"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div v-for="(value, key, index) in formData.magic_regex" :key="key" class="form-group mb-2">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">魔法名</span>
                </div>
                <input type="text" :data-oldkey="key" v-model="key" class="form-control" @change="updateMagicRegexKey($event.target.dataset.oldkey, $event.target.value)" placeholder="自定义名称">
                <div class="input-group-prepend">
                  <span class="input-group-text">正则命名</span>
                </div>
                <input type="text" v-model="value.pattern" class="form-control" placeholder="匹配表达式">
                <input type="text" v-model="value.replace" class="form-control" placeholder="替换表达式">
                <div class="input-group-append">
                  <button v-if="index === Object.keys(formData.magic_regex).length - 1" type="button" class="btn btn-outline-primary" @click="addMagicRegex()"><i class="bi bi-plus-lg"></i></button>
                  <button type="button" class="btn btn-outline-danger" @click="removeMagicRegex(key)"><i class="bi bi-dash-lg"></i></button>
                </div>
              </div>
            </div>

            <!-- 剧集识别模块 -->
            <div class="row title" title="识别文件名中的剧集编号，用于自动重命名，留空时使用内置默认规则，支持输入自定义规则作为补充，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">剧集识别</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://github.com/x1ao4/quark-auto-save-x/wiki/命名规则#25-剧集命名" target="_blank"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">集编号识别规则</span>
                </div>
                <input type="text" class="form-control" v-model="episodePatternsText" placeholder="留空使用内置默认规则，或输入自定义正则表达式作为补充规则，多个表达式用竖线分隔，特殊符号需要转义">
              </div>
            </div>

            <!-- 任务设置模块 -->
            <div class="row title" title="配置影视发现页面创建任务时的智能填充规则，请使用你的网盘目录替换默认的目录前缀，否则将自动继承任务列表最后一个任务的保存路径，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">任务设置</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://github.com/x1ao4/quark-auto-save-x/wiki/影视发现任务设置" target="_blank"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">电影保存路径</span>
                </div>
                <input type="text" class="form-control" v-model="formData.task_settings.movie_save_path" placeholder="电影目录前缀/片名 (年份)" title="设置电影类型内容的默认保存路径格式，在影视发现页面创建电影任务时自动填充。支持变量：片名、年份等">
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">剧集保存路径</span>
                </div>
                <input type="text" class="form-control" v-model="formData.task_settings.tv_save_path" placeholder="剧集目录前缀/剧名 (年份)/剧名 - S季数" title="设置电视剧类型内容的默认保存路径格式，在影视发现页面创建剧集任务时自动填充。支持变量：剧名、年份、季数等">
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">动画保存路径</span>
                </div>
                <input type="text" class="form-control" v-model="formData.task_settings.anime_save_path" placeholder="动画目录前缀/剧名 (年份)/剧名 - S季数" title="设置动画类型内容的默认保存路径格式，在影视发现页面创建动画任务时自动填充。支持变量：剧名、年份、季数等">
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">综艺保存路径</span>
                </div>
                <input type="text" class="form-control" v-model="formData.task_settings.variety_save_path" placeholder="综艺目录前缀/剧名 (年份)/剧名 - S季数" title="设置综艺类型内容的默认保存路径格式，在影视发现页面创建综艺任务时自动填充。支持变量：剧名、年份、季数等">
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">纪录片保存路径</span>
                </div>
                <input type="text" class="form-control" v-model="formData.task_settings.documentary_save_path" placeholder="纪录片目录前缀/剧名 (年份)/剧名 - S季数" title="设置纪录片类型内容的默认保存路径格式，在影视发现页面创建纪录片任务时自动填充。支持变量：剧名、年份、季数等">
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">电影命名规则</span>
                </div>
                <input type="text" class="form-control" v-model="formData.task_settings.movie_naming_pattern" placeholder="^(.*)\.([^.]+)" title="设置电影类型内容的默认文件命名规则的匹配表达式，在影视发现页面创建电影任务时自动填充">
                <input type="text" class="form-control" v-model="formData.task_settings.movie_naming_replace" placeholder="片名 (年份).\2" title="设置电影类型内容的默认文件命名规则的替换表达式，在影视发现页面创建电影任务时自动填充。支持变量：片名、年份等">
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">电视命名规则</span>
                </div>
                <input type="text" class="form-control" v-model="formData.task_settings.tv_naming_rule" placeholder="剧名 - S季数E[]" title="设置电视类型内容的默认文件命名规则，在影视发现页面创建电视任务时自动填充。支持变量：剧名、季数、集数等，[]表示集数占位符">
                <div class="input-group-append" title="保存时只比较文件名的部分，01.mp4和01.mkv视同为同一文件，不重复转存">
                  <div class="input-group-text">
                    <input type="checkbox" v-model="formData.task_settings.tv_ignore_extension">&nbsp;忽略后缀
                  </div>
                </div>
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">字幕命名规则</span>
                </div>
                <input type="text" class="form-control" v-model="formData.task_settings.subtitle_naming_rule" placeholder="zh" title="设置字幕文件的语言代码，在重命名字幕文件时会在文件名末尾添加此代码">
                <div class="input-group-append" title="启用后，重命名字幕文件时会在文件名末尾添加语言代码后缀">
                  <div class="input-group-text">
                    <input type="checkbox" v-model="formData.task_settings.subtitle_add_language_code">&nbsp;添加语言代码
                  </div>
                </div>
              </div>
            </div>

            <div class="form-group">
              <div class="input-group">
                <div class="input-group-prepend">
                  <span class="input-group-text">自动搜索资源</span>
                </div>
                <select class="form-control" v-model="formData.task_settings.auto_search_resources" title="启用后，在影视发现页面点击海报创建任务时，如果配置了有效的CloudSaver信息，将自动触发资源搜索功能">
                  <option value="disabled">禁用</option>
                  <option value="enabled">启用</option>
                </select>
              </div>
            </div>

            <div class="row title" title="设置任务列表、追剧日历等页面的任务按钮和相关信息的显示及排序方式，支持拖拽模块调整显示顺序，个别项目的禁用状态将被应用到所有页面，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">显示设置</h2>
                <span class="badge badge-pill badge-light">
                  <a target="_blank" href="https://github.com/x1ao4/quark-auto-save-x/wiki/显示设置"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            
            <!-- 可拖拽显示设置（统一在一个容器中渲染，保持原样式，仅可拖动排序） -->
            <div class="row mb-2 display-setting-row" id="display-setting-draggable">
              <div class="col-lg-3 col-md-6 mb-2 draggable-item" v-for="key in formData.button_display_order" :key="key" draggable="true" @dragstart="onDisplayDragStart($event, key)" @dragend="onDisplayDragEnd($event)" @dragover.prevent="onDisplayDragOver($event)" @drop="onDisplayDrop($event, key)">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text" :title="getDisplayHelp(key)">{{ getDisplayLabel(key) }}</span>
                  </div>
                  <select v-if="key==='run_task' || key==='delete_task'" class="form-control" v-model="formData.button_display[key]" :title="getDisplayHelp(key)">
                    <option value="always">始终显示</option>
                    <option value="hover">悬停显示</option>
                  </select>
                  <select v-else class="form-control" v-model="formData.button_display[key]" :title="getDisplayHelp(key)">
                    <option value="always">始终显示</option>
                    <option value="hover">悬停显示</option>
                    <option value="disabled">禁用</option>
                  </select>
                </div>
              </div>
              
              <!-- 海报语言设置（不支持拖拽，但样式与其他设置一致） -->
              <div class="col-lg-3 col-md-6 mb-2">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text" title="任务列表与追剧日历的海报语言优先级，选择中文将优先使用zh-CN海报，选择原始语言将优先使用原始语言海报">海报语言</span>
                  </div>
                  <select class="form-control" v-model="formData.poster_language" title="任务列表与追剧日历的海报语言优先级，选择中文将优先使用zh-CN海报，选择原始语言将优先使用原始语言海报">
                    <option value="zh-CN">中文</option>
                    <option value="original">原始语言</option>
                  </select>
                </div>
              </div>
            </div>

            <!-- 性能设置 -->
            <div class="row title" title="配置文件加载、数据缓存和自动刷新的关键参数，以兼顾性能与效率，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">性能设置</h2>
                <span class="badge badge-pill badge-light">
                  <a target="_blank" href="https://github.com/x1ao4/quark-auto-save-x/wiki/性能设置"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div class="row mb-2 performance-setting-row">
              <div class="col-lg-3 col-md-6 mb-2">
                <div class="input-group" title="每次请求夸克API时获取的文件数量，适当增大该数值可减少请求次数，提升大文件夹的加载效率。建议值：100–500">
                  <div class="input-group-prepend">
                    <span class="input-group-text">单次请求文件数量</span>
                  </div>
                  <input type="text" class="form-control no-spinner" v-model="formData.file_performance.api_page_size" placeholder="200">
                  <div class="input-group-append">
                    <span class="input-group-text square-append">个</span>
                  </div>
                </div>
              </div>
              <div class="col-lg-3 col-md-6 mb-2">
                <div class="input-group" title="文件列表在本地缓存的持续时间，过期后将自动清除。设置过短会增加API请求次数，设置过长可能无法及时反映最新变动。建议值：0-300，0表示不缓存">
                  <div class="input-group-prepend">
                    <span class="input-group-text">文件列表缓存时长</span>
                  </div>
                  <input type="text" class="form-control no-spinner" v-model="formData.file_performance.cache_expire_time" placeholder="30">
                  <div class="input-group-append">
                    <span class="input-group-text square-append">秒</span>
                  </div>
                </div>
              </div>
              <div class="col-lg-3 col-md-6 mb-2">
                <div class="input-group" title="影视发现页面每个榜单显示的项目数量。建议值：20-100">
                  <div class="input-group-prepend">
                    <span class="input-group-text">影视榜单项目数量</span>
                  </div>
                  <input type="text" class="form-control no-spinner" v-model="formData.file_performance.discovery_items_count" placeholder="30">
                  <div class="input-group-append">
                    <span class="input-group-text square-append">个</span>
                  </div>
                </div>
              </div>
              <div class="col-lg-3 col-md-6 mb-2">
                <div class="input-group" title="追剧日历最新季元数据自动刷新周期，单位秒。设置为0或负数可关闭自动刷新。默认值：21600（6小时）">
                  <div class="input-group-prepend">
                    <span class="input-group-text">追剧日历刷新周期</span>
                  </div>
                  <input type="text" class="form-control no-spinner" v-model="formData.performance.calendar_refresh_interval_seconds" placeholder="21600">
                  <div class="input-group-append">
                    <span class="input-group-text square-append">秒</span>
                  </div>
                </div>
              </div>
              <div class="col-lg-3 col-md-6 mb-2">
                <div class="input-group" title="已播出集数（及任务进度）自动刷新时间，24小时制，格式：HH:MM。默认值：00:00（每天0点）">
                  <div class="input-group-prepend">
                    <span class="input-group-text">播出集数刷新时间</span>
                  </div>
                  <input type="text" class="form-control no-spinner" v-model="formData.performance.aired_refresh_time" placeholder="00:00">
                </div>
              </div>
            </div>
            <div class="row title" title="QASX API 支持第三方添加任务、开发插件及自动化操作，查阅Wiki了解详情">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">QASX API</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://github.com/x1ao4/quark-auto-save-x/wiki/API" target="_blank"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div class="input-group">
              <div class="input-group-prepend">
                <span class="input-group-text">Token</span>
              </div>
              <input type="text" v-model="formData.api_token" class="form-control token-display" disabled>
            </div>

            <div class="row title" title="TMDB API密钥，用于追剧日历功能获取电视节目信息和播出时间表，点击获取TMDB API密钥">
              <div class="col">
                <h2 style="display: inline-block; font-size: 1.5rem;">TMDB API</h2>
                <span class="badge badge-pill badge-light">
                  <a href="https://www.themoviedb.org/settings/api" target="_blank" title="TMDB API密钥，用于获取电视节目信息和播出时间表，点击获取TMDB API密钥"><i class="bi bi-question-circle"></i></a>
                </span>
              </div>
            </div>
            <div class="input-group">
              <div class="input-group-prepend">
                <span class="input-group-text">API 密钥</span>
              </div>
              <input type="text" v-model="formData.tmdb_api_key" class="form-control" placeholder="请输入 TMDB API 密钥，用于追剧日历功能">
            </div>
            <p class="tmdb-attribution">部分功能基于 <a href="https://www.themoviedb.org/" target="_blank">TMDB</a> 提供的数据实现，本应用与 TMDB 无关。</p>

          </div>

          <div v-if="activeTab === 'tasklist'">
            <div style="height: 20px;"></div>
            <div class="row" style="margin-bottom: 8px;">
              <div class="col-lg-6 col-md-6 mb-2 mb-md-0">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">名称筛选</span>
                  </div>
                  <input type="text" class="form-control" v-model="taskNameFilter" placeholder="任务名称关键词">
                  <div class="input-group-append">
                    <button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('taskNameFilter')"><i class="bi bi-x-lg"></i></button>
                  </div>
                </div>
              </div>
              <div class="col-lg-6 col-md-6">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">任务筛选</span>
                  </div>
                  <div class="position-relative" style="flex: 1;">
                    <select class="form-control task-filter-select" v-model="taskDirSelected" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 24px !important;">
                      <option value="">全部任务</option>
                      <option v-for="task in taskNames" :value="task" v-html="task"></option>
                    </select>
                    <!-- <i class="bi bi-chevron-down select-arrow" style="position: absolute; pointer-events: none; color: var(--dark-text-color);"></i> -->
                  </div>
                  <div class="input-group-append">
                    <button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('taskDirSelected')"><i class="bi bi-x-lg"></i></button>
                  </div>
                </div>
              </div>
            </div>
            <!-- 任务列表：类型筛选按钮和排序组件（复用追剧日历移动端样式） -->
            <div class="row mb-3 tasklist-header-row">
              <div class="col-lg-8 col-md-6">
                <!-- 类型筛选按钮 -->
                <div class="calendar-category-buttons tasklist-type-filter">
                  <button type="button"
                          class="btn btn-outline-secondary calendar-category-btn"
                          :class="{ active: tasklist.selectedType === 'all' }"
                          @click="selectTasklistType('all')">
                    全部
                  </button>
                  <button type="button"
                          class="btn btn-outline-secondary calendar-category-btn"
                          v-for="type in tasklist.contentTypes"
                          :key="'tasklist-'+type"
                          :class="{ active: tasklist.selectedType === type }"
                          @click="selectTasklistType(type)">
                    {{ getContentTypeDisplayName(type) }}
                  </button>
                </div>
              </div>
              <div class="col-lg-4 col-md-6">
                <!-- 排序组件 -->
                <div class="tasklist-sort-controls d-flex justify-content-end">
                  <div class="tasklist-sort-wrapper">
                    <span class="tasklist-sort-pill tasklist-sort-pill-icon" title="切换排序">按</span>
                    <select class="tasklist-sort-pill tasklist-sort-select task-filter-select" :value="tasklistSort.by" @change="changeTasklistSortBy($event.target.value)" title="选择排序方式">
                      <option value="index">任务编号</option>
                      <option value="name">任务名称</option>
                      <option v-if="formData.tmdb_api_key" value="progress">任务进度</option>
                      <option value="update_time">更新时间</option>
                    </select>
                    <span class="tasklist-sort-pill tasklist-sort-order" @click="toggleTasklistSortOrder" title="切换升降序">
                      {{ tasklistSort.order === 'asc' ? '升序排列' : '降序排列' }}
                    </span>
                  </div>
                  <!-- 视图切换按钮：列表视图 与 海报视图 -->
                  <div class="ml-2">
                    <button type="button"
                            class="btn btn-outline-secondary btn-sm tasklist-view-toggle-btn"
                            @click="toggleTasklistViewMode"
                            :title="tasklist.viewMode === 'list' ? '当前：列表视图，点击切换到海报视图' : '当前：海报视图，点击切换到列表视图'">
                      <i :class="tasklist.viewMode === 'list' ? 'bi bi-grid-3x3-gap' : 'bi bi-list-ol'"></i>
                    </button>
                  </div>
                  <!-- 创建任务按钮 -->
                  <div class="ml-2">
                    <button type="button"
                            class="btn btn-outline-secondary btn-sm tasklist-create-task-btn"
                            @click="openCreateTaskModal"
                            title="创建任务">
                      <i class="bi bi-plus-lg"></i>
                    </button>
                  </div>
              </div>
            </div>
            </div>
            <!-- 任务列表：列表视图 -->
            <template v-if="tasklist.viewMode === 'list'">
            <div v-for="(task, index) in sortedTasklist" :key="'list-'+index" class="task mb-3" v-show="shouldShowTasklist">
              <template v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter) && tasklistFilterByType(task)">
                <hr>
                <div class="form-group row" style="align-items:center">
                  <div class="col pl-0" data-toggle="collapse" :data-target="'#collapse_'+index" aria-expanded="true" :aria-controls="'collapse_'+index">
                    <div class="btn btn-block text-left">
                      <i class="bi bi-caret-right-fill"></i> #<span v-html="`${String((task.__originalIndex !== undefined ? task.__originalIndex : index) + 1).padStart(2, '0')} ${task.taskname}`"></span>
                      <template v-for="key in formData.button_display_order">
                        <span v-if="key==='latest_transfer_file' && formData.button_display.latest_transfer_file !== 'disabled' && taskLatestFiles[task.taskname]"
                              class="task-latest-file"
                              :class="{'hover-only': formData.button_display.latest_transfer_file === 'hover'}">
                           · {{ taskLatestFiles[task.taskname] }}
                        </span>
                        <span v-else-if="key==='season_counts' && formData.button_display.season_counts !== 'disabled' && getTaskSeasonCounts(task.taskname)"
                              class="task-season-counts"
                              :class="{'hover-only': formData.button_display.season_counts === 'hover'}"
                              v-html="' · ' + formatSeasonCounts(getTaskSeasonCounts(task.taskname))">
                        </span>
                        <span v-else-if="key==='latest_update_date' && formData.button_display.latest_update_date !== 'disabled' && taskLatestRecords[task.taskname]"
                              class="task-latest-date"
                              :class="{'hover-only': formData.button_display.latest_update_date === 'hover'}">
                           · {{ getTaskLatestRecordDisplay(task.taskname) }}
                        </span>
                        <span v-else-if="key==='task_progress' && formData.button_display.task_progress !== 'disabled' && getTaskProgress(task.taskname) !== null"
                              class="task-progress"
                              :class="{'hover-only': formData.button_display.task_progress === 'hover'}">
                           · {{ getTaskProgress(task.taskname) }}%
                        </span>
                        <span v-else-if="key==='show_status' && formData.button_display.show_status !== 'disabled' && getTaskShowStatus(task.taskname)"
                              class="task-show-status"
                              :class="{'hover-only': formData.button_display.show_status === 'hover'}">
                           · {{ getTaskShowStatus(task.taskname) }}
                        </span>
                      </template>
                      <span v-if="isTaskUpdatedToday(task.taskname) && shouldShowTodayIndicator()"
                            class="task-today-indicator"
                            :class="getTodayIndicatorClass()">
                        <i class="bi bi-stars"></i>
                      </span>
                    </div>
                  </div>
                  <div class="col-auto task-buttons">
                    <template v-for="key in formData.button_display_order">
                      <button v-if="key==='refresh_plex' && formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" type="button" class="btn btn-outline-plex" :class="{'hover-only': formData.button_display.refresh_plex === 'hover'}" @click="refreshPlexLibrary(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="刷新Plex媒体库"><img src="./static/images/Plex.svg" class="plex-icon"></button>
                      <button v-else-if="key==='refresh_alist' && formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" type="button" class="btn btn-outline-alist" :class="{'hover-only': formData.button_display.refresh_alist === 'hover'}" @click="refreshAlistDirectory(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
                      <template v-else-if="key==='run_task'">
                        <button v-if="!task.shareurl_ban" type="button" class="btn btn-outline-primary" :class="{'hover-only': formData.button_display.run_task === 'hover'}" @click="runScriptNow(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="运行此任务"><i class="bi bi-caret-right"></i></button>
                        <button v-else type="button" class="btn btn-warning" :title="formatShareUrlBanMessage(task.shareurl_ban) || task.shareurl_ban" disabled><i class="bi bi-exclamation-circle"></i></button>
                      </template>
                      <button v-else-if="key==='delete_task'" type="button" class="btn btn-outline-danger" :class="{'hover-only': formData.button_display.delete_task === 'hover'}" @click="removeTask(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="删除此任务"><i class="bi bi-trash3"></i></button>
                    </template>
                  </div>
                </div>
                <div class="collapse ml-3" :id="'collapse_'+index">
                  <div class="alert alert-warning" role="alert" v-if="task.shareurl_ban" v-html="formatShareUrlBanMessage(task.shareurl_ban) || task.shareurl_ban"></div>
                  <div class="form-group row">
                    <label class="col-sm-2 col-form-label">任务名称</label>
                    <div class="col-sm-10">
                      <div class="input-group">
                        <input type="text" name="taskname[]" class="form-control" v-model="task.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(task.__originalIndex !== undefined ? task.__originalIndex : index, task)" @input="changeTaskname(task.__originalIndex !== undefined ? task.__originalIndex : index, task)">
                        <div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.index === (task.__originalIndex !== undefined ? task.__originalIndex : index) && (smart_param.taskSuggestions.success || smart_param.isSearching)">
                          <div class="dropdown-item text-muted" v-if="smart_param.isSearching" style="font-size:14px; padding: 0 8px; text-align: left;">
                            <span v-if="smart_param.validating">
                              <i class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></i> 
                              正在验证链接有效性...（{{ smart_param.validateProgress.current }}/{{ smart_param.validateProgress.total }}）<span v-if="smart_param.validateProgress.valid > 0">已找到 {{ smart_param.validateProgress.valid }} 个有效链接</span>
                            </span>
                            <span v-else>正在搜索资源...</span>
                          </div>
                          <div class="dropdown-item text-muted" v-else style="font-size:14px; padding-left: 8px; text-align: left;">
                            {{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length ? `以下资源由 ${(smart_param.taskSuggestions.source || '').replace(/,\s*/g, '、')} 搜索提供（仅显示有效链接，共 ${(smart_param.taskSuggestions.data || []).length} 个），如有侵权请联系资源发布方` : "未搜索到有效资源" }}
                          </div>
                          <div v-for="suggestion in smart_param.taskSuggestions.data || []" :key="(suggestion.shareurl || '') + '_' + (suggestion.taskname || '') + '_' + (suggestion.publish_date || '')" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(task.__originalIndex !== undefined ? task.__originalIndex : index, suggestion)" style="font-size: 14px;" :title="getSuggestionHoverTitle(suggestion)">
                            <span v-html="suggestion.verify ? '✅': ''"></span> {{ suggestion.taskname }}
                            <small class="text-muted">
                              <a :href="suggestion.shareurl" target="_blank" @click.stop> · {{ suggestion.shareurl.replace(/^https?:\/\/pan\.quark\.cn\/s\//, '') }}</a>
                              <template v-if="suggestion.source"><span class="source-badge" :class="suggestion.source.toLowerCase()" :data-publish-date="suggestion.publish_date ? ' · ' + formatPublishDate(suggestion.publish_date, suggestion.pansou_source, suggestion.source) : ''">{{ suggestion.source }}</span></template>
                            </small>
                          </div>
                        </div>
                        <div class="input-group-append" title="资源搜索">
                          <button class="btn btn-primary" type="button" @click="searchSuggestions(task.__originalIndex !== undefined ? task.__originalIndex : index, task.taskname)">
                            <i v-if="smart_param.isSearching && smart_param.index === (task.__originalIndex !== undefined ? task.__originalIndex : index)" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></i>
                            <i v-else class="bi bi-search"></i>
                          </button>
                          <div class="input-group-text" title="谷歌搜索">
                            <a target="_blank" :href="`https://www.google.com/search?q=%22pan.quark.cn/s%22+${cleanTaskNameForSearch(task.taskname)}`"><i class="bi bi-google"></i></a>
                          </div>
                          <div class="input-group-text" title="TMDB搜索">
                            <a target="_blank" :href="`https://www.themoviedb.org/search?query=${cleanTaskNameForSearch(task.taskname)}`"><img src="./static/images/TMDB.svg" class="tmdb-icon"></a>
                          </div>
                          <div class="input-group-text" title="豆瓣搜索">
                            <a target="_blank" :href="`https://search.douban.com/movie/subject_search?search_text=${cleanTaskNameForSearch(task.taskname)}`"><img src="./static/images/Douban.svg" class="douban-icon"></a>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="form-group row" title="支持子目录链接，Web端打开分享点入目录，复制浏览器的URL即可；支持带提取码链接，查阅Wiki了解详情">
                    <label class="col-sm-2 col-form-label">分享链接</label>
                    <div class="col-sm-10">
                      <div class="input-group">
                        <input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)">
                        <div class="input-group-append" v-if="task.shareurl">
                          <button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="选择需转存的文件夹"><i class="bi bi-folder"></i></button>
                          <div class="input-group-text">
                            <a target="_blank" :href="task.shareurl"><i class="bi bi-link-45deg"></i></a>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="form-group row">
                    <label class="col-sm-2 col-form-label">保存路径</label>
                    <div class="col-sm-10">
                      <div class="input-group">
                        <input type="text" name="savepath[]" class="form-control" v-model="task.savepath" placeholder="必填" @focus="focusTaskname(task.__originalIndex !== undefined ? task.__originalIndex : index, task)" @input="onSavepathChange(task.__originalIndex !== undefined ? task.__originalIndex : index, task)">
                        <div class="input-group-append">
                          <button class="btn btn-outline-secondary" type="button" v-if="smart_param.savepath && smart_param.index == (task.__originalIndex !== undefined ? task.__originalIndex : index) && task.savepath != smart_param.origin_savepath" @click="task.savepath = smart_param.origin_savepath" title="恢复保存路径"><i class="bi bi-reply"></i></button>
                          <button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="选择保存到的文件夹"><i class="bi bi-folder"></i></button>
                          <button type="button" class="btn btn-outline-secondary" @click="resetFolderContent(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="重置文件夹：此操作将删除当前保存路径中的所有文件及相关转存记录，且不可恢复，请谨慎操作"><i class="bi bi-folder-x"></i></button>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="form-group row" :title="task.use_sequence_naming || task.use_episode_naming ? (task.use_sequence_naming ? '输入带{}占位符的重命名格式，如：剧名 - S01E{}、剧名.S03E{}等，{}将被替换为集序号（按文件名和修改日期智能排序），目录中的文件夹会被自动过滤' : '输入带[]占位符的重命名格式，如：剧名 - S01E[]、剧名.S03E[]等，[]将被替换为从文件名中提取的集编号，目录中的文件夹会被自动过滤') : '可用作筛选，只转存匹配到文件名的文件，留空则直接转存所有文件'">
                    <label class="col-sm-2 col-form-label">命名规则</label>
                    <div class="col-sm-10">
                      <div class="input-group">
                        <div class="input-group-prepend">
                          <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.previewRegex=true;fileSelect.sortBy='file_name_re';fileSelect.sortOrder='desc';showShareSelect(task.__originalIndex !== undefined ? task.__originalIndex : index)" :title="task.use_sequence_naming ? '预览顺序命名效果' : (task.use_episode_naming ? '预览剧集命名效果' : '预览正则命名效果')">
                            {{ task.use_sequence_naming ? '顺序命名' : (task.use_episode_naming ? '剧集命名' : '正则命名') }}
                          </button>
                        </div>
                        <input type="text" name="pattern[]" class="form-control" v-model="task.pattern" :placeholder="task.use_sequence_naming ? '输入带{}占位符的重命名格式，如：剧名 - S01E{}' : (task.use_episode_naming ? '输入带[]占位符的重命名格式，如：剧名 - S01E[]' : '匹配表达式')"  list="magicRegex" @input="detectNamingMode(task); onPatternChange(task.__originalIndex !== undefined ? task.__originalIndex : index, task)">
                        <input v-if="!task.use_sequence_naming && !task.use_episode_naming" type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
                        <div class="input-group-append" title="保存时只比较文件名的部分，01.mp4和01.mkv视同为同一文件，不重复转存">
                          <div class="input-group-text">
                            <input type="checkbox" v-model="task.ignore_extension">&nbsp;忽略后缀
                          </div>
                        </div>
                      </div>
                      <datalist id="magicRegex">
                        <option v-for="(value, key) in formData.magic_regex" :key="key" :value="`${key}`" v-html="`${value.pattern.replace('<', '<\u200B')} → ${value.replace}`"></option>
                      </datalist>
                    </div>
                  </div>
                  
                  <div class="form-group row" title="名称包含过滤词汇的项目不会被转存，多个词用逗号分隔，支持通过文件名和扩展名过滤文件和文件夹，支持使用保留词|过滤词的格式实现高级过滤，查阅Wiki了解详情">
                    <label class="col-sm-2 col-form-label">过滤规则</label>
                    <div class="col-sm-10">
                      <div class="input-group">
                        <input type="text" name="filterwords[]" class="form-control" v-model="task.filterwords" placeholder="可选，输入过滤词汇，用逗号分隔，如：纯享，txt，超前企划，名称包含过滤词汇的项目不会被转存，支持使用保留词|过滤词的格式实现高级过滤">
                      </div>
                    </div>
                  </div>
                  
                  <div class="form-group row" title="只转存比选中文件更新的文件，请在符合筛选条件的文件中进行选择，在更换分享链接时非常有用">
                    <label class="col-sm-2 col-form-label">起始文件</label>
                    <div class="col-sm-10">
                      <div class="input-group">
                        <input type="text" class="form-control" placeholder="可选，只转存比此文件更新的文件，请在符合筛选条件的文件中进行选择" name="startfid[]" v-model="task.startfid">
                        <div class="input-group-append" v-if="task.shareurl">
                          <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;showShareSelect(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="选择起始文件"><i class="bi bi-folder"></i></button>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="form-group row" title="匹配成功的文件夹的所有嵌套目录都会被更新，并且会应用与根目录相同的正则命名和过滤规则。注意：原理是逐级索引，深层嵌套目录的场景下效率非常低，慎用.*">
                    <label class="col-sm-2 col-form-label">更新目录</label>
                    <div class="col-sm-10">
                      <input type="text" name="update_subdir[]" class="form-control" v-model="task.update_subdir" placeholder="可选，输入需要更新的子目录的文件夹名称（或正则表达式），多个项目用竖线分隔，如：4K|1080P">
                    </div>
                  </div>
                  <div class="form-group row">
                    <label class="col-sm-2 col-form-label">截止日期</label>
                    <div class="col-sm-10">
                      <div class="input-group">
                        <input type="date" name="enddate[]" class="form-control date-input-no-icon" v-model="task.enddate" placeholder="可选" :ref="'enddate_' + index">
                        <div class="input-group-append">
                          <button type="button" class="btn btn-outline-secondary" @click="openDatePicker(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="选择截止日期">
                            <i class="bi bi-calendar3"></i>
                          </button>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="form-group row" title="只在勾选的星期时才运行，对周更的内容非常有用">
                    <label class="col-sm-2 col-form-label">执行周期</label>
                    <div class="col-sm-10 col-form-label">
                      <div class="form-check form-check-inline" title="也可用作任务总开关">
                        <input class="form-check-input" type="checkbox" :checked="task.runweek.length === 7" @change="toggleAllWeekdays(task)" :indeterminate.prop="task.runweek.length > 0 && task.runweek.length < 7" :disabled="(task.execution_mode || formData.execution_mode || 'manual') === 'auto'">
                        <label class="form-check-label" :class="{'text-muted': (task.execution_mode || formData.execution_mode || 'manual') === 'auto'}">全选</label>
                      </div>
                      <div class="form-check form-check-inline" v-for="(day, index) in weekdays" :key="index">
                        <input class="form-check-input" type="checkbox" v-model="task.runweek" :value="index+1" :disabled="(task.execution_mode || formData.execution_mode || 'manual') === 'auto'">
                        <label class="form-check-label" :class="{'text-muted': (task.execution_mode || formData.execution_mode || 'manual') === 'auto'}" v-html="day"></label>
                      </div>
                      <div class="form-check form-check-inline">
                        <input class="form-check-input" type="checkbox" :checked="(task.execution_mode || formData.execution_mode || 'manual') === 'auto'" @change="task.execution_mode = $event.target.checked ? 'auto' : 'manual'">
                        <label class="form-check-label">自动</label>
                      </div>
                    </div>
                  </div>
                  <div class="form-group row" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length" :title="getPluginConfigTitle(task)">
                    <label class="col-sm-2 col-form-label">插件配置</label>
                    <div class="col-sm-10">
                      <v-jsoneditor v-model="task.addition" :options="{mode:'tree'}" :plus="false" height="162px" style="margin-bottom: -8px;" :disabled="isPluginConfigDisabled(task)"></v-jsoneditor>
                    </div>
                  </div>
                </div>
              </template>
            </div>
            </template>
            <!-- 任务列表：海报视图 -->
            <div v-else class="tasklist-poster-mode">
              <!-- 复用管理视图的海报网格样式与自适应布局 -->
              <div class="discovery-grid">
                <div class="discovery-item"
                     v-for="(task, index) in sortedTasklist"
                     :key="'poster-'+(task.taskname || index)"
                     v-if="(taskDirSelected == '' || task.taskname == taskDirSelected) && task.taskname.includes(taskNameFilter) && tasklistFilterByType(task)">
                  <div class="discovery-poster" @mouseenter="handleManagementPosterHover($event, getCalendarTaskByName(task.taskname) || {})">
                    <img :src="getEpisodePosterUrl(getTasklistPosterLikeEpisode(task))"
                         :alt="task.taskname"
                         referrerpolicy="no-referrer"
                         crossorigin="anonymous"
                         decoding="async"
                         loading="lazy"
                         :fetchpriority="index < 50 ? 'high' : 'auto'"
                         @load="tasklistPosterLoaded[task.taskname] = true"
                         @error="handleImageError($event)">

                    <!-- 按钮行容器：自动补位布局 -->
                    <div class="discovery-actions-row" style="top: 8px;" v-if="tasklistPosterLoaded[task.taskname]">
                      <!-- 运行此任务按钮（正常状态） -->
                      <div v-if="!task.shareurl_ban" class="discovery-refresh-metadata tasklist-run-btn" @click.stop="runScriptNow(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="运行此任务">
                        <i class="bi bi-caret-right"></i>
                      </div>
                      <!-- 资源失效警告按钮（失效状态） -->
                      <div v-else class="discovery-warning-btn tasklist-warning-btn" :title="formatShareUrlBanMessage(task.shareurl_ban) || task.shareurl_ban">
                        <i class="bi bi-exclamation-circle"></i>
                      </div>
                      <div class="discovery-edit-metadata tasklist-delete-btn" @click.stop="removeTask(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="删除此任务">
                        <i class="bi bi-trash3"></i>
                      </div>
                      <div class="discovery-edit-metadata tasklist-edit-task-btn" @click.stop="openEditTaskModal(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="编辑此任务">
                        <i class="bi bi-pencil"></i>
                      </div>
                    </div>
                    <div class="discovery-actions-row" style="top: 36px;" v-if="tasklistPosterLoaded[task.taskname]">
                      <div class="discovery-refresh-metadata" v-if="getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).match_tmdb_id" @click="refreshSeasonMetadata(getCalendarTaskByName(task.taskname))" title="刷新元数据">
                        <i class="bi bi-arrow-clockwise"></i>
                      </div>
                      <div class="discovery-edit-metadata tasklist-edit-metadata-btn" @click.stop="openEditMetadataModal(getCalendarTaskByName(task.taskname) || { task_name: task.taskname })" title="编辑元数据">
                        <i class="bi bi-tag"></i>
                      </div>
                      <template v-for="key in formData.button_display_order">
                        <div v-if="key==='refresh_plex' && formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" class="discovery-edit-metadata tasklist-plex-btn" @click.stop="refreshPlexLibrary(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="刷新Plex媒体库">
                          <img src="./static/images/Plex.svg" class="plex-icon" style="filter: brightness(0) invert(1);">
                        </div>
                        <div v-else-if="key==='refresh_alist' && formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" class="discovery-edit-metadata tasklist-alist-btn" @click.stop="refreshAlistDirectory(task.__originalIndex !== undefined ? task.__originalIndex : index)" title="刷新AList目录">
                          <img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon" style="filter: brightness(0) invert(1);">
                        </div>
                      </template>
                    </div>

                    <!-- 转存进度徽标（复用评分样式） -->
                    <div class="discovery-rating"
                         v-if="tasklistPosterLoaded[task.taskname] && getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name && getCalendarTaskByName(task.taskname).season_counts"
                         :class="getProgressBadgeClass(getCalendarTaskByName(task.taskname))"
                         :title="'已转存/已播出：' + getTaskTransferredCount(getCalendarTaskByName(task.taskname)) + '/' + getTaskAiredCount(getCalendarTaskByName(task.taskname))">
                      {{ getTransferProgress(getCalendarTaskByName(task.taskname)) }}%
                    </div>
                    <!-- 左上角任务编号徽标：按需求移除 -->

                    <!-- 海报悬停信息 -->
                    <div class="discovery-poster-overlay" v-if="tasklistPosterLoaded[task.taskname]">
                      <!-- 任务编号 -->
                      <div class="info-line">#{{ String((task.__originalIndex !== undefined ? task.__originalIndex : index) + 1).padStart(2, '0') }}</div>
                      <!-- 匹配的剧名 -->
                      <div class="info-line">
                        <template v-if="getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name">
                          {{ getCalendarTaskByName(task.taskname).matched_show_name }}
                        </template>
                        <template v-else>
                          未匹配
                        </template>
                      </div>
                      <!-- 季名称 -->
                      <div class="info-line" v-if="getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).latest_season_name && getCalendarTaskByName(task.taskname).latest_season_name.trim() !== ''">
                        {{ getCalendarTaskByName(task.taskname).latest_season_name }}
                      </div>
                      <!-- 最近转存文件 -->
                      <div class="info-line" v-if="taskLatestFiles[task.taskname]">{{ taskLatestFiles[task.taskname] }}</div>
                      <!-- 最近更新日期 -->
                      <div class="info-line" v-if="taskLatestRecords[task.taskname]">{{ getTaskLatestRecordDisplay(task.taskname) }}</div>
                    </div>
                  </div>
                  <div class="discovery-info">
                    <div class="discovery-title" 
                         :title="getTasklistPosterTitle(task, index)" 
                         :style="{ cursor: getTaskTmdbId(task) ? 'pointer' : 'default' }"
                         @click="openTaskTmdbPage(task)">
                      {{ task.taskname }}
                      <span v-if="getTaskShowStatus(task.taskname)"> · {{ getTaskShowStatus(task.taskname) }}</span>
                      <span v-if="isTaskUpdatedToday(task.taskname) && shouldShowTodayIndicator()"
                            class="task-today-indicator"
                            :class="getTodayIndicatorClass()">
                        <i class="bi bi-stars"></i>
                      </span>
                    </div>
                    <div class="discovery-genre"
                         :title="(getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name && getCalendarTaskByName(task.taskname).season_counts)
                           ? ('已转存集数 / 已播出集数 / 节目总集数：' + getTaskTransferredCount(getCalendarTaskByName(task.taskname)) + ' / ' + getTaskAiredCount(getCalendarTaskByName(task.taskname)) + ' / ' + getTaskTotalCount(getCalendarTaskByName(task.taskname)))
                           : ''">
                      <template v-if="getCalendarTaskByName(task.taskname) && getCalendarTaskByName(task.taskname).matched_show_name && getCalendarTaskByName(task.taskname).season_counts">
                        <span v-html="`${getTaskTransferredCount(getCalendarTaskByName(task.taskname))} <span class='count-slash'>/</span> ${getTaskAiredCount(getCalendarTaskByName(task.taskname))} <span class='count-slash'>/</span> ${getTaskTotalCount(getCalendarTaskByName(task.taskname))}`"></span>
                      </template>
                      <template v-else>
                        暂无数据
                      </template>
                    </div>
                  </div>
                </div>
              </div>
            </div>

            <hr v-if="tasklist.viewMode === 'list' && formData.tasklist.length > 0" class="task-divider">
            <!-- 海报视图下隐藏底部添加任务按钮（仅列表视图显示） -->
            <div class="row" v-if="tasklist.viewMode === 'list'">
              <div class="col-sm-12 d-flex justify-content-end">
                <button type="button" class="btn btn-outline-primary" @click="addTask()" title="添加任务"><i class="bi bi-plus-lg"></i></button>
              </div>
            </div>
          </div>

          <div v-if="activeTab === 'history'">
            <div style="height: 20px;"></div>
            <div class="row" style="margin-bottom: 20px;">
              <div class="col-lg-6 col-md-6 mb-2 mb-md-0">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">名称筛选</span>
                  </div>
                  <input type="text" class="form-control" v-model="historyNameFilter" placeholder="任务或转存为名称关键词">
                  <div class="input-group-append">
                    <button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('historyNameFilter')"><i class="bi bi-x-lg"></i></button>
                  </div>
                </div>
              </div>
              <div class="col-lg-6 col-md-6">
                <div class="input-group">
                  <div class="input-group-prepend">
                    <span class="input-group-text">任务筛选</span>
                  </div>
                  <div class="position-relative" style="flex: 1;">
                    <select class="form-control task-filter-select" v-model="historyTaskSelected" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 24px !important;">
                      <option value="">全部任务</option>
                      <option v-for="task in historyTasks" :value="task" v-html="task"></option>
                    </select>
                    <!-- <i class="bi bi-chevron-down select-arrow" style="position: absolute; pointer-events: none; color: var(--dark-text-color);"></i> -->
                  </div>
                  <div class="input-group-append">
                    <button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearData('historyTaskSelected')"><i class="bi bi-x-lg"></i></button>
                  </div>
                </div>
              </div>
            </div>
            
            <div class="table-responsive">
              <table class="table table-hover selectable-records">
                <thead>
                  <tr>
                    <th style="width:10%" class="cursor-pointer" @click="sortHistory('transfer_time')">转存日期 <i v-if="historyParams.sortBy === 'transfer_time'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <th style="width:18%" class="cursor-pointer" @click="sortHistory('task_name')">任务名称 <i v-if="historyParams.sortBy === 'task_name'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <th style="width:25%" class="cursor-pointer" @click="sortHistory('original_name')">原文件 <i v-if="historyParams.sortBy === 'original_name'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <th style="width:25%" class="cursor-pointer" @click="sortHistory('renamed_to')">转存为 <i v-if="historyParams.sortBy === 'renamed_to'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <th style="width:7%; min-width: 60px;" class="cursor-pointer file-size-column" @click="sortHistory('file_size')">大小 <i v-if="historyParams.sortBy === 'file_size'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <th style="width:10%" class="cursor-pointer" @click="sortHistory('modify_date')">修改日期 <i v-if="historyParams.sortBy === 'modify_date'" :class="historyParams.order === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                  </tr>
                </thead>
                <tbody>
                  <tr v-if="history.hasLoaded && filteredHistoryRecords.length === 0">
                    <td colspan="6" class="text-center">暂无记录</td>
                  </tr>
                  <tr v-for="(record, index) in filteredHistoryRecords" :key="record.id" 
                      :class="{'selected-record': selectedRecords.includes(record.id)}" 
                      @click="selectRecord($event, record.id)">
                    <td>{{ record.transfer_time_readable }}</td>
                    <td class="position-relative">
                      <div v-if="!record._expandedFields || !record._expandedFields.includes('task_name')" 
                           class="text-truncate" 
                           :title="record.task_name" 
                           v-check-overflow="index + '|task_name'"
                           :class="{'task-name-hover': true}"
                           @click.stop="filterByTaskName(record.task_name, $event)">
                        {{ record.task_name }}
                      </div>
                      <div class="expand-button" v-if="isTextTruncated(record.task_name, index, 'task_name')" @click.stop="toggleExpand(index, 'task_name', $event)">
                        <i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
                      </div>
                      <div class="expanded-text task-name-hover" v-if="record._expandedFields && record._expandedFields.includes('task_name')" @click.stop="filterByTaskName(record.task_name, $event)">
                        {{ record.task_name }}
                      </div>
                    </td>
                    <td class="position-relative">
                      <div v-if="!record._expandedFields || !record._expandedFields.includes('original_name')" 
                           class="text-truncate" 
                           :title="record.original_name" 
                           v-check-overflow="index + '|original_name'">
                        {{ record.original_name }}
                      </div>
                      <div class="expand-button" v-if="isTextTruncated(record.original_name, index, 'original_name')" @click.stop="toggleExpand(index, 'original_name', $event)">
                        <i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
                      </div>
                      <div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('original_name')">
                        {{ record.original_name }}
                      </div>
                    </td>
                    <td class="position-relative">
                      <div v-if="!record._expandedFields || !record._expandedFields.includes('renamed_to')"
                           class="text-truncate"
                           :title="record.renamed_to"
                           v-check-overflow="index + '|renamed_to'">
                        {{ record.renamed_to }}
                        <span v-if="isRecordUpdatedToday(record) && shouldShowTodayIndicator()"
                              class="task-today-indicator"
                              :class="getTodayIndicatorClass()">
                          <i class="bi bi-stars"></i>
                        </span>
                      </div>
                      <div class="expand-button" v-if="isTextTruncated(record.renamed_to, index, 'renamed_to')" @click.stop="toggleExpand(index, 'renamed_to', $event)">
                        <i :class="record._expandedFields && record._expandedFields.length > 0 ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
                      </div>
                      <div class="expanded-text" v-if="record._expandedFields && record._expandedFields.includes('renamed_to')">
                        {{ record.renamed_to }}
                        <span v-if="isRecordUpdatedToday(record) && shouldShowTodayIndicator()"
                              class="task-today-indicator"
                              :class="getTodayIndicatorClass()">
                          <i class="bi bi-stars"></i>
                        </span>
                      </div>
                    </td>
                    <td class="file-size-cell">
                      <span class="file-size-value">{{ record.file_size_readable }}</span>
                      <span class="delete-record-btn" @click.stop="deleteRecord(record.id, record.original_name)" title="删除记录">
                        <i class="bi bi-trash3"></i>
                      </span>
                    </td>
                    <td>{{ record.modify_date_readable }}</td>
                  </tr>
                </tbody>
              </table>
            </div>
            
            <!-- 分页控制 -->
            <div class="pagination-container d-flex justify-content-between align-items-center mt-3">
              <div class="page-info text-secondary">
                显示 {{ history.pagination && history.pagination.total_records > 0 ? ((historyParams.page - 1) * historyParams.page_size + 1) + '-' + Math.min(historyParams.page * historyParams.page_size, history.pagination.total_records) : '0' }} 条，共 {{ history.pagination ? history.pagination.total_records : 0 }} 条记录{{ selectedRecords.length > 0 ? '，已选中 ' + selectedRecords.length + ' 条记录' : '' }}
              </div>
              <div class="pagination-controls d-flex align-items-center">
                <button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: historyParams.page <= 1 }" @click="changePage(historyParams.page - 1)" :disabled="historyParams.page <= 1">
                  <i class="bi bi-chevron-left"></i>
                </button>
                
                <!-- 分页按钮 -->
                <template v-if="totalPages > 0">
                  <!-- 第一页按钮 -->
                  <button type="button" class="btn btn-sm mx-1" 
                          :class="historyParams.page === 1 ? 'btn-primary' : 'btn-outline-secondary'" 
                          @click="changePage(1)">
                    1
                  </button>
                  
                  <!-- 省略号(前) -->
                  <span class="mx-1" v-if="getVisiblePageNumbers().length > 0 && getVisiblePageNumbers()[0] > 2">...</span>
                  
                  <!-- 中间页码 -->
                  <button v-for="page in getVisiblePageNumbers()" :key="page" type="button" 
                          class="btn btn-sm mx-1" 
                          :class="historyParams.page === page ? 'btn-primary' : 'btn-outline-secondary'" 
                          @click="changePage(page)">
                    {{ page }}
                  </button>
                  
                  <!-- 省略号(后) -->
                  <span class="mx-1" v-if="getVisiblePageNumbers().length > 0 && getVisiblePageNumbers()[getVisiblePageNumbers().length - 1] < totalPages - 1">...</span>
                  
                  <!-- 最后一页按钮 -->
                  <button v-if="totalPages > 1" type="button" class="btn btn-sm mx-1" 
                          :class="historyParams.page === totalPages ? 'btn-primary' : 'btn-outline-secondary'" 
                          @click="changePage(totalPages)">
                    {{ totalPages }}
                  </button>
                </template>
                
                <button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: historyParams.page >= totalPages }" @click="changePage(historyParams.page + 1)" :disabled="historyParams.page >= totalPages">
                  <i class="bi bi-chevron-right"></i>
                </button>
              </div>
              <div class="pagination-settings d-flex align-items-center">
                <span class="text-secondary mr-1">跳到第</span>
                <div class="input-group mx-1" style="width: 70px">
                  <input type="number" class="form-control form-control-sm" v-model.number="gotoPage" min="1" :max="totalPages">
                </div>
                <span class="text-secondary mr-2">页</span>
                <button type="button" class="btn btn-outline-secondary btn-sm mr-2" @click="changePage(gotoPage)">确定</button>
                <div class="dropdown">
                  <button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
                    {{ historyParams.page_size === 99999 ? '全部记录' : historyParams.page_size + ' 条/页' }}
                  </button>
                  <div class="dropdown-menu">
                    <a class="dropdown-item" href="#" v-for="size in [15, 30, 50, 100, 500, 1000, 'all']" :key="size" @click.prevent="changePageSizeTo(size)">{{ size === 'all' ? '全部记录' : size + ' 条/页' }}</a>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <div v-if="activeTab === 'filemanager'">
            <div style="height: 20px;"></div>
            <!-- 账号选择栏 -->
            <div class="row file-manager-account-selector">
              <div class="col-12">
                <!-- 桌面端账号选择 -->
                <div class="d-flex align-items-center file-manager-rule-bar">
                  <div class="input-group-prepend">
                    <span class="input-group-text">夸克账号</span>
                  </div>
                  <div class="position-relative" style="flex: 1;">
                    <select class="form-control task-filter-select file-manager-input file-manager-account-select" v-model="fileManager.selectedAccountIndex" @change="onAccountChange">
                      <option v-for="(account, index) in accountsDetail" :key="index" :value="index">{{ account.display_text }}</option>
                    </select>
                  </div>
                  <button type="button" class="btn btn-outline-plex batch-rename-btn" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" @click="refreshFileManagerPlexLibrary" title="刷新Plex媒体库"><img src="./static/images/Plex.svg" class="plex-icon"></button>
                  <button type="button" class="btn btn-outline-alist batch-rename-btn" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" @click="refreshFileManagerAlistDirectory" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
                  <button type="button" class="btn btn-outline-primary batch-rename-btn" @click="createNewFolder" title="新建文件夹">
                    <i class="bi bi-folder-plus"></i>
                  </button>
                  <button type="button" class="btn btn-outline-primary batch-rename-btn" @click="refreshCurrentFolderCache" title="刷新当前目录缓存">
                    <i class="bi bi-arrow-clockwise"></i>
                  </button>
                </div>
                <!-- 移动端账号选择 -->
                <div class="file-manager-rule-bar-responsive">
                  <div class="d-flex align-items-center">
                    <div class="input-group" style="flex: 1;">
                      <div class="input-group-prepend">
                        <span class="input-group-text">夸克账号</span>
                      </div>
                      <select class="form-control task-filter-select file-manager-account-select" v-model="fileManager.selectedAccountIndex" @change="onAccountChange">
                        <option v-for="(account, index) in accountsDetail" :key="index" :value="index">{{ account.display_text }}</option>
                      </select>
                    </div>
                    <button type="button" class="btn btn-outline-plex batch-rename-btn" v-if="formData.plugins && formData.plugins.plex && formData.plugins.plex.url && formData.plugins.plex.token && formData.plugins.plex.quark_root_path && formData.button_display.refresh_plex !== 'disabled'" @click="refreshFileManagerPlexLibrary" title="刷新Plex媒体库"><img src="./static/images/Plex.svg" class="plex-icon"></button>
                    <button type="button" class="btn btn-outline-alist batch-rename-btn" v-if="formData.plugins && formData.plugins.alist && formData.plugins.alist.url && formData.plugins.alist.token && formData.plugins.alist.storage_id && formData.button_display.refresh_alist !== 'disabled'" @click="refreshFileManagerAlistDirectory" title="刷新AList目录"><img src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg" class="alist-icon"></button>
                    <button type="button" class="btn btn-outline-primary batch-rename-btn" @click="createNewFolder" title="新建文件夹">
                      <i class="bi bi-folder-plus"></i>
                    </button>
                    <button type="button" class="btn btn-outline-primary batch-rename-btn" @click="refreshCurrentFolderCache" title="刷新当前目录缓存">
                      <i class="bi bi-arrow-clockwise"></i>
                    </button>
                  </div>
                </div>
              </div>
            </div>
            <!-- 重命名规则设置栏 -->
            <div class="row mb-3">
              <div class="col-12">
                <!-- 桌面端结构（原有） -->
                <div class="d-flex align-items-center file-manager-rule-bar">
                  <div class="input-group-prepend">
                    <button class="btn btn-outline-secondary" type="button" @click="previewAndRename"
                      :title="fileManager.use_sequence_naming ? '预览顺序命名效果' : (fileManager.use_episode_naming ? '预览剧集命名效果' : '预览正则命名效果')">
                      {{ fileManager.use_sequence_naming ? '顺序命名' : (fileManager.use_episode_naming ? '剧集命名' : '正则命名') }}
                    </button>
                  </div>
                  <input type="text" class="form-control file-manager-input" v-model="fileManager.pattern"
                    :placeholder="fileManager.use_sequence_naming ? '输入带{}占位符的重命名格式，如：剧名 - S01E{}' : (fileManager.use_episode_naming ? '输入带[]占位符的重命名格式，如：剧名 - S01E[]' : '匹配表达式')"
                    @input="detectFileManagerNamingMode"
                    :title="fileManager.use_sequence_naming ? '输入带{}占位符的重命名格式，如：剧名 - S01E{}、剧名.S03E{}等，{}将被替换为集序号（按文件名和修改日期智能排序）' : (fileManager.use_episode_naming ? '输入带[]占位符的重命名格式，如：剧名 - S01E[]、剧名.S03E[]等，[]将被替换为从文件名中提取的集编号' : '只重命名匹配到文件名的文件，留空不会进行重命名')">
                  <input v-if="!fileManager.use_sequence_naming && !fileManager.use_episode_naming" type="text" class="form-control file-manager-input" v-model="fileManager.replace" placeholder="替换表达式" title="替换表达式">
                  <input type="text" class="form-control file-manager-input" v-model="fileManager.filterwords" placeholder="可选，输入过滤词汇，用逗号分隔，如：纯享，txt，超前企划" title="名称包含过滤词汇的项目不会被重命名，多个词用逗号分隔，支持通过文件名和扩展名过滤文件和文件夹，支持使用保留词|过滤词的格式实现高级过滤，查阅Wiki了解详情">
                  <div class="input-group-append">
                    <div class="input-group-text" title="勾选后，重命名和过滤规则也将应用于文件夹">
                      <input type="checkbox" v-model="fileManager.include_folders">&nbsp;含文件夹
                    </div>
                  </div>
                  <button type="button" class="btn btn-outline-primary batch-rename-btn" @click="previewAndRename" title="预览并执行重命名">
                    <i class="bi bi-pencil"></i>
                  </button>
                </div>
                <!-- 移动端结构（还原为input-group风格，两行，样式与桌面端一致） -->
                <div class="file-manager-rule-bar-responsive">
                  <!-- 第一行：命名按钮+表达式+替换表达式 -->
                  <div class="input-group mb-2">
                    <div class="input-group-prepend">
                      <button class="btn btn-outline-secondary" type="button" @click="previewAndRename"
                        :title="fileManager.use_sequence_naming ? '预览顺序命名效果' : (fileManager.use_episode_naming ? '预览剧集命名效果' : '预览正则命名效果')">
                        {{ fileManager.use_sequence_naming ? '顺序命名' : (fileManager.use_episode_naming ? '剧集命名' : '正则命名') }}
                      </button>
                    </div>
                    <input type="text" class="form-control" v-model="fileManager.pattern"
                      :placeholder="fileManager.use_sequence_naming ? '输入带{}占位符的重命名格式，如：剧名 - S01E{}' : (fileManager.use_episode_naming ? '输入带[]占位符的重命名格式，如：剧名 - S01E[]' : '匹配表达式')"
                      @input="detectFileManagerNamingMode"
                      :title="fileManager.use_sequence_naming ? '输入带{}占位符的重命名格式，如：剧名 - S01E{}、剧名.S03E{}等，{}将被替换为集序号（按文件名和修改日期智能排序）' : (fileManager.use_episode_naming ? '输入带[]占位符的重命名格式，如：剧名 - S01E[]、剧名.S03E[]等，[]将被替换为从文件名中提取的集编号' : '只重命名匹配到文件名的文件，留空不会进行重命名')">
                    <input v-if="!fileManager.use_sequence_naming && !fileManager.use_episode_naming" type="text" class="form-control" v-model="fileManager.replace" placeholder="替换表达式" title="替换表达式">
                  </div>
                  <!-- 第二行：过滤规则+含文件夹+按钮，flex包裹，含文件夹有圆角，按钮有8px间距和圆角 -->
                  <div class="input-group d-flex align-items-center">
                    <div class="input-group-prepend">
                      <span class="input-group-text">过滤规则</span>
                    </div>
                    <input type="text" class="form-control" v-model="fileManager.filterwords" placeholder="可选，输入过滤词汇，用逗号分隔，如：纯享，txt，超前企划" title="名称包含过滤词汇的项目不会被重命名，多个词用逗号分隔，支持通过文件名和扩展名过滤文件和文件夹，支持使用保留词|过滤词的格式实现高级过滤，查阅Wiki了解详情">
                    <span class="input-group-text file-folder-rounded">
                      <input type="checkbox" v-model="fileManager.include_folders">&nbsp;含文件夹
                    </span>
                    <button type="button" class="btn btn-outline-primary batch-rename-btn" @click="previewAndRename" title="预览并执行重命名">
                      <i class="bi bi-pencil"></i>
                    </button>
                  </div>
                </div>
              </div>
            </div>

            <!-- 面包屑导航 -->
            <nav aria-label="breadcrumb" class="file-manager-breadcrumb">
              <ol class="breadcrumb">
                <li class="breadcrumb-item cursor-pointer" @click="navigateToFolder('root')">全部文件</li>
                <li v-for="(item, index) in fileManager.paths" :key="index" class="breadcrumb-item">
                  <a v-if="index != fileManager.paths.length - 1" href="#" @click.prevent="navigateToFolder(item.fid, item.name)">{{ item.name }}</a>
                  <span v-else class="text-muted">{{ item.name }}</span>
                </li>
              </ol>
            </nav>

            <!-- 文件表格 -->
            <div class="table-responsive">
              <table class="table table-hover selectable-files">
                <thead>
                  <tr>
                    <th style="width:80%" class="cursor-pointer" @click="sortFiles('file_name')">文件名 <i v-if="fileManager.sortBy === 'file_name'" :class="fileManager.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <th style="width:10%; min-width: 80px;" class="cursor-pointer" @click="sortFiles('file_size')">大小 <i v-if="fileManager.sortBy === 'file_size'" :class="fileManager.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <th style="width:10%" class="cursor-pointer" @click="sortFiles('updated_at')">修改日期 <i v-if="fileManager.sortBy === 'updated_at'" :class="fileManager.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                  </tr>
                </thead>
                <tbody>
                  <tr v-if="fileManager.hasLoaded && fileManager.fileList.length === 0">
                    <td colspan="3" class="text-center">当前文件夹为空</td>
                  </tr>
                  <template v-else>
                    <tr v-for="(file, index) in fileManager.fileList" :key="file.fid"
                        @click="handleFileRowClick(file, $event)"
                        :class="{'selected-file': fileManager.selectedFiles.includes(file.fid)}"
                        class="cursor-pointer">
                      <td class="position-relative">
                        <!-- 编辑状态 -->
                        <div v-if="file._editing" class="d-flex align-items-center" @click.stop>
                          <i class="bi mr-2" :class="getFileIconClass(file.file_name, file.dir)"></i>
                          <input
                            type="text"
                            class="form-control form-control-sm rename-input"
                            v-model="file._editingName"
                            @keyup.enter="saveRenameFile(file)"
                            @keyup.escape="cancelRenameFile(file)"
                            @blur="saveRenameFile(file)"
                            @click.stop
                            :ref="'renameInput_' + file.fid">
                        </div>
                        <!-- 正常显示状态 -->
                        <div v-else-if="!file._expanded" class="d-flex align-items-center">
                          <i class="bi mr-2" :class="getFileIconClass(file.file_name, file.dir)"></i>
                          <div class="text-truncate"
                              :title="file.file_name"
                              v-check-file-overflow="index + '|file_name'">
                            {{ file.file_name }}
                            <span v-if="isFileUpdatedToday(file) && shouldShowTodayIndicator()"
                                  class="task-today-indicator"
                                  :class="getTodayIndicatorClass()">
                              <i class="bi bi-stars"></i>
                            </span>
                          </div>
                        </div>
                        <div v-else style="white-space: normal; word-break: break-word; padding-right: 25px;">
                          <i class="bi mr-2" :class="getFileIconClass(file.file_name, file.dir)"></i>{{ file.file_name }}
                          <span v-if="isFileUpdatedToday(file) && shouldShowTodayIndicator()"
                                class="task-today-indicator"
                                :class="getTodayIndicatorClass()">
                            <i class="bi bi-stars"></i>
                          </span>
                        </div>
                        <div class="expand-button" v-if="!file._editing && isFilenameTruncated(file.file_name, index, 'file_name')" @click.stop="toggleExpandFilename(file)">
                          <i :class="file._expanded ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
                        </div>
                      </td>
                      <td class="file-size-cell">
                        <span class="file-size-value">{{ file.dir ? (file.include_items + ' 项') : formatFileSize(file.size || file.file_size) }}</span>
                        <span class="rename-record-btn" @click.stop="startRenameFile(file)" title="重命名文件">
                          <i class="bi bi-pencil"></i>
                        </span>
                        <span class="move-record-btn" @click.stop="startMoveFile(file)" title="移动文件">
                          <i class="bi bi-arrow-up-right-circle"></i>
                        </span>
                        <span class="delete-record-btn" @click.stop="deleteSelectedFilesForManager(file.fid, file.file_name, file.dir)" title="删除文件">
                          <i class="bi bi-trash3"></i>
                        </span>
                      </td>
                      <td>{{ formatDate(file.updated_at) }}</td>
                    </tr>
                  </template>
                </tbody>
              </table>
            </div>
            
            <!-- 分页区域 -->
            <div class="pagination-container d-flex justify-content-between align-items-center mt-3">
              <div class="page-info text-secondary">
                显示 {{ fileManager.total > 0 ? ((fileManager.currentPage - 1) * fileManager.pageSize + 1) + '-' + Math.min(fileManager.currentPage * fileManager.pageSize, fileManager.total) : '0-0' }} 项，共 {{ fileManager.total }} 个项目{{ fileManager.selectedFiles.length > 0 ? '，已选中 ' + fileManager.selectedFiles.length + ' 项' : '' }}
              </div>
              <div class="pagination-controls d-flex align-items-center">
                <button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: fileManager.currentPage <= 1 }" @click="changeFolderPage(fileManager.currentPage - 1)" :disabled="fileManager.currentPage <= 1">
                  <i class="bi bi-chevron-left"></i>
                </button>
                
                <template v-if="fileManager.totalPages > 0">
                  <button type="button" class="btn btn-sm mx-1" 
                          :class="fileManager.currentPage === 1 ? 'btn-primary' : 'btn-outline-secondary'" 
                          @click="changeFolderPage(1)">1</button>
                  
                  <span class="mx-1" v-if="getVisibleFolderPageNumbers().length > 0 && getVisibleFolderPageNumbers()[0] > 2">...</span>
                  
                  <button v-for="page in getVisibleFolderPageNumbers()" :key="page" type="button" 
                          class="btn btn-sm mx-1" 
                          :class="fileManager.currentPage === page ? 'btn-primary' : 'btn-outline-secondary'" 
                          @click="changeFolderPage(page)">
                    {{ page }}
                  </button>
                  
                  <span class="mx-1" v-if="getVisibleFolderPageNumbers().length > 0 && getVisibleFolderPageNumbers()[getVisibleFolderPageNumbers().length - 1] < fileManager.totalPages - 1">...</span>
                  
                  <button v-if="fileManager.totalPages > 1" type="button" class="btn btn-sm mx-1" 
                          :class="fileManager.currentPage === fileManager.totalPages ? 'btn-primary' : 'btn-outline-secondary'" 
                          @click="changeFolderPage(fileManager.totalPages)">
                    {{ fileManager.totalPages }}
                  </button>
                </template>
                
                <button type="button" class="btn btn-outline-secondary btn-sm mx-1" :class="{ disabled: fileManager.currentPage >= fileManager.totalPages }" @click="changeFolderPage(fileManager.currentPage + 1)" :disabled="fileManager.currentPage >= fileManager.totalPages">
                  <i class="bi bi-chevron-right"></i>
                </button>
              </div>
              <div class="pagination-settings d-flex align-items-center">
                <span class="text-secondary mr-1">跳到第</span>
                <div class="input-group mx-1" style="width: 70px">
                  <input type="number" class="form-control form-control-sm" v-model.number="fileManager.gotoPage" min="1" :max="fileManager.totalPages">
                </div>
                <span class="text-secondary mr-2">页</span>
                <button type="button" class="btn btn-outline-secondary btn-sm mr-2" @click="changeFolderPage(fileManager.gotoPage)">确定</button>
                <div class="dropdown">
                  <button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
                    {{ fileManager.pageSize === 99999 ? '全部项目' : fileManager.pageSize + ' 项/页' }}
                  </button>
                  <div class="dropdown-menu">
                    <a class="dropdown-item" href="#" v-for="size in [15, 30, 50, 100, 500, 1000, 'all']" :key="size" @click.prevent="changeFileManagerPageSize(size)">{{ size === 'all' ? '全部项目' : size + ' 项/页' }}</a>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- 影视发现页面 -->
          <div v-if="activeTab === 'discovery'">
            <div style="height: 20px;"></div>
            <!-- 榜单切换按钮区域 -->
            <div class="discovery-controls">
              <!-- 第一排：主榜单按钮 -->
              <div class="discovery-main-buttons">
                <button type="button"
                        class="btn btn-outline-secondary discovery-btn"
                        v-for="category in discovery.mainCategories"
                        :key="category.key"
                        :class="{ active: discovery.selectedMainCategory === category.key }"
                        @click="selectMainCategory(category.key)">
                  {{ category.name }}
                </button>
              </div>

              <!-- 第二排：子榜单按钮 -->
              <div class="discovery-sub-buttons">
                <!-- 调试信息 -->
                <div v-if="currentSubCategories.length === 0" style="color: red; font-size: 12px;">
                  调试：当前主分类 {{ discovery.selectedMainCategory }}，子分类数量：{{ currentSubCategories.length }}
                </div>
                <button type="button"
                        class="btn btn-outline-secondary discovery-btn"
                        v-for="subCategory in currentSubCategories"
                        :key="subCategory.key"
                        :class="{ active: discovery.selectedSubCategory === subCategory.key }"
                        @click="selectSubCategory(subCategory.key)">
                  {{ subCategory.name }}
                </button>
              </div>
            </div>

            <!-- 只在已初始化时显示内容，避免页面刷新时的闪烁 -->
            <div v-if="discovery.isInitialized || discovery.hasLoaded">
              <!-- 错误提示 -->
              <div v-if="discovery.error" class="alert alert-warning" role="alert">
                <i class="bi bi-exclamation-triangle"></i>
                {{ discovery.error }}
              </div>

              <!-- 影片海报网格 -->
              <div v-else-if="discovery.items.length > 0" class="discovery-grid">
                <div class="discovery-item"
                     v-for="item in discovery.items"
                     :key="item.id">
                  <div class="discovery-poster" @mouseenter="handlePosterHover($event, item)">
                    <img :src="(item.pic && item.pic.normal) ? getProxiedImageUrl(item.pic.normal) : '/static/images/no-poster.svg'"
                         :alt="item.title"
                         referrerpolicy="no-referrer"
                         crossorigin="anonymous"
                         @error="handleImageError($event)">
                    <div class="discovery-rating" v-if="item.rating && item.rating.value">
                      {{ parseFloat(item.rating.value).toFixed(1) }}
                    </div>
                    <div class="discovery-create-task" @click="createTaskFromDiscovery(item)" title="创建任务">
                      <i class="bi bi-plus-lg"></i>
                    </div>
                    <div class="discovery-poster-overlay">
                      <div class="info-line" v-for="(detail, index) in getMovieDetails(item)" :key="index">
                        {{ detail }}
                      </div>
                    </div>
                  </div>
                  <div class="discovery-info">
                    <div class="discovery-title" :title="item.title" @click="openDoubanPage(item)">{{ item.title }}</div>
                    <div class="discovery-genre" :title="getMovieGenreText(item)" v-html="getMovieGenre(item)"></div>
                    <div class="discovery-year" v-if="item.year">{{ item.year }}</div>
                  </div>
                </div>
              </div>

              <!-- 空状态 - 只有在加载完成且无数据且无错误时才显示 -->
              <div v-else-if="discovery.hasLoaded" class="text-center py-5 text-muted">
                <i class="bi bi-film" style="font-size: 3rem;"></i>
                <div class="mt-2">暂无数据</div>
              </div>
            </div>
          </div>

          <!-- 追剧日历页面 -->
          <div v-if="activeTab === 'calendar'" ref="calendarRoot">
            <div style="height: 20px;"></div>
            
            <!-- TMDB API未配置提示 -->
            <div v-if="configHasLoaded && !formData.tmdb_api_key" class="alert alert-warning" role="alert">
              追剧日历需要配置 TMDB API 密钥后才能使用，请在<a href="#" class="link-focus" @click.prevent="changeTab('config')">系统配置</a>页面的 TMDB API 部分进行配置
            </div>
            
            <!-- 追剧日历内容 -->
            <div v-else>
              <!-- 顶部筛选和导航 -->
              <div class="row calendar-filter-row" style="margin-bottom: 20px;">
                <div class="col-lg-6 col-md-6 mb-2 mb-md-0">
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text">名称筛选</span>
                    </div>
                    <input type="text" class="form-control" v-model="calendar.nameFilter" placeholder="任务或节目名称关键词">
                    <div class="input-group-append">
                      <button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearCalendarFilter('nameFilter')"><i class="bi bi-x-lg"></i></button>
                    </div>
                  </div>
                </div>
                <div class="col-lg-6 col-md-6">
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text">任务筛选</span>
                    </div>
                    <div class="position-relative" style="flex: 1;">
                      <select class="form-control task-filter-select" v-model="calendar.taskFilter" style="padding-left: 8px !important; text-indent: 0 !important; display: flex !important; align-items: center !important; line-height: 1.5 !important; padding-right: 24px !important;">
                        <option value="">全部任务</option>
                        <option v-for="task in calendar.taskNames" :value="task" v-html="task"></option>
                      </select>
                    </div>
                    <div class="input-group-append">
                      <button type="button" class="btn btn-outline-secondary filter-btn-square" @click="clearCalendarFilter('taskFilter')"><i class="bi bi-x-lg"></i></button>
                    </div>
                  </div>
                </div>
              </div>
              
              <!-- 分类按钮和视图切换 -->
              <div class="row mb-3 calendar-header-row">
                <div class="col-lg-8 col-md-6">
                  <!-- 分类按钮 -->
                  <div class="calendar-category-buttons">
                    <button type="button" 
                            class="btn btn-outline-secondary calendar-category-btn"
                            :class="{ active: calendar.selectedType === 'all' }"
                            @click="selectCalendarType('all')">
                      全部
                    </button>
                    <button type="button" 
                            class="btn btn-outline-secondary calendar-category-btn"
                            v-for="type in filteredContentTypes"
                            :key="type"
                            :class="{ active: calendar.selectedType === type }"
                            @click="selectCalendarType(type)">
                      {{ getContentTypeDisplayName(type) }}
                    </button>
                  </div>
                </div>
                <div class="col-lg-4 col-md-6">
                  <!-- 日期切换和视图切换 -->
                  <div class="calendar-controls d-flex justify-content-end">
                    <!-- 日期切换按钮 -->
                    <div class="btn-group" v-if="calendar.viewMode === 'poster'">
                      <button type="button" class="btn btn-outline-secondary btn-sm" @click="changeCalendarDate('prevWeek')" title="前一周">
                        <i class="bi bi-chevron-double-left"></i>
                      </button>
                      <button type="button" class="btn btn-outline-secondary btn-sm" @click="changeCalendarDate('prevDay')" title="前一天">
                        <i class="bi bi-chevron-left"></i>
                      </button>
                      <button type="button" class="btn btn-primary btn-sm" @click="goToToday">今天</button>
                      <button type="button" class="btn btn-outline-secondary btn-sm" @click="changeCalendarDate('nextDay')" title="后一天">
                        <i class="bi bi-chevron-right"></i>
                      </button>
                      <button type="button" class="btn btn-outline-secondary btn-sm" @click="changeCalendarDate('nextWeek')" title="后一周">
                        <i class="bi bi-chevron-double-right"></i>
                      </button>
                    </div>
                    <div class="btn-group" v-else>
                      <button type="button" class="btn btn-outline-secondary btn-sm" @click="changeCalendarDate('prevMonth')" title="前一月">
                        <i class="bi bi-chevron-double-left"></i>
                      </button>
                      <button type="button" class="btn btn-primary btn-sm" @click="goToToday">今天</button>
                      <button type="button" class="btn btn-outline-secondary btn-sm" @click="changeCalendarDate('nextMonth')" title="后一月">
                        <i class="bi bi-chevron-double-right"></i>
                      </button>
                    </div>
                    
                    <!-- 视图切换按钮 -->
                    <button type="button" 
                            class="btn btn-outline-secondary btn-sm"
                            @click="toggleCalendarViewMode"
                            :title="calendar.viewMode === 'poster' ? '当前：海报视图，点击切换到日历视图' : '当前：日历视图，点击切换到海报视图'">
                      <i :class="calendar.viewMode === 'poster' ? 'bi bi-calendar3' : 'bi bi-grid-3x3-gap'"></i>
                    </button>
                    
                    <!-- 合并集切换按钮 -->
                    <button type="button" 
                            class="btn btn-outline-secondary btn-sm"
                            @click="toggleCalendarMergeEpisodes"
                            :title="calendar.mergeEpisodes ? '当前：合并集模式，点击切换到拆分集模式' : '当前：拆分集模式，点击切换到合并集模式'">
                      <i :class="calendar.mergeEpisodes ? 'bi bi-file-break' : 'bi bi-file'"></i>
                    </button>
                    
                    <!-- 刷新按钮 -->
                    <button type="button" 
                            class="btn btn-outline-primary batch-rename-btn"
                            @click="refreshCalendarData"
                            title="刷新追剧日历数据">
                      <i class="bi bi-arrow-clockwise"></i>
                    </button>

                    <!-- 内容管理按钮 -->
                    <button type="button"
                            class="btn btn-outline-secondary btn-sm"
                            @click="toggleCalendarManageMode"
                            :title="calendar.manageMode ? '返回追剧日历' : '内容管理'">
                      <i :class="calendar.manageMode ? 'bi bi-reply' : 'bi bi-tags'"></i>
                    </button>
                  </div>
                </div>
              </div>
              
              <!-- 内容管理模式 -->
              <div v-if="calendar.manageMode" style="margin-top: 20px;">
                <!-- 完全复用影视发现页面的海报网格样式与自适应布局 -->
                <div class="discovery-grid">
                  <div class="discovery-item"
                       v-for="task in managementTasksFiltered"
                       :key="task.task_name">
                    <div class="discovery-poster" @mouseenter="handleManagementPosterHover($event, task)">
                      <img :src="getEpisodePosterUrl(getTaskPosterLikeEpisode(task))"
                           :alt="task.show_name || task.task_name"
                           referrerpolicy="no-referrer"
                           crossorigin="anonymous"
                           @error="handleImageError($event)">
                      
                      <!-- 刷新元数据按钮（左上角，悬停显示） -->
                      <div class="discovery-refresh-metadata" 
                           v-if="task.match_tmdb_id"
                           @click="refreshSeasonMetadata(task)" 
                           title="刷新元数据">
                        <i class="bi bi-arrow-clockwise"></i>
                      </div>
                      <!-- 编辑元数据按钮（已匹配：靠右；未匹配：靠左） -->
                      <div class="discovery-edit-metadata tasklist-edit-metadata-btn"
                           :style="{ left: task.match_tmdb_id ? '36px' : '8px' }"
                           @click.stop="openEditMetadataModal(task)"
                           title="编辑元数据">
                        <i class="bi bi-tag"></i>
                      </div>
                      
                      <!-- 转存进度徽标（复用评分样式） -->
                      <div class="discovery-rating"
                           v-if="task.matched_show_name && task.season_counts"
                           :class="getProgressBadgeClass(task)"
                           :title="'已转存/已播出：' + getTaskTransferredCount(task) + '/' + getTaskAiredCount(task)">
                        {{ getTransferProgress(task) }}%
                      </div>
                      <div class="discovery-poster-overlay">
                        <div class="info-line">
                          <template v-if="task.matched_show_name">
                            {{ task.matched_show_name }}
                          </template>
                          <template v-else>
                            未匹配
                          </template>
                        </div>
                        <div class="info-line" v-if="task.matched_year && String(task.matched_year).trim() !== ''">{{ task.matched_year }}</div>
                        <div class="info-line" v-if="task.latest_season_name && task.latest_season_name.trim() !== ''">{{ task.latest_season_name }}</div>
                        <div class="info-line" v-if="task.matched_show_name && task.season_counts"
                             v-html="`${getTaskTransferredCount(task)} <span class='count-slash'>/</span> ${getTaskAiredCount(task)} <span class='count-slash'>/</span> ${getTaskTotalCount(task)}`">
                        </div>
                        <div class="info-line" v-if="task.matched_status && task.matched_status.trim() !== ''">{{ task.matched_status }}</div>
                      </div>
                    </div>
                    <div class="discovery-info">
                      <div class="discovery-title" :title="getTaskShowStatusTooltip(task)" @click.stop="openTaskMatchedTmdbPage(task)" style="cursor: pointer;">
                        {{ task.task_name }}
                        <span v-if="getTaskShowStatus(task)"> · {{ getTaskShowStatus(task) }}</span>
                        <span v-if="isCalendarTaskUpdatedToday(task) && shouldShowTodayIndicator()"
                              class="task-today-indicator"
                              :class="getTodayIndicatorClass()">
                          <i class="bi bi-stars"></i>
                        </span>
                      </div>
                      <div class="discovery-genre" :title="getContentTypeCN(task.content_type)">{{ getContentTypeCN(task.content_type) }}</div>
                    </div>
                  </div>
                </div>
              </div>

              <!-- 海报模式 -->
              <div v-else-if="calendar.viewMode === 'poster'" class="calendar-poster-mode">
                <!-- 日期导航 -->
                <div class="calendar-date-navigation">
                  <div class="calendar-date-item" 
                       v-for="(date, index) in calendar.weekDates" 
                       :key="index"
                       :class="{ 'today': date.isToday }">
                    <div class="calendar-date-weekday">{{ date.weekday }}</div>
                    <div class="calendar-date-day">{{ date.day }}</div>
                  </div>
                </div>
                
                <!-- 海报网格 -->
                <div class="calendar-poster-grid">
                  <div class="calendar-poster-column" 
                       v-for="(date, dateIndex) in calendar.weekDates" 
                       :key="dateIndex">
                    <div class="calendar-poster-item" 
                         v-for="episode in getEpisodesByDate(date.date)"
                         :key="episode.id">
                      <div class="calendar-poster" 
                           @mouseenter="handleCalendarPosterHover($event, episode)"
                           @mouseleave="hideCalendarPosterHover">
                        <img :src="getEpisodePosterUrl(episode)" 
                             :alt="episode.show_name"
                             class="calendar-poster-image"
                             crossorigin="anonymous"
                             referrerpolicy="no-referrer"
                             @error="handleCalendarImageError">
                        
                        <!-- 刷新元数据按钮（左上角，悬停显示） -->
                        <div class="calendar-refresh-metadata" 
                             @click="refreshEpisodeMetadata(episode)" 
                             title="刷新元数据">
                          <i class="bi bi-arrow-clockwise"></i>
                        </div>
                        
                        <!-- 已转存标识（与影视发现评分标识风格一致） -->
                        <div class="discovery-rating calendar-transferred-badge" v-if="isEpisodeReachedProgress(episode)" title="已转存">
                          <i class="bi bi-check2"></i>
                        </div>
                        
                        <!-- 海报悬停效果 -->
                        <div class="calendar-poster-overlay">
                          <div class="info-line">{{ episode.name || episode.show_name }}</div>
                          <div class="info-line overview" v-if="episode.overview" :title="episode.overview" :data-fulltext="episode.overview">{{ episode.overview }}</div>
                        </div>
                        
                        
                      </div>
                      
                      <div class="calendar-episode-info">
                        <div class="calendar-show-name" :title="getEpisodeShowTitleWithStatus(episode)" @click="openShowTmdbPage(episode)">
                          {{ episode.show_name }}
                          <span v-if="getEpisodeFinaleStatus(episode)"> · {{ getEpisodeFinaleStatus(episode) }}</span>
                          <span v-if="isEpisodeUpdatedToday(episode) && shouldShowTodayIndicator()"
                                class="task-today-indicator"
                                :class="getTodayIndicatorClass()">
                            <i class="bi bi-stars"></i>
                          </span>
                        </div>
                        <div class="calendar-episode-number"
                             :class="{ 'episode-number-reached': isEpisodeReachedProgress(episode) }"
                             :title="getEpisodeTooltip(episode)"
                             @click="openEpisodeTmdbPage(episode)">
                          {{ getEpisodeDisplayNumber(episode) }}
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
              
              <!-- 日历模式 -->
              <div v-else class="calendar-month-mode">
                <div class="calendar-month-table">
                  <div class="calendar-month-header">
                    <div class="calendar-month-header-cell" v-for="weekday in ['周一', '周二', '周三', '周四', '周五', '周六', '周日']">
                      {{ weekday }}
                    </div>
                  </div>
                  <div class="calendar-month-body">
                    <div class="calendar-month-row" 
                         v-for="week in calendar.monthWeeks" 
                         :key="week.weekIndex">
                      <div class="calendar-month-cell" 
                           v-for="day in week.days" 
                           :key="day.date"
                           :class="{ 
                             'other-month': !day.isCurrentMonth,
                             'today': day.isToday,
                             'has-episodes': day.episodes && day.episodes.length > 0,
                             'selected': day.date === calendar.selectedDate
                           }"
                           @click="selectCalendarDate(day)">
                        <div class="calendar-month-date">
                          <span v-if="day.dayNumber === 1">
                            {{ day.isCurrentMonth ? (calendar.currentDate.getMonth() + 1) : (calendar.currentDate.getMonth() + 2) }}/1
                          </span>
                          <span v-else>
                            {{ day.dayNumber }}
                          </span>
                        </div>
                        <div class="calendar-month-episodes" v-if="day.episodes && day.episodes.length > 0">
                          <div class="calendar-month-episode" 
                               v-for="episode in day.episodes" 
                               :key="episode.id">
                            <span class="episode-title" :title="getEpisodeShowTitleWithStatus(episode)" @click.stop="openShowTmdbPage(episode)">
                              {{ episode.show_name }}
                              <span v-if="getEpisodeFinaleStatus(episode)"> · {{ getEpisodeFinaleStatus(episode) }}</span>
                              <span v-if="isEpisodeUpdatedToday(episode) && shouldShowTodayIndicator()"
                                    class="task-today-indicator"
                                    :class="getTodayIndicatorClass()">
                                <i class="bi bi-stars"></i>
                              </span>
                            </span>
                            <span :class="['episode-number', { 'episode-number-reached': isEpisodeReachedProgress(episode) }]"
                                  :title="getEpisodeTooltip(episode)"
                                  :data-full="getEpisodeDisplayNumber(episode)"
                                  :data-episode-only="getEpisodeOnlyNumber(episode)"
                                  :data-number-only="getNumberOnly(episode)"
                                  @click.stop="openEpisodeTmdbPage(episode)">
                              {{ getEpisodeDisplayNumber(episode) }}
                            </span>
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
                <!-- 超小模式：在日历表格下方显示选中日期的集卡片（仅小屏手机显示） -->
                <div class="calendar-selected-episodes d-sm-none">
                  <template v-if="calendar.selectedDate">
                    <div class="calendar-month-episodes" v-if="getEpisodesByDate(calendar.selectedDate).length > 0">
                      <div class="calendar-month-episode" 
                           v-for="episode in getEpisodesByDate(calendar.selectedDate)" 
                           :key="episode.id">
                        <span class="episode-title" :title="getEpisodeShowTitleWithStatus(episode)" @click.stop="openShowTmdbPage(episode)">
                          {{ episode.show_name }}
                          <span v-if="getEpisodeFinaleStatus(episode)"> · {{ getEpisodeFinaleStatus(episode) }}</span>
                          <span v-if="isEpisodeUpdatedToday(episode) && shouldShowTodayIndicator()"
                                class="task-today-indicator"
                                :class="getTodayIndicatorClass()">
                            <i class="bi bi-stars"></i>
                          </span>
                        </span>
                        <span :class="['episode-number', { 'episode-number-reached': isEpisodeReachedProgress(episode) }]"
                              :title="getEpisodeTooltip(episode)"
                              :data-full="getEpisodeDisplayNumber(episode)"
                              :data-episode-only="getEpisodeOnlyNumber(episode)"
                              :data-number-only="getNumberOnly(episode)"
                              @click.stop="openEpisodeTmdbPage(episode)">
                          {{ getEpisodeDisplayNumber(episode) }}
                        </span>
                      </div>
                    </div>
                  </template>
                </div>
              </div>
            </div>
          </div>

        </form>
      </main>
    </div>

    <!-- 模态框 运行日志 -->
    <div class="modal" tabindex="-1" id="logModal">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title"><b>运行日志</b>
              <div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div>
            </h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <i class="bi bi-x-lg"></i>
            </button>
          </div>
          <div class="modal-body">
            <pre v-html="run_log"></pre>
          </div>
        </div>
      </div>
    </div>

    <!-- 模态框 文件选择 -->
    <div class="modal" tabindex="-1" id="fileSelectModal">
      <div class="modal-dialog modal-lg">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">
              <span v-if="fileSelect.previewRegex && fileSelect.index >= 0 && formData.tasklist[fileSelect.index]" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ formData.tasklist[fileSelect.index].use_sequence_naming ? '顺序命名预览' : (formData.tasklist[fileSelect.index].use_episode_naming ? '剧集命名预览' : '正则命名预览') }}</span>
              <span v-else-if="fileSelect.previewRegex && fileSelect.index === -1 && document.getElementById('fileSelectModal').getAttribute('data-modal-type') === 'preview'" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ createTask.taskData.use_sequence_naming ? '顺序命名预览' : (createTask.taskData.use_episode_naming ? '剧集命名预览' : '正则命名预览') }}</span>
              <span v-else-if="fileSelect.previewRegex && fileSelect.index === -1 && document.getElementById('fileSelectModal').getAttribute('data-modal-type') === 'preview-filemanager'" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ fileManager.use_sequence_naming ? '顺序命名预览' : (fileManager.use_episode_naming ? '剧集命名预览' : '正则命名预览') }}</span>
              <span v-else-if="fileSelect.selectDir" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">选择{{fileSelect.moveMode ? '移动到的' : (fileSelect.selectShare ? '需转存的' : '保存到的')}}文件夹</span>
              <span v-else style="font-weight: 600; font-family: inherit; letter-spacing: normal;">选择起始文件</span>
              <div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div>
            </h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <i class="bi bi-x-lg"></i>
            </button>
          </div>
          <div class="modal-body small">
            <div class="alert alert-warning" v-if="fileSelect.error" v-html="fileSelect.error"></div>
            <div v-else>
              <!-- 面包屑导航 -->
              <nav aria-label="breadcrumb" v-if="fileSelect.selectDir">
                <ol class="breadcrumb">
                  <li class="breadcrumb-item cursor-pointer" @click="navigateTo('0','/')"><i class="bi bi-house-door"></i></li>
                  <li v-for="(item, index) in fileSelect.paths" class="breadcrumb-item">
                    <a v-if="index != fileSelect.paths.length - 1" href="#" @click="navigateTo(item.fid, item.name)">{{ item.name }}</a>
                    <span v-else class="text-muted">{{ item.name }}</span>
                  </li>
                </ol>
              </nav>
              <!-- 文件列表 -->
              <div class="mb-3" v-if="fileSelect.previewRegex">
                <!-- 任务配置的命名预览 -->
                <div v-if="fileSelect.index >= 0 && formData.tasklist[fileSelect.index]">
                  <div v-if="formData.tasklist[fileSelect.index].use_sequence_naming">
                    <div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">顺序命名表达式：</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
                    </div>
                    <div style="padding-left: 12px; display: flex; align-items: center;">
                      <span class="text-muted" style="font-family: inherit; letter-spacing: normal;">预览结果仅供参考，新文件序号将基于网盘或转存记录中已有文件的最大序号递增。若分享链接文件不完整，实际结果可能有所不同。</span>
                    </div>
                  </div>
                  <div v-else-if="formData.tasklist[fileSelect.index].use_episode_naming">
                    <div style="padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">剧集命名表达式：</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
                    </div>
                  </div>
                  <div v-else>
                    <div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">匹配表达式：</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span>
                    </div>
                    <div style="padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">替换表达式：</span><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].replace"></span>
                    </div>
                  </div>
                </div>
                <!-- 文件管理器的命名预览 -->
                <!-- 创建任务模态框的命名预览 -->
                <div v-else-if="fileSelect.index === -1 && document.getElementById('fileSelectModal').getAttribute('data-modal-type') === 'preview'">
                  <div v-if="createTask.taskData.use_sequence_naming">
                    <div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">顺序命名表达式：</span><span class="badge badge-info" v-html="createTask.taskData.pattern"></span>
                    </div>
                    <div style="padding-left: 12px; display: flex; align-items: center;">
                      <span class="text-muted" style="font-family: inherit; letter-spacing: normal;">预览结果仅供参考，新文件序号将基于网盘或转存记录中已有文件的最大序号递增。若分享链接文件不完整，实际结果可能有所不同。</span>
                    </div>
                  </div>
                  <div v-else-if="createTask.taskData.use_episode_naming">
                    <div style="padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">剧集命名表达式：</span><span class="badge badge-info" v-html="createTask.taskData.pattern"></span>
                    </div>
                  </div>
                  <div v-else>
                    <div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">匹配表达式：</span><span class="badge badge-info" v-html="createTask.taskData.pattern"></span>
                    </div>
                    <div style="padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">替换表达式：</span><span class="badge badge-info" v-html="createTask.taskData.replace"></span>
                    </div>
                  </div>
                </div>
                <!-- 文件管理器的命名预览 -->
                <div v-else-if="fileSelect.index === -1 && document.getElementById('fileSelectModal').getAttribute('data-modal-type') === 'preview-filemanager'">
                  <div v-if="fileManager.use_sequence_naming">
                    <div style="margin-bottom: 12px; padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">顺序命名表达式：</span><span class="badge badge-info" v-html="fileManager.pattern"></span>
                    </div>
                  </div>
                  <div v-else-if="fileManager.use_episode_naming">
                    <div style="padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">剧集命名表达式：</span><span class="badge badge-info" v-html="fileManager.pattern"></span>
                    </div>
                  </div>
                  <div v-else>
                    <div style="margin-bottom: 1px; padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">匹配表达式：</span><span class="badge badge-info" v-html="fileManager.pattern"></span>
                    </div>
                    <div style="padding-left: 12px; display: flex; align-items: center;">
                      <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">替换表达式：</span><span class="badge badge-info" v-html="fileManager.replace"></span>
                    </div>
                  </div>
                </div>
              </div>
              <table class="table table-hover table-sm">
                <thead>
                  <tr>
                    <th scope="col" class="col-filename cursor-pointer" style="padding-left: 5px; font-family: inherit; letter-spacing: normal;" @click="sortFileList('file_name')">文件名 <i v-if="fileSelect.sortBy === 'file_name'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <th scope="col" class="col-rename cursor-pointer" v-if="fileSelect.selectShare || fileSelect.previewRegex" style="padding-left: 5px; font-family: inherit; letter-spacing: normal;" @click="sortFileList('file_name_re')">{{
                      fileSelect.index !== null && fileSelect.index >= 0 && formData.tasklist[fileSelect.index] ?
                        (formData.tasklist[fileSelect.index].use_sequence_naming ? '顺序命名' :
                        (formData.tasklist[fileSelect.index].use_episode_naming ? '剧集命名' : '正则命名')) :
                      fileSelect.index === -1 && (document.getElementById('fileSelectModal').getAttribute('data-modal-type') === 'preview' || document.getElementById('fileSelectModal').getAttribute('data-modal-type') === 'source' || document.getElementById('fileSelectModal').getAttribute('data-modal-type') === 'start-file') ?
                        (createTask.taskData.use_sequence_naming ? '顺序命名' :
                        (createTask.taskData.use_episode_naming ? '剧集命名' : '正则命名')) :
                        (fileManager.use_sequence_naming ? '顺序命名' :
                        (fileManager.use_episode_naming ? '剧集命名' : '正则命名'))
                    }} <i v-if="fileSelect.sortBy === 'file_name_re'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                    <template v-if="!fileSelect.previewRegex">
                      <th scope="col" class="col-size cursor-pointer" style="font-family: inherit; letter-spacing: normal;" @click="sortFileList('size')">大小 <i v-if="fileSelect.sortBy === 'size'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                      <th scope="col" class="col-date cursor-pointer" style="font-family: inherit; letter-spacing: normal;" @click="sortFileList('updated_at')">修改日期 <i v-if="fileSelect.sortBy === 'updated_at'" :class="fileSelect.sortOrder === 'asc' ? 'bi bi-arrow-up' : 'bi bi-arrow-down'"></i></th>
                      <th scope="col" class="col-action" v-if="!fileSelect.selectShare && !fileSelect.moveMode" style="font-family: inherit; letter-spacing: normal;">操作</th>
                    </template>
                  </tr>
                </thead>
                <tbody>
                  <tr v-for="(file, key) in fileSelect.fileList" :key="key" 
                      @click="fileSelect.selectDir ? (file.dir ? navigateTo(file.fid, file.file_name) : selectFileItem($event, file.fid)) : selectStartFid(file.fid)" 
                      :class="{
                        'cursor-pointer': (fileSelect.previewRegex ? file.dir : (file.dir || !fileSelect.selectShare || (!fileSelect.selectDir && !file.dir))),
                        'selected-file': fileSelect.selectedFiles.includes(file.fid)
                      }" 
                      style="vertical-align: top;" 
                      @mousedown="preventTextSelection($event, file.dir)">
                    <td class="col-filename position-relative" style="padding-left: 5px; vertical-align: top;">
                      <div v-if="!file._expandedFields || !file._expandedFields.includes('file_name')" 
                           style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 25px;" 
                           :title="file.file_name" 
                           v-check-modal-overflow="key + '|file_name'">
                        <i class="bi" :class="getFileIconClass(file.file_name, file.dir)"></i> {{file.file_name}}
                      </div>
                      <div class="expand-button" v-if="isModalTextTruncated(file.file_name, key, 'file_name')" @click.stop="toggleModalExpand(key, 'file_name')">
                        <i :class="file._expandedFields && file._expandedFields.includes('file_name') ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
                      </div>
                      <div v-if="file._expandedFields && file._expandedFields.includes('file_name')" style="white-space: normal; word-break: break-word; padding-right: 25px; display: block; width: 100%; position: relative;">
                        <i class="bi" :class="getFileIconClass(file.file_name, file.dir)"></i> {{file.file_name}}
                      </div>
                    </td>
                    <td class="col-rename position-relative" v-if="fileSelect.selectShare || fileSelect.previewRegex" :class="{'text-success': file.file_name_re && !file.file_name_re.startsWith('×'), 'text-danger': !file.file_name_re || file.file_name_re === '×' || file.file_name_re.startsWith('× ')}" style="padding-left: 5px; vertical-align: top;">
                      <div v-if="!file._expandedFields || !file._expandedFields.includes('file_name_re')" 
                           style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 25px; color: inherit;" 
                           :title="file.file_name_re || '×'" 
                           v-check-modal-overflow="key + '|file_name_re'">
                        <template v-if="file.file_name_re && file.file_name_re.startsWith('× ')">
                          <span class="episode-x">×</span> <span class="episode-number-text">无法识别剧集编号</span>
                        </template>
                        <template v-else>
                          <span class="episode-x">{{file.file_name_re || '×'}}</span>
                        </template>
                      </div>
                      <div class="expand-button" v-if="isModalTextTruncated(file.file_name_re || '×', key, 'file_name_re')" @click.stop="toggleModalExpand(key, 'file_name_re')">
                        <i :class="file._expandedFields && file._expandedFields.includes('file_name_re') ? 'bi bi-chevron-up' : 'bi bi-chevron-down'"></i>
                      </div>
                      <div v-if="file._expandedFields && file._expandedFields.includes('file_name_re')" style="white-space: normal; word-break: break-word; padding-right: 25px; display: block; width: 100%; position: relative; color: inherit;">
                        <template v-if="file.file_name_re && file.file_name_re.startsWith('× ')">
                          <span class="episode-x">×</span> <span class="episode-number-text">无法识别剧集编号</span>
                        </template>
                        <template v-else>
                          <span class="episode-x">{{file.file_name_re || '×'}}</span>
                        </template>
                      </div>
                    </td>
                    <template v-if="!fileSelect.previewRegex">
                      <td class="col-size" v-if="file.dir" style="vertical-align: top;">{{ file.include_items }} 项</td>
                      <td class="col-size" v-else style="vertical-align: top;">{{file.size | size}}</td>
                      <td class="col-date" style="vertical-align: top;">{{file.updated_at | ts2date}}</td>
                      <td class="col-action" v-if="!fileSelect.selectShare && !fileSelect.moveMode" style="vertical-align: top;">
                        <a @click.stop.prevent="deleteSelectedFiles(file.fid, file.file_name, file.dir)" style="cursor: pointer;">删除文件</a>
                        <a @click.stop.prevent="deleteSelectedFiles(file.fid, file.file_name, file.dir, true)" style="cursor: pointer; margin-left: 10px;">删除文件和记录</a>
                      </td>
                    </template>
                  </tr>
                </tbody>
              </table>
            </div>
          </div>
          <div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
            <div class="file-selection-info mr-auto" style="color: var(--dark-text-color); font-size: 0.875rem; line-height: 1.5;">
              <span v-if="fileSelect.selectShare && smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length > 1 && isCurrentResourceInSearchResults()">
                第 {{ smart_param.currentResourceIndex + 1 }} 个资源，
              </span>
              共 {{ fileSelect.fileList.length }} 个项目<span v-if="fileSelect.selectedFiles.length > 0">，已选中 {{ fileSelect.selectedFiles.length }} 个项目</span>
            </div>
            <!-- 连续浏览按钮组（仅在资源搜索模式下且当前资源在搜索结果中时显示） -->
            <div v-if="fileSelect.selectShare && smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length > 1 && isCurrentResourceInSearchResults()" class="resource-navigation-buttons">
              <button type="button" class="btn btn-primary btn-sm" 
                      :disabled="smart_param.currentResourceIndex <= 0" 
                      @click="navigateToPreviousResource()"
                      title="上一个资源">
                上一个
              </button>
              <button type="button" class="btn btn-primary btn-sm" 
                      :disabled="smart_param.currentResourceIndex >= smart_param.taskSuggestions.data.length - 1" 
                      @click="navigateToNextResource()"
                      title="下一个资源">
                下一个
              </button>
            </div>
            <button type="button" class="btn btn-primary btn-cancel" @click="$('#fileSelectModal').modal('hide')" v-if="fileSelect.moveMode">取消</button>
            <button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">
              <span v-if="fileSelect.moveMode">移动到当前文件夹</span>
              <span v-else>{{fileSelect.selectShare ? '转存当前文件夹' : '保存到当前文件夹'}}</span>
            </button>
            <button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare && !fileSelect.moveMode && fileSelect.index === -1 && createTask.taskData.taskname" @click="selectCurrentFolder(true)">保存到当前位置的「<span class="badge badge-light" v-html="generateCustomFolderPath(createTask.taskData)"></span>」文件夹</button>
            <button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare && !fileSelect.moveMode && fileSelect.index !== null && fileSelect.index >= 0 && formData.tasklist[fileSelect.index]" @click="selectCurrentFolder(true)">保存到当前位置的「<span class="badge badge-light" v-html="formData.tasklist[fileSelect.index].taskname"></span>」文件夹</button>
          </div>
          <div class="modal-footer" v-if="fileSelect.previewRegex && fileSelect.index === -1 && document.getElementById('fileSelectModal').getAttribute('data-modal-type') === 'preview-filemanager'">
            <div class="file-selection-info mr-auto" style="color: var(--dark-text-color); font-size: 0.875rem; line-height: 1.5;">
              共 {{ fileSelect.fileList.filter(f => f.file_name_re && f.file_name_re !== '×' && !f.file_name_re.startsWith('×')).length }} 个项目可重命名
            </div>
            <button type="button" class="btn btn-primary btn-cancel" @click="cancelPreviewRename">取消</button>
            <button type="button" class="btn btn-primary" :disabled="!fileSelect.canUndoRename || modalLoading" @click="undoPreviewRename">撤销</button>
            <button type="button" class="btn btn-primary" :disabled="modalLoading || !fileSelect.fileList.some(f => f.file_name_re && f.file_name_re !== '×' && !f.file_name_re.startsWith('×'))" @click="applyPreviewRename">
              重命名
            </button>
          </div>
        </div>
      </div>
    </div>

    <!-- 模态框 创建任务 -->
    <div class="modal" tabindex="-1" id="createTaskModal">
      <div class="modal-dialog modal-lg">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">
              <span style="font-weight: 600; font-family: inherit; letter-spacing: normal;">{{ createTask.isEditMode ? '编辑任务' : '创建任务' }}</span>
              <div v-if="createTask.loading" class="spinner-border spinner-border-sm m-1" role="status"></div>
            </h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <i class="bi bi-x-lg"></i>
            </button>
          </div>
          <div class="modal-body small">
            <div class="alert alert-warning" v-if="createTask.error" v-html="createTask.error"></div>
            <div v-else>
              <!-- 直接复制任务配置的完整代码 -->
              <div class="alert alert-warning" role="alert" v-if="createTask.taskData.shareurl_ban" v-html="formatShareUrlBanMessage(createTask.taskData.shareurl_ban) || createTask.taskData.shareurl_ban"></div>
              <div class="form-group row">
                <label class="col-sm-2 col-form-label">任务名称</label>
                <div class="col-sm-10">
                  <div class="input-group">
                    <input type="text" name="taskname[]" class="form-control" v-model="createTask.taskData.taskname" placeholder="必填" @focus="smart_param.showSuggestions=true;focusTaskname(-1, createTask.taskData)" @input="changeTaskname(-1, createTask.taskData)">
                    <div class="dropdown-menu show task-suggestions" v-if="smart_param.showSuggestions && smart_param.index === -1 && (smart_param.taskSuggestions.success || smart_param.isSearching)">
                      <div class="dropdown-item text-muted" v-if="smart_param.isSearching" style="font-size:14px; padding: 0 8px; text-align: left;">
                        <span v-if="smart_param.validating">
                          <i class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></i>
                          正在验证链接有效性...（{{ smart_param.validateProgress.current }}/{{ smart_param.validateProgress.total }}）<span v-if="smart_param.validateProgress.valid > 0">已找到 {{ smart_param.validateProgress.valid }} 个有效链接</span>
                        </span>
                        <span v-else>正在搜索资源...</span>
                      </div>
                      <div class="dropdown-item text-muted" v-else style="font-size:14px; padding-left: 8px; text-align: left;">
                        {{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length ? `以下资源由 ${(smart_param.taskSuggestions.source || '').replace(/,\s*/g, '、')} 搜索提供（仅显示有效链接，共 ${(smart_param.taskSuggestions.data || []).length} 个），如有侵权请联系资源发布方` : "未搜索到有效资源" }}
                      </div>
                      <div v-for="suggestion in smart_param.taskSuggestions.data || []" :key="(suggestion.shareurl || '') + '_' + (suggestion.taskname || '') + '_' + (suggestion.publish_date || '')" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(-1, suggestion)" style="font-size: 14px;" :title="getSuggestionHoverTitle(suggestion)">
                        <span v-html="suggestion.verify ? '✅': ''"></span> {{ suggestion.taskname }}
                        <small class="text-muted">
                          <a :href="suggestion.shareurl" target="_blank" @click.stop> · {{ suggestion.shareurl.replace(/^https?:\/\/pan\.quark\.cn\/s\//, '') }}</a>
                          <template v-if="suggestion.source"><span class="source-badge" :class="suggestion.source.toLowerCase()" :data-publish-date="suggestion.publish_date ? ' · ' + formatPublishDate(suggestion.publish_date, suggestion.pansou_source, suggestion.source) : ''">{{ suggestion.source }}</span></template>
                        </small>
                      </div>
                    </div>
                    <div class="input-group-append" title="资源搜索">
                      <button class="btn btn-primary" type="button" @click="searchSuggestions(-1, createTask.taskData.taskname)">
                        <i v-if="smart_param.isSearching && smart_param.index === -1" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></i>
                        <i v-else class="bi bi-search"></i>
                      </button>
                      <div class="input-group-text" title="谷歌搜索">
                        <a target="_blank" :href="`https://www.google.com/search?q=%22pan.quark.cn/s%22+${cleanTaskNameForSearch(createTask.taskData.taskname)}`"><i class="bi bi-google"></i></a>
                      </div>
                      <div class="input-group-text" title="TMDB搜索">
                        <a target="_blank" :href="`https://www.themoviedb.org/search?query=${cleanTaskNameForSearch(createTask.taskData.taskname)}`"><img src="./static/images/TMDB.svg" class="tmdb-icon"></a>
                      </div>
                      <div class="input-group-text" title="豆瓣搜索">
                        <a target="_blank" :href="`https://search.douban.com/movie/subject_search?search_text=${cleanTaskNameForSearch(createTask.taskData.taskname)}`"><img src="./static/images/Douban.svg" class="douban-icon"></a>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
              <div class="form-group row" title="支持子目录链接，Web端打开分享点入目录，复制浏览器的URL即可；支持带提取码链接，查阅Wiki了解详情">
                <label class="col-sm-2 col-form-label">分享链接</label>
                <div class="col-sm-10">
                  <div class="input-group">
                    <input type="text" name="shareurl[]" class="form-control" v-model="createTask.taskData.shareurl" placeholder="必填" @blur="changeShareurl(createTask.taskData)">
                    <div class="input-group-append" v-if="createTask.taskData.shareurl">
                      <button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(-1)" title="选择需转存的文件夹"><i class="bi bi-folder"></i></button>
                      <div class="input-group-text">
                        <a target="_blank" :href="createTask.taskData.shareurl"><i class="bi bi-link-45deg"></i></a>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
              <div class="form-group row">
                <label class="col-sm-2 col-form-label">保存路径</label>
                <div class="col-sm-10">
                  <div class="input-group">
                    <input type="text" name="savepath[]" class="form-control" v-model="createTask.taskData.savepath" placeholder="必填" @focus="focusTaskname(-1, createTask.taskData)" @input="onSavepathChange(-1, createTask.taskData)">
                    <div class="input-group-append">
                      <button class="btn btn-outline-secondary" type="button" v-if="smart_param.savepath && smart_param.index == -1 && createTask.taskData.savepath != smart_param.origin_savepath" @click="createTask.taskData.savepath = smart_param.origin_savepath" title="恢复保存路径"><i class="bi bi-reply"></i></button>
                      <button class="btn btn-outline-secondary" type="button" @click="showSavepathSelect(-1)" title="选择保存到的文件夹"><i class="bi bi-folder"></i></button>
                      <button type="button" class="btn btn-outline-secondary" @click="resetFolderContent(-1)" title="重置文件夹：此操作将删除当前保存路径中的所有文件及相关转存记录，且不可恢复，请谨慎操作"><i class="bi bi-folder-x"></i></button>
                    </div>
                  </div>
                </div>
              </div>
              <div class="form-group row" :title="createTask.taskData.use_sequence_naming || createTask.taskData.use_episode_naming ? (createTask.taskData.use_sequence_naming ? '输入带{}占位符的重命名格式，如：剧名 - S01E{}、剧名.S03E{}等，{}将被替换为集序号（按文件名和修改日期智能排序），目录中的文件夹会被自动过滤' : '输入带[]占位符的重命名格式，如：剧名 - S01E[]、剧名.S03E[]等，[]将被替换为从文件名中提取的集编号，目录中的文件夹会被自动过滤') : '可用作筛选，只转存匹配到文件名的文件，留空则直接转存所有文件'">
                <label class="col-sm-2 col-form-label">命名规则</label>
                <div class="col-sm-10">
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.previewRegex=true;fileSelect.sortBy='file_name_re';fileSelect.sortOrder='desc';showShareSelect(-1)" :title="createTask.taskData.use_sequence_naming ? '预览顺序命名效果' : (createTask.taskData.use_episode_naming ? '预览剧集命名效果' : '预览正则命名效果')">
                        {{ createTask.taskData.use_sequence_naming ? '顺序命名' : (createTask.taskData.use_episode_naming ? '剧集命名' : '正则命名') }}
                      </button>
                    </div>
                    <input type="text" name="pattern[]" class="form-control" v-model="createTask.taskData.pattern" :placeholder="createTask.taskData.use_sequence_naming ? '输入带{}占位符的重命名格式，如：剧名 - S01E{}' : (createTask.taskData.use_episode_naming ? '输入带[]占位符的重命名格式，如：剧名 - S01E[]' : '匹配表达式')"  list="magicRegex" @input="detectNamingMode(createTask.taskData); onPatternChange(-1, createTask.taskData)">
                    <input v-if="!createTask.taskData.use_sequence_naming && !createTask.taskData.use_episode_naming" type="text" name="replace[]" class="form-control" v-model="createTask.taskData.replace" placeholder="替换表达式">
                    <div class="input-group-append" title="保存时只比较文件名的部分，01.mp4和01.mkv视同为同一文件，不重复转存">
                      <div class="input-group-text">
                        <input type="checkbox" v-model="createTask.taskData.ignore_extension">&nbsp;忽略后缀
                      </div>
                    </div>
                  </div>
                  <datalist id="magicRegex">
                    <option v-for="(value, key) in formData.magic_regex" :key="key" :value="`${key}`" v-html="`${value.pattern.replace('<', '<\u200B')} → ${value.replace}`"></option>
                  </datalist>
                </div>
              </div>

              <div class="form-group row" title="名称包含过滤词汇的项目不会被转存，多个词用逗号分隔，支持通过文件名和扩展名过滤文件和文件夹，支持使用保留词|过滤词的格式实现高级过滤，查阅Wiki了解详情">
                <label class="col-sm-2 col-form-label">过滤规则</label>
                <div class="col-sm-10">
                  <div class="input-group">
                    <input type="text" name="filterwords[]" class="form-control" v-model="createTask.taskData.filterwords" placeholder="可选，输入过滤词汇，用逗号分隔，名称包含过滤词汇的项目不会被转存，支持使用保留词|过滤词的格式实现高级过滤">
                  </div>
                </div>
              </div>

              <div class="form-group row" title="只转存比选中文件更新的文件，请在符合筛选条件的文件中进行选择，在更换分享链接时非常有用">
                <label class="col-sm-2 col-form-label">起始文件</label>
                <div class="col-sm-10">
                  <div class="input-group">
                    <input type="text" class="form-control" placeholder="可选，只转存比此文件更新的文件，请在符合筛选条件的文件中进行选择" name="startfid[]" v-model="createTask.taskData.startfid">
                    <div class="input-group-append" v-if="createTask.taskData.shareurl">
                      <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;showShareSelect(-1)" title="选择起始文件"><i class="bi bi-folder"></i></button>
                    </div>
                  </div>
                </div>
              </div>
              <div class="form-group row" title="匹配成功的文件夹的所有嵌套目录都会被更新，并且会应用与根目录相同的正则命名和过滤规则。注意：原理是逐级索引，深层嵌套目录的场景下效率非常低，慎用.*">
                <label class="col-sm-2 col-form-label">更新目录</label>
                <div class="col-sm-10">
                  <input type="text" name="update_subdir[]" class="form-control" v-model="createTask.taskData.update_subdir" placeholder="可选，输入需要更新的子目录的文件夹名称（或正则表达式），多个项目用竖线分隔，如：4K|1080P">
                </div>
              </div>
              <div class="form-group row">
                <label class="col-sm-2 col-form-label">截止日期</label>
                <div class="col-sm-10">
                  <div class="input-group">
                    <input type="date" name="enddate[]" class="form-control date-input-no-icon" v-model="createTask.taskData.enddate" placeholder="可选" ref="createTaskEnddate">
                    <div class="input-group-append">
                      <button type="button" class="btn btn-outline-secondary" @click="openCreateTaskDatePicker()" title="选择截止日期">
                        <i class="bi bi-calendar3"></i>
                      </button>
                    </div>
                  </div>
                </div>
              </div>
              <div class="form-group row" title="只在勾选的星期时才运行，对周更的内容非常有用">
                <label class="col-sm-2 col-form-label">执行周期</label>
                <div class="col-sm-10 col-form-label">
                  <div class="form-check form-check-inline" title="也可用作任务总开关">
                    <input class="form-check-input" type="checkbox" :checked="createTask.taskData.runweek.length === 7" @change="toggleAllWeekdays(createTask.taskData)" :indeterminate.prop="createTask.taskData.runweek.length > 0 && createTask.taskData.runweek.length < 7" :disabled="(createTask.taskData.execution_mode || formData.execution_mode || 'manual') === 'auto'">
                    <label class="form-check-label" :class="{'text-muted': (createTask.taskData.execution_mode || formData.execution_mode || 'manual') === 'auto'}">全选</label>
                  </div>
                  <div class="form-check form-check-inline" v-for="(day, index) in weekdays" :key="index">
                    <input class="form-check-input" type="checkbox" v-model="createTask.taskData.runweek" :value="index+1" :disabled="(createTask.taskData.execution_mode || formData.execution_mode || 'manual') === 'auto'">
                    <label class="form-check-label" :class="{'text-muted': (createTask.taskData.execution_mode || formData.execution_mode || 'manual') === 'auto'}" v-html="day"></label>
                  </div>
                  <div class="form-check form-check-inline">
                    <input class="form-check-input" type="checkbox" :checked="(createTask.taskData.execution_mode || formData.execution_mode || 'manual') === 'auto'" @change="createTask.taskData.execution_mode = $event.target.checked ? 'auto' : 'manual'">
                    <label class="form-check-label">自动</label>
                  </div>
                </div>
              </div>
              <div class="form-group row" v-if="Object.keys(getAvailablePlugins(formData.plugins)).length" :title="getCreateTaskPluginConfigTitle()">
                <label class="col-sm-2 col-form-label">插件配置</label>
                <div class="col-sm-10">
                  <v-jsoneditor v-model="createTask.taskData.addition" :options="{mode:'tree'}" :plus="false" height="162px" :disabled="isPluginConfigDisabled(createTask.taskData)"></v-jsoneditor>
                </div>
              </div>
            </div>
          </div>
          <div class="modal-footer">
            <div class="file-selection-info mr-auto" v-if="createTask.movieData" style="color: var(--dark-text-color); font-size: 0.875rem; line-height: 1.5;">
              {{ createTask.movieData.title + (createTask.movieData.year ? ' (' + createTask.movieData.year + ')' : '') }}
            </div>
            <button type="button" class="btn btn-primary btn-cancel" @click="cancelCreateTask">取消</button>
            <template v-if="createTask.isEditMode">
              <button type="button" class="btn btn-primary" :disabled="createTask.loading" @click="confirmEditTask">
                保存
              </button>
              <button type="button" class="btn btn-primary" :disabled="createTask.loading" @click="confirmEditAndRunTask" style="margin-left: 4px;">
                保存并运行任务
              </button>
            </template>
            <template v-else>
              <button type="button" class="btn btn-primary" :disabled="createTask.loading" @click="confirmCreateTask">
                创建任务
              </button>
              <button type="button" class="btn btn-primary" :disabled="createTask.loading" @click="confirmCreateAndRunTask">
                创建并运行任务
              </button>
              <button type="button" class="btn btn-primary" :disabled="createTask.loading" @click="confirmCreateRunAndDeleteTask">
                创建、运行并删除任务
              </button>
            </template>
          </div>
        </div>
      </div>
    </div>

    <!-- 模态框 编辑元数据（放在 #app 内，继承相同样式） -->
    <div class="modal" tabindex="-1" id="editMetadataModal">
      <div class="modal-dialog modal-lg">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" style="font-weight: 600; font-family: inherit; letter-spacing: normal;">编辑元数据
              <div v-show="editMetadata && editMetadata.loading" class="spinner-border spinner-border-sm m-1" role="status"></div>
            </h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <i class="bi bi-x-lg"></i>
            </button>
          </div>
          <div class="modal-body small">
            <div>
              <div class="row">
                <div class="col-md-6 mb-2">
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text" style="background-color: var(--button-gray-background-color);">任务名称</span>
                    </div>
                    <input type="text" class="form-control" v-model="editMetadata.form.task_name" placeholder="任务名称">
                  </div>
                </div>
                <div class="col-md-6 mb-2">
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text" style="background-color: var(--button-gray-background-color);">任务类型</span>
                    </div>
                    <select class="form-control" v-model="editMetadata.form.content_type">
                      <option value="tv">剧集</option>
                      <option value="anime">动画</option>
                      <option value="variety">综艺</option>
                      <option value="documentary">纪录片</option>
                      <option value="other">其他</option>
                    </select>
                  </div>
                </div>
              </div>

              <div class="row">
                <!-- 左：匹配结果 -->
                <div class="col-md-6 mb-2">
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text" style="background-color: var(--button-gray-background-color);">匹配结果</span>
                    </div>
                    <div class="form-control matched-result" style="display: flex; align-items: center; gap: 8px;">
                      <div class="matched-main">
                        <a v-if="editMetadata.display.matched_tmdb_id" href="#" @click.prevent="openTaskMatchedTmdbPage({ match: { tmdb_id: editMetadata.display.matched_tmdb_id } })" class="matched-link">
                          {{ editMetadata.display.matched_label }}
                        </a>
                        <span v-else>{{ editMetadata.display.matched_label }}</span>
                      </div>
                      <span v-if="editMetadata.display.matched_season_number" class="matched-season-wrap">
                        <span class="input-group-text square-append">第</span>
                        <a href="#" @click.prevent="editMetadata.display.matched_tmdb_id && window.open('https://www.themoviedb.org/tv/' + editMetadata.display.matched_tmdb_id + '/season/' + parseInt(editMetadata.display.matched_season_number), '_blank')" class="matched-link matched-season">
                          {{ editMetadata.display.matched_season_number }}
                        </a>
                        <span class="input-group-text square-append">季</span>
                      </span>
                    </div>
                  </div>
                </div>

                <!-- 右：修正匹配 -->
                <div class="col-md-6 mb-2">
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text" style="background-color: var(--button-gray-background-color);">修正匹配</span>
                    </div>
                    <input type="text" class="form-control" v-model="editMetadata.form.tmdb_id" placeholder="输入电视节目的 TMDB ID">
                    <div class="input-group-append">
                      <span class="input-group-text square-append season-left">第</span>
                    </div>
                    <input type="number" min="1" class="form-control no-spinner edit-season-number"
                           v-model.number="editMetadata.form.season_number"
                           placeholder="1"
                           :style="{ width: (editMetadata.display.seasonInputWidth || '32px'), minWidth: '32px', flex: '0 0 auto', padding: '0 8px' }"
                           @input="autoSizeSeasonInput($event)"
                    >
                    <div class="input-group-append">
                      <span class="input-group-text square-append season-right">季</span>
                    </div>
                  </div>
                </div>
              </div>

              <!-- 更换海报 -->
              <div class="row">
                <div class="col-12 mb-2">
                  <div class="input-group">
                    <div class="input-group-prepend">
                      <span class="input-group-text" style="background-color: var(--button-gray-background-color);">更换海报</span>
                    </div>
                    <input type="text" class="form-control" v-model="editMetadata.form.custom_poster_url" placeholder="输入海报地址，支持网络地址或映射后的本地地址">
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div class="modal-footer">
            <div class="file-selection-info mr-auto" v-html="editMetadata.hint" style="color: var(--dark-text-color); font-size: 0.875rem; line-height: 1.5;"></div>
            <button type="button" class="btn btn-primary btn-cancel" data-dismiss="modal"><span class="btn-text">取消</span></button>
            <button type="button" class="btn btn-primary" @click="saveEditMetadata"><span class="btn-text">保存</span></button>
          </div>
        </div>
      </div>
    </div>

  </div>


  <script>
    var app = new Vue({
      el: '#app',
      data: {
        version: "[[ version ]]",
        versionTips: "",
        plugin_flags: "[[ plugin_flags ]]",
        weekdays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
        // 全局缓存穿透时间戳（兜底）
        imageCacheBustTick: 0,
        // 按节目维度的缓存穿透时间戳
        imageCacheBustById: {},       // { [tmdb_id:number]: number }
        imageCacheBustByShowName: {}, // { [show_name:string]: number }
        sidebarCollapsed: false,
        showCloudSaverPassword: false,
        showWebuiPassword: false,
        pageWidthMode: 'medium', // 页面宽度模式：narrow, medium, wide
        pluginDisplayAliases: {
          alist: 'AList',
          alist_strm: 'AList Strm',
          alist_strm_gen: 'AList Strm Gen',
          aria2: 'Aria2',
          emby: 'Emby',
          plex: 'Plex'
        },
        configHasLoaded: false,
        formData: {
          cookie: [],
          push_config: {},
          push_notify_type: 'full',
          media_servers: {},
          tasklist: [],
          magic_regex: {},
          episode_patterns: [],
          task_settings: {
            movie_save_path: "电影目录前缀/片名 (年份)",
            tv_save_path: "剧集目录前缀/剧名 (年份)/剧名 - S季数",
            anime_save_path: "动画目录前缀/剧名 (年份)/剧名 - S季数",
            variety_save_path: "综艺目录前缀/剧名 (年份)/剧名 - S季数",
            documentary_save_path: "纪录片目录前缀/剧名 (年份)/剧名 - S季数",
            movie_naming_pattern: "^(.*)\\.([^.]+)",
            movie_naming_replace: "片名 (年份).\\2",
            tv_naming_rule: "剧名 - S季数E[]",
            tv_ignore_extension: true,
            subtitle_naming_rule: "zh",
            subtitle_add_language_code: false
          },
          source: {
            cloudsaver: {
              server: "",
              username: "",
              password: "",
              token: ""
            },
            pansou: {
              server: "https://so.252035.xyz"
            }
          },
          webui: {
            username: "",
            password: ""
          },
          button_display: {
            run_task: "always",
            delete_task: "always",
            refresh_plex: "always",
            refresh_alist: "always",
            season_counts: "always",
            latest_update_date: "always",
            task_progress: "always",
            show_status: "always",
            latest_transfer_file: "always",
            today_update_indicator: "always"
          },
          poster_language: "zh-CN",
          // 显示顺序（可拖拽修改）：按钮 + 信息
          button_display_order: [
            "refresh_plex",
            "refresh_alist",
            "run_task",
            "delete_task",
            "latest_transfer_file",
            "season_counts",
            "latest_update_date",
            "task_progress",
            "show_status",
            "today_update_indicator"
          ],
          file_performance: {
            api_page_size: 200,
            cache_expire_time: 30,
            discovery_items_count: 30
          },
          performance: {
            // 追剧日历刷新周期（秒），默认6小时（21600秒）
            calendar_refresh_interval_seconds: 21600,
            // 已播出集数刷新时间，24小时制，格式：HH:MM，默认00:00
            aired_refresh_time: "00:00"
          },
          plugin_config_mode: {
            aria2: "independent",
            alist_strm_gen: "independent",
            emby: "independent"
          },
          global_plugin_config: {
            aria2: {
              auto_download: true,
              pause: false,
              auto_delete_quark_files: false
            },
            alist_strm_gen: {
              auto_gen: true
            },
            emby: {
              try_match: true,
              media_id: ""
            }
          },
          tmdb_api_key: ""
        },
        userInfoList: [], // 用户信息列表
        accountsDetail: [], // 账号详细信息列表
        newTask: {
          taskname: "",
          shareurl: "",
          savepath: "",
          pattern: "",
          replace: "",
          enddate: "",
          addition: {},
          ignore_extension: false,
          filterwords: "",
          runweek: [1, 2, 3, 4, 5, 6, 7],
          sequence_naming: "",
          use_sequence_naming: false,
          episode_naming: "",
          use_episode_naming: false,
          execution_mode: null // 将在openCreateTaskModal中设置为formData.execution_mode
        },
        run_log: "",
        taskDirs: [""],
        taskDirSelected: "",
        taskNameFilter: "",
        taskLatestRecords: {}, // 存储每个任务的最新转存记录日期
        taskLatestFiles: {}, // 存储每个任务的最近转存文件
        modalLoading: false,
        // 编辑元数据状态
        editMetadata: {
          loading: false,
          visible: false,
          original: {
            task_name: '',
            content_type: '',
            tmdb_id: '',
            season_number: ''
          },
          form: {
            task_name: '',
            content_type: '',
            tmdb_id: '',
            season_number: 1
          },
          display: {
            matched_label: '未匹配',
            matched_tmdb_id: '',
            matched_season_number: ''
          },
          hint: ''
        },
        smart_param: {
          index: null,
          savepath: "",
          origin_savepath: "",
          taskSuggestions: {
            success: false,
            data: []
          },
          showSuggestions: false,
          isSearching: false,
          validating: false,
          validateProgress: {
            total: 0,
            current: 0,
            valid: 0
          },
          searchTimer: null,
          // 新增：搜索会话号用于取消上一次验证/渲染，避免卡死和重复
          searchSessionId: 0,
          // 新增：当前浏览的资源索引，用于连续浏览功能
          currentResourceIndex: -1
        },
        activeTab: 'config',
        configModified: false,
        // 标志：当由后端推送或编辑元数据保存触发的程序性更新时，暂时抑制未保存提示
        suppressConfigModifiedOnce: false,
        fileSelect: {
          index: null,
          shareurl: "",
          stoken: "",
          fileList: [],
          paths: [],
          selectDir: true,
          selectShare: true,
          previewRegex: false,
          sortBy: "updated_at", // 默认排序字段
          sortOrder: "desc", // 默认排序顺序
          selectedFiles: [], // 存储选中的文件ID
          lastSelectedFileIndex: -1, // 记录最后选择的文件索引
          canUndoRename: false,
          moveMode: false, // 是否为移动文件模式
          moveFileIds: [] // 要移动的文件ID列表
        },
        historyParams: {
          sortBy: "transfer_time",
          order: "desc",
          page_size: 15,
          page: 1
        },
        history: {
          hasLoaded: false, // 标记是否已完成首次加载
          records: [],
          pagination: {}
        },
        historyNameFilter: "",
        historyTaskSelected: "",
        gotoPage: 1,
        totalPages: 1,
        displayedPages: [],
        allTaskNames: [],
        toastMessage: "",
        selectedRecords: [],
        lastSelectedRecordIndex: -1, // 记录最后选择的记录索引，用于Shift选择
        fileManager: {
          hasLoaded: false, // 标记是否已完成首次加载
          // 当前文件夹ID - 根据localStorage和账号索引决定初始目录
          currentFolder: (() => {
            const selectedAccountIndex = parseInt(localStorage.getItem('quarkAutoSave_selectedAccountIndex') || '0');
            return localStorage.getItem(`quarkAutoSave_fileManagerLastFolder_${selectedAccountIndex}`) || 'root';
          })(),
          // 选中的账号索引 - 从localStorage读取，默认为0
          selectedAccountIndex: parseInt(localStorage.getItem('quarkAutoSave_selectedAccountIndex') || '0'),
          // 面包屑导航路径
          paths: [],
          // 文件列表
          fileList: [],
          // 总数
          total: 0,
          // 当前页码
          currentPage: 1,
          // 页面大小
          pageSize: 15,
          // 总页数
          totalPages: 1,
          // 排序列 - 从localStorage读取，如果没有则使用默认值
          sortBy: (() => {
            const savedSort = localStorage.getItem('quarkAutoSave_fileManagerSort');
            if (savedSort) {
              try {
                return JSON.parse(savedSort).sortBy || 'file_name';
              } catch (e) {
                return 'file_name';
              }
            }
            return 'file_name';
          })(),
          // 排序方式 - 从localStorage读取，如果没有则使用默认值
          sortOrder: (() => {
            const savedSort = localStorage.getItem('quarkAutoSave_fileManagerSort');
            if (savedSort) {
              try {
                return JSON.parse(savedSort).sortOrder || 'asc';
              } catch (e) {
                return 'asc';
              }
            }
            return 'asc';
          })(),
          // 命名模式
          use_sequence_naming: false,
          use_episode_naming: false,
          // 正则表达式
          pattern: '',
          replace: '',
          // 过滤关键词
          filterwords: '',
          // 包含文件夹
          include_folders: false,
          // 选中的文件
          selectedFiles: [],
          // 上次选中的文件索引
          lastSelectedFileIndex: -1,
          // 跳转到页码
          gotoPage: 1
        },
        batchRename: {
          loading: false,
          results: [],
          namingMode: 'regex',
          errors: []
        },
        // 影视发现相关数据
        discovery: {
          hasLoaded: false, // 标记是否已完成首次加载
          isInitialized: false, // 标记是否已初始化，避免页面刷新时闪烁
          error: null,
          items: [],
          selectedMainCategory: localStorage.getItem('discovery_main_category') || 'movie_hot', // 从localStorage读取或默认选中热门电影
          selectedSubCategory: localStorage.getItem('discovery_sub_category') || '全部', // 从localStorage读取或默认选中全部
          mainCategories: [
            { key: 'movie_hot', name: '热门电影' },
            { key: 'movie_latest', name: '最新电影' },
            { key: 'movie_top', name: '豆瓣高分' },
            { key: 'movie_underrated', name: '冷门佳片' },
            { key: 'tv_drama', name: '热门剧集' },
            { key: 'tv_animation', name: '热门动画' },
            { key: 'tv_variety', name: '热门综艺' },
            { key: 'tv_documentary', name: '热门纪录片' }
          ],
          subCategories: {
            movie_hot: [
              { key: '全部', name: '全部' },
              { key: '华语', name: '华语' },
              { key: '欧美', name: '欧美' },
              { key: '日本', name: '日本' },
              { key: '韩国', name: '韩国' }
            ],
            movie_latest: [
              { key: '全部', name: '全部' },
              { key: '华语', name: '华语' },
              { key: '欧美', name: '欧美' },
              { key: '日本', name: '日本' },
              { key: '韩国', name: '韩国' }
            ],
            movie_top: [
              { key: '全部', name: '全部' },
              { key: '华语', name: '华语' },
              { key: '欧美', name: '欧美' },
              { key: '日本', name: '日本' },
              { key: '韩国', name: '韩国' }
            ],
            movie_underrated: [
              { key: '全部', name: '全部' },
              { key: '华语', name: '华语' },
              { key: '欧美', name: '欧美' },
              { key: '日本', name: '日本' },
              { key: '韩国', name: '韩国' }
            ],
            tv_drama: [
              { key: '综合', name: '全部' },
              { key: '国产剧', name: '华语' },
              { key: '欧美剧', name: '欧美' },
              { key: '日剧', name: '日本' },
              { key: '韩剧', name: '韩国' }
            ],
            tv_animation: [
              { key: '动画', name: '全部' }
            ],
            tv_variety: [
              { key: '综合', name: '全部' },
              { key: '国内', name: '国内' },
              { key: '国外', name: '国外' }
            ],
            tv_documentary: [
              { key: '纪录片', name: '全部' }
            ]
          }
        },
        // 追剧日历相关数据
        calendar: {
          hasLoaded: false,
          error: null,
          tasks: [],
          taskMapByName: {},
          manageMode: false,
          layoutTick: 0,
          contentTypes: [],
          // 仅用于“已转存”判定的进度映射（来源：/task_latest_info.latest_files）
          progressByTaskName: {},
          progressByShowName: {},
          // 当日更新数据（来源：/api/calendar/today_updates_local）
          todayUpdatesByTaskName: {}, // { [task_name]: true }
          todayUpdatesByShow: {},     // { [show_name]: Set(keys) }，key: 'S01E02' 或 'D:YYYY-MM-DD'
          // 从本地存储读取已选择的类型，默认 all
          selectedType: (localStorage.getItem('calendar_selected_type') || 'all'),
          nameFilter: '',
          taskFilter: '',
          taskNames: [],
          viewMode: (localStorage.getItem('calendar_view_mode') === 'month' || localStorage.getItem('calendar_view_mode') === 'poster')
            ? localStorage.getItem('calendar_view_mode')
            : 'poster', // poster 或 month（支持持久化）
          mergeEpisodes: localStorage.getItem('calendar_merge_episodes') === 'true', // 合并集模式（支持持久化）
          currentDate: new Date(),
          weekDates: [],
          monthWeeks: [],
          episodes: [],
          // 选中日期（YYYY-MM-DD），默认今天
          selectedDate: (function(){
            const d = new Date();
            const y = d.getFullYear();
            const m = (d.getMonth()+1).toString().padStart(2,'0');
            const day = d.getDate().toString().padStart(2,'0');
            return `${y}-${m}-${day}`;
          })()
        },
        // 任务列表：类型筛选状态
        tasklist: {
          selectedType: (localStorage.getItem('tasklist_selected_type') || 'all'),
          contentTypes: [],
          // 任务列表视图模式：list 或 poster（默认列表视图，支持持久化）
          viewMode: (localStorage.getItem('tasklist_view_mode') === 'poster') ? 'poster' : 'list'
        },
        // 任务列表海报加载标记：taskname -> boolean（用于优先显示图片，再渐进显示其他信息）
        tasklistPosterLoaded: {},
        // 任务列表排序设置（记忆到localStorage）
        tasklistSort: (() => {
          try {
            const saved = JSON.parse(localStorage.getItem('tasklist_sort_options') || '{}');
            let by = ['index','name','progress','update_time'].includes(saved.by) ? saved.by : 'index';
            const order = (saved.order === 'asc' || saved.order === 'desc') ? saved.order : 'asc';
            // 注意：这里无法检查 TMDB API 配置，会在 mounted 中处理
            return { by, order };
          } catch (e) {
            return { by: 'index', order: 'asc' };
          }
        })(),
        // 日历页面resize监听器
        calendarResizeHandler: null,
        // 日历自动检测更新相关
        calendarAutoWatchTimer: null,
        calendarLatestFilesSignature: '',
        calendarAutoWatchTickRef: null,
        calendarAutoWatchFocusHandler: null,
        calendarAutoWatchVisibilityHandler: null,
        // 任务列表自动检测更新相关
        tasklistAutoWatchTimer: null,
        tasklistLatestFilesSignature: '',
        // 全局 SSE 单例与监听标志
        appSSE: null,
        appSSEInitialized: false,
        calendarSSEListenerAdded: false,
        tasklistSSEListenerAdded: false,
        // 存放已绑定的事件处理器，便于避免重复绑定
        onCalendarChangedHandler: null,
        onTasklistChangedHandler: null,
        // 兼容旧字段（不再使用独立 SSE 实例）
        tasklistSSE: null,
        calendarSSE: null,
        // 创建任务相关数据
        createTask: {
          loading: false,
          error: null,
          movieData: null, // 存储当前要创建任务的影视作品数据
          isEditMode: false, // 是否为编辑模式
          editTaskIndex: null, // 编辑的任务索引
          taskData: {
            taskname: "",
            shareurl: "",
            savepath: "",
            pattern: "",
            replace: "",
            enddate: "",
            addition: {},
            ignore_extension: false,
            filterwords: "",
            runweek: [1, 2, 3, 4, 5, 6, 7],
            sequence_naming: "",
            use_sequence_naming: false,
            episode_naming: "",
            use_episode_naming: false,
            update_subdir: "",
            startfid: "",
            shareurl_ban: null
          }
        }
      },
      computed: {
        episodePatternsText: {
          get() {
            if (!this.formData.episode_patterns) return '';
            return this.formData.episode_patterns.map(p => p.regex || '').join('|');
          },
          set(value) {
            // 支持竖线分割的多个正则表达式
            if (!value || value.trim() === '') {
              this.formData.episode_patterns = [];
              return;
            }
            
            // 按竖线分割并创建多个正则表达式对象
            const patterns = value.split('|').map(p => p.trim()).filter(p => p !== '');
            this.formData.episode_patterns = patterns.map(regex => ({ regex }));
          }
        },
        // 管理视图：按任务名（拼音）排序并应用顶部筛选
        managementTasksFiltered() {
          if (!this.calendar.tasks || this.calendar.tasks.length === 0) return [];
          let list = this.calendar.tasks.slice();
          // 顶部筛选：类型
          if (this.calendar.selectedType && this.calendar.selectedType !== 'all') {
            list = list.filter(t => (t.content_type || 'other') === this.calendar.selectedType);
          }
          // 顶部筛选：名称关键词（任务名或剧名）
          if (this.calendar.nameFilter && this.calendar.nameFilter.trim() !== '') {
            const kw = this.calendar.nameFilter.toLowerCase();
            list = list.filter(t => (t.task_name || '').toLowerCase().includes(kw) || (t.show_name || '').toLowerCase().includes(kw));
          }
          // 顶部筛选：任务筛选（精确任务名）
          if (this.calendar.taskFilter && this.calendar.taskFilter.trim() !== '') {
            list = list.filter(t => (t.task_name || '') === this.calendar.taskFilter);
          }
          // 按匹配状态和任务名称拼音排序：匹配的项目在前，未匹配的项目在后
          try {
            list.sort((a, b) => {
              // 首先按匹配状态排序：匹配的在前，未匹配的在后
              const aMatched = !!(a.matched_show_name && a.matched_show_name.trim() !== '');
              const bMatched = !!(b.matched_show_name && b.matched_show_name.trim() !== '');
              
              if (aMatched !== bMatched) {
                return aMatched ? -1 : 1; // 匹配的排在前面
              }
              
              // 匹配状态相同时，按任务名称拼音排序
              const aKey = pinyinPro.pinyin(a.task_name || '', { toneType: 'none', type: 'string' }).toLowerCase();
              const bKey = pinyinPro.pinyin(b.task_name || '', { toneType: 'none', type: 'string' }).toLowerCase();
              if (aKey < bKey) return -1; if (aKey > bKey) return 1; return 0;
            });
          } catch (e) {}
          return list;
        },

        // 任务列表：带排序的视图数据（使用计算属性，不修改原始数据）
        sortedTasklist() {
          return this.getSortedTasklist();
        },

        // 判断是否应该显示任务列表（避免排序闪烁）
        shouldShowTasklist() {
          const { by } = this.tasklistSort || { by: 'index' };
          
          // 任务编号和任务名称排序不需要额外数据，可以直接显示
          if (by === 'index' || by === 'name') {
            return true;
          }
          
          // 任务进度排序需要 taskLatestFiles 和 calendar.tasks 数据
          if (by === 'progress') {
            return this.taskLatestFiles && Object.keys(this.taskLatestFiles).length > 0 &&
                   this.calendar.tasks && Array.isArray(this.calendar.tasks) && this.calendar.tasks.length > 0;
          }
          
          // 更新时间排序需要 taskLatestRecords 数据
          if (by === 'update_time') {
            return this.taskLatestRecords && Object.keys(this.taskLatestRecords).length > 0;
          }
          
          // 默认显示
          return true;
        },

        // 海报和日历视图下过滤掉所有内容都是未匹配的分类
        filteredContentTypes() {
          if (!this.calendar.tasks || this.calendar.tasks.length === 0) {
            return this.calendar.contentTypes;
          }
          
          // 只在海报和日历视图下进行过滤
          if (this.calendar.manageMode) {
            return this.calendar.contentTypes;
          }
          
          return this.calendar.contentTypes.filter(type => {
            // 获取该分类下的所有任务
            const typeTasks = this.calendar.tasks.filter(task => (task.content_type || 'other') === type);
            
            // 如果该分类下没有任务，隐藏该分类按钮
            if (typeTasks.length === 0) {
              return false;
            }
            
            // 检查该分类下是否有匹配的任务
            const hasMatchedTask = typeTasks.some(task => 
              task.matched_show_name && task.matched_show_name.trim() !== ''
            );
            
            // 如果有匹配的任务，保留该分类按钮
            return hasMatchedTask;
          });
        },

        // 管理视图：将过滤后的任务按当前海报列数进行分栏，复用相同布局
        managementColumns() {
          const tasks = this.managementTasksFiltered;
          if (!tasks || tasks.length === 0) return [];
          // 依赖布局tick以在窗口变化时强制重新计算
          void this.calendar.layoutTick;
          // 与周视图一致的列数推导逻辑
          const availableWidth = this.getCalendarAvailableWidth ? this.getCalendarAvailableWidth() : 1200;
          const minColumnWidth = 140;
          const columnGap = 20;
          // 列数计算与周视图一致，加入 eps 避免边界像素/滚动条导致的多列
          const eps = 0.1;
          // 桌面端也允许最少 2 列，避免在某些宽度下被强制多出一列
          const columns = Math.max(2, Math.floor((availableWidth + columnGap - eps) / (minColumnWidth + columnGap)));
          const cols = Array.from({ length: columns }, () => []);
          // 简单逐列分配，保证与日历海报列宽/间距一致
          tasks.forEach((t, idx) => {
            cols[idx % columns].push(t);
          });
          return cols;
        },
        // 基于已加载的日历剧集构建：show_name -> poster_local_path 映射
        posterByShowName() {
          const map = {};
          try {
            (this.calendar.episodes || []).forEach(ep => {
              const key = (ep && ep.show_name) ? String(ep.show_name).trim() : '';
              const poster = ep && ep.poster_local_path;
              if (key && poster && !map[key]) map[key] = poster;
            });
          } catch (e) {}
          return map;
        },
        
        
        filteredHistoryRecords() {
          // 直接返回服务器端已筛选的数据
          if (!this.history.records || this.history.records.length === 0) {
            return [];
          }
          return this.history.records;
        },
        historyTasks() {
          // 如果已经通过API加载了所有任务名称，则使用它
          if (this.allTaskNames && this.allTaskNames.length > 0) {
            return this.allTaskNames;
          }
          
          // 否则，从当前页的历史记录中提取唯一的任务名称（后备方案）
          if (!this.history.records || this.history.records.length === 0) {
            return [];
          }
          
          const taskNames = new Set();
          this.history.records.forEach(record => {
            if (record.task_name) {
              taskNames.add(record.task_name);
            }
          });
          
          return this.sortTaskNamesByPinyin([...taskNames]);
        },
        taskNames() {
          // 从任务列表中提取唯一的任务名称
          if (!this.formData.tasklist || this.formData.tasklist.length === 0) {
            return [];
          }
          
          const taskNames = new Set();
          this.formData.tasklist.forEach(task => {
            if (task.taskname) {
              taskNames.add(task.taskname);
            }
          });
          
          return this.sortTaskNamesByPinyin([...taskNames]);
        },
        totalPages() {
          // 直接使用后端返回的total_pages
          if (this.history.pagination && this.history.pagination.total_pages) {
            return parseInt(this.history.pagination.total_pages);
          }
          
          // 后备方案
          return 1;
        },
        displayedPages() {
          // 显示有限数量的页码，最多显示5个页码
          const pageCount = this.totalPages;
          const currentPage = this.historyParams.page;
          
          if (pageCount <= 5) {
            // 页数少于等于5，显示所有页码
            return Array.from({ length: pageCount }, (_, i) => i + 1);
          } else {
            // 页数多于5，显示当前页附近的页码
            const pages = [];
            let startPage = Math.max(currentPage - 2, 1);
            let endPage = Math.min(startPage + 4, pageCount);
            
            if (endPage - startPage < 4) {
              startPage = Math.max(endPage - 4, 1);
            }
            
            for (let i = startPage; i <= endPage; i++) {
              pages.push(i);
            }
            
            return pages;
          }
        },
        // 影视发现计算属性
        currentSubCategories() {
          return this.discovery.subCategories[this.discovery.selectedMainCategory] || [];
        }
      },
      filters: {
        ts2date: function (value) {
          const date = new Date(value);
          return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;
        },
        size: function (value) {
          if (!value) return "";
          const unitArr = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
          const srcsize = parseFloat(value);
          const index = srcsize ? Math.floor(Math.log(srcsize) / Math.log(1024)) : 0;
          const size = (srcsize / Math.pow(1024, index)).toFixed(1).replace(/\.?0+$/, "");
          return size + " " + unitArr[index];
        }
      },
      watch: {
        formData: {
          handler(newVal, oldVal) {
            if (this.suppressConfigModifiedOnce) {
              // 消费一次抑制标志，不触发未保存提示
              this.suppressConfigModifiedOnce = false;
            } else {
              this.configModified = true;
            }
          },
          deep: true
        },
        // 名称筛选变化时重建月视图
        'calendar.nameFilter': function(newVal, oldVal) {
          if (this.calendar.viewMode === 'month') {
            this.initializeCalendarDates();
          }
        },
        // 任务筛选变化时重建月视图
        'calendar.taskFilter': function(newVal, oldVal) {
          if (this.calendar.viewMode === 'month') {
            this.initializeCalendarDates();
          }
        },
        // 侧边栏折叠/展开变化时，触发布局重算
        sidebarCollapsed(val) {
          if (this.activeTab === 'calendar') {
            if (this.calendar.manageMode) {
              this.calendar.layoutTick = Date.now();
            } else if (this.calendar.viewMode === 'poster') {
              this.updateWeekDates();
            }
          }
        },
        // 页面宽度模式变化（窄/中/宽），触发布局重算
        pageWidthMode(val) {
          if (this.activeTab === 'calendar') {
            if (this.calendar.manageMode) {
              this.calendar.layoutTick = Date.now();
            } else if (this.calendar.viewMode === 'poster') {
              this.updateWeekDates();
            }
          }
        },

        historyNameFilter: {
          handler(newVal, oldVal) {
            // 延迟加载，避免频繁请求
            if (this._filterTimeout) {
              clearTimeout(this._filterTimeout);
            }
            this._filterTimeout = setTimeout(() => {
              this.loadHistoryRecords();
            }, 500);
          }
        },
        historyTaskSelected: {
          handler(newVal, oldVal) {
            // 选择任务筛选条件后立即加载数据
            this.loadHistoryRecords();
          }
        },
        activeTab(newValue, oldValue) {
          // 如果切换到任务列表页面，则刷新任务最新信息和元数据
          if (newValue === 'tasklist') {
            // 确保全局 SSE 已建立
            try { this.ensureGlobalSSE(); } catch (e) {}
            this.loadTaskLatestInfo();
            this.loadTasklistMetadata();
            // 启动任务列表的后台监听
            this.startTasklistAutoWatch();
          } else if (oldValue === 'tasklist') {
            // 离开任务列表页面时停止后台监听
            this.stopTasklistAutoWatch();
          }
          // 切换到追剧日历：立刻检查一次并启动后台监听；离开则停止监听
          if (newValue === 'calendar') {
            // 确保全局 SSE 已建立
            try { this.ensureGlobalSSE(); } catch (e) {}
            // 立即检查一次（若已初始化过监听，直接调用tick引用）
            // 先本地读取一次，立刻应用“已转存”状态（不依赖轮询）
            try { this.loadCalendarEpisodesLocal && this.loadCalendarEpisodesLocal(); } catch (e) {}
            // 先启动监听以确保 tickRef 已就绪
            this.startCalendarAutoWatch();
            // 重置签名，确保本次切换能触发一次增量检查
            this.calendarLatestFilesSignature = '';
            // 进入页面即刻用现有的最近文件重建一次进度映射，避免UI滞后
            try {
              this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(this.taskLatestFiles || {});
              this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
            } catch (e) {}
            // 立刻触发一次增量检查与热更新（无需等待下一轮定时器）
            if (this.calendarAutoWatchTickRef) {
              this.calendarAutoWatchTickRef();
            }
            // 离开再返回时恢复管理模式记忆
            try {
              const savedManage = localStorage.getItem('calendar_manage_mode');
              if (savedManage === 'true') this.calendar.manageMode = true;
            } catch (e) {}
          } else if (oldValue === 'calendar') {
            this.stopCalendarAutoWatch();
          }
          // 如果切换到文件整理页面，则加载文件列表
          if (newValue === 'filemanager') {
            this.fetchAccountsDetail();
            this.loadFileListWithoutLoading(this.fileManager.currentFolder);
          }
          // 如果切换到影视发现页面，则加载榜单数据
          if (newValue === 'discovery') {
            this.loadDiscoveryData();
          }
        },
        'fileManager.pattern': {
          handler(newValue, oldValue) {
            // 自动检测并切换命名模式
            this.detectFileManagerNamingMode();
          }
        }
      },
      mounted() {
        this.fetchData();
        this.checkNewVersion();
        this.fetchUserInfo(); // 获取用户信息
        this.fetchAccountsDetail(); // 获取账号详细信息

        // 应用级别：在挂载时确保全局 SSE 建立一次
        try { this.ensureGlobalSSE(); } catch (e) {}

        // 迁移旧的localStorage数据到新格式（为每个账号单独存储目录）
        this.migrateFileManagerFolderData();

        // 添加点击事件监听
        document.addEventListener('click', this.handleOutsideClick);
        document.addEventListener('click', this.handleModalOutsideClick);
        document.addEventListener('click', this.handleFileManagerOutsideClick);
        
        // 添加模态框关闭事件监听
        $('#fileSelectModal').on('hidden.bs.modal', () => {
          this.fileSelect.selectedFiles = [];
          this.fileSelect.lastSelectedFileIndex = -1;
          // 重置移动模式相关参数
          this.fileSelect.moveMode = false;
          this.fileSelect.moveFileIds = [];
          // 重置z-index
          document.getElementById('fileSelectModal').style.zIndex = '';
        });
        
        // 检查本地存储中的标签页状态
        const savedTab = localStorage.getItem('quarkAutoSave_activeTab');
        if (savedTab) {
          this.activeTab = savedTab;
        }
        
        // 从本地存储中恢复侧边栏折叠状态
        const savedSidebarState = localStorage.getItem('quarkAutoSave_sidebarCollapsed');
        if (savedSidebarState) {
          this.sidebarCollapsed = savedSidebarState === 'true';
        }
        
        // 从本地存储中恢复用户设置的每页记录数
        const savedPageSize = localStorage.getItem('quarkAutoSave_pageSize');
        if (savedPageSize) {
          this.historyParams.page_size = savedPageSize === 'all' ? 99999 : parseInt(savedPageSize);
        }
        
        // 从本地存储中恢复文件管理器的每页显示数
        const savedFileManagerPageSize = localStorage.getItem('quarkAutoSave_fileManagerPageSize');
        if (savedFileManagerPageSize) {
          this.fileManager.pageSize = savedFileManagerPageSize === 'all' ? 99999 : parseInt(savedFileManagerPageSize);
        }
        
        // 从本地存储中恢复页面宽度设置
        const savedPageWidthMode = localStorage.getItem('quarkAutoSave_pageWidthMode');
        if (savedPageWidthMode) {
          this.pageWidthMode = savedPageWidthMode;
          document.body.classList.add('page-width-' + this.pageWidthMode);
        } else {
          // 默认使用中等宽度
          document.body.classList.add('page-width-medium');
        }
        
        $('[data-toggle="tooltip"]').tooltip();
        // 窗口尺寸变化时，及时重算追剧日历布局，避免列数滞后
        this._onCalendarResize = this.debounce(() => {
          if (this.activeTab === 'calendar') {
            this.calendar.layoutTick = Date.now();
            this.initializeCalendarDates();
          }
        }, 80);
        window.addEventListener('resize', this._onCalendarResize);
        window.addEventListener('orientationchange', this._onCalendarResize);
        // 使用 ResizeObserver 监听日历根容器尺寸变化，实时刷新列数
        if (window.ResizeObserver) {
          this._calendarResizeObserver = new ResizeObserver(() => {
            if (this.activeTab === 'calendar') {
              this._onCalendarResize();
            }
          });
          if (this.$refs && this.$refs.calendarRoot) {
            this._calendarResizeObserver.observe(this.$refs.calendarRoot);
          }
        }
        document.addEventListener('keydown', this.handleKeyDown);
        document.addEventListener('click', (e) => {
          // 如果点击的是输入框、搜索按钮或下拉菜单本身，不关闭下拉菜单
          if (e.target.closest('.input-group input') || 
              e.target.closest('.btn-primary[type="button"]') || 
              e.target.closest('.dropdown-menu.task-suggestions') ||
              e.target.closest('.bi-search') ||
              // 新增：点击发生在文件选择模态框内（包括右上角关闭按钮）时，不关闭下拉
              e.target.closest('#fileSelectModal')) {
            return;
          }
          // 只隐藏下拉菜单，不清空搜索结果，这样点击同一任务的输入框时还能看到之前的搜索结果
          this.smart_param.showSuggestions = false;
        });
        
        // 添加点击事件监听器，用于在点击表格外区域时取消选择记录
        document.addEventListener('click', this.handleOutsideClick);
        
        // 添加点击事件监听器，用于在点击模态框表格外区域时取消选择文件
        document.addEventListener('click', this.handleModalOutsideClick);
        
        // 添加模态框关闭事件监听，清空选中文件列表
        $('#fileSelectModal').on('hidden.bs.modal', () => {
          this.fileSelect.selectedFiles = [];
          this.fileSelect.lastSelectedFileIndex = -1;
          // 重置移动模式相关参数
          this.fileSelect.moveMode = false;
          this.fileSelect.moveFileIds = [];
          // 重置z-index
          document.getElementById('fileSelectModal').style.zIndex = '';
          // 重置资源浏览索引
          this.smart_param.currentResourceIndex = -1;
        });
        
        window.addEventListener('beforeunload', this.handleBeforeUnload);
        
        // 监听模态框显示事件，检查滚动条状态
        $('#fileSelectModal').on('shown.bs.modal', this.checkPreviewScrollbar);
        // 监听窗口大小改变，重新检查滚动条状态
        window.addEventListener('resize', this.checkPreviewScrollbar);
        
        // 初始化时检查所有任务的命名模式
        setTimeout(() => {
          if (this.formData.tasklist && this.formData.tasklist.length > 0) {
            this.formData.tasklist.forEach(task => {
              // 检查现有的顺序命名设置
              if (task.use_sequence_naming && task.sequence_naming) {
                // 已经设置过顺序命名的，将顺序命名模式转换为匹配表达式
                if (!task.pattern || task._pattern_backup) {
                  task.pattern = task.sequence_naming;
                }
              } else if (task.use_episode_naming && task.episode_naming) {
                // 已经设置过剧集命名的，将剧集命名模式转换为匹配表达式
                if (!task.pattern || task._pattern_backup) {
                  task.pattern = task.episode_naming;
                }
              } else {
                // 检测是否包含顺序命名或剧集命名模式
                this.detectNamingMode(task);
              }
            });
          }
          
          // 确保剧集识别模式字段存在（但不自动添加默认规则）
          if (!this.formData.episode_patterns) {
            this.formData.episode_patterns = [];
          }
          
          // 如果当前标签是历史记录，则加载历史记录
          if (this.activeTab === 'history') {
            this.loadHistoryRecords();
            // 加载所有任务名称用于筛选
            this.loadAllTaskNames();
          }
          
          // 如果当前标签是文件整理，则加载文件列表
          if (this.activeTab === 'filemanager') {
            this.fetchAccountsDetail();
            this.loadFileListWithoutLoading(this.fileManager.currentFolder);
          }
          
          // 添加对history.pagination的监听
          this.$watch('history.pagination', function(newVal) {
            if (newVal && newVal.total_pages) {
              this.$nextTick(() => {
                // 强制Vue更新视图
                this.$forceUpdate();
              });
            }
          }, { deep: true });
          
          // 检查分享链接状态
          this.checkShareUrlStatus();

          // 添加移动端任务列表展开/收起状态监听
          this.setupMobileTaskListToggle();
        }, 500);
      },
      beforeDestroy() {
        window.removeEventListener('beforeunload', this.handleBeforeUnload);
        if (this._onCalendarResize) {
          window.removeEventListener('resize', this._onCalendarResize);
          window.removeEventListener('orientationchange', this._onCalendarResize);
        }
        if (this._calendarResizeObserver && this.$refs && this.$refs.calendarRoot) {
          this._calendarResizeObserver.unobserve(this.$refs.calendarRoot);
          this._calendarResizeObserver.disconnect();
          this._calendarResizeObserver = null;
        }
        // 移除点击事件监听器
        document.removeEventListener('click', this.handleOutsideClick);
        // 清理日历后台监听
        this.stopCalendarAutoWatch();
      },
      methods: {
        // 计算来源徽标 class：多来源时使用 multi-source，单来源使用小写来源名
        getSourceBadgeClass(source) {
          try {
            if (!source) return '';
            if (typeof source === 'string' && source.indexOf('·') !== -1) return 'multi-source';
            return String(source).toLowerCase();
          } catch (e) { return ''; }
        },
        // 启动任务列表轮询兜底（仅在未运行时启动）
        startTasklistPollingFallback() {
          try {
            if (!this.tasklistAutoWatchTimer) {
              this.tasklistAutoWatchTimer = setInterval(async () => {
                try {
                  const res = await axios.get('/task_latest_info');
                  if (res.data && res.data.success) {
                    const latestFiles = res.data.data.latest_files || {};
                    const sig = this.calcLatestFilesSignature(latestFiles);
                    if (sig && sig !== this.tasklistLatestFilesSignature) {
                      // 更新签名，触发热更新
                      this.tasklistLatestFilesSignature = sig;
                      this.taskLatestFiles = latestFiles;
                      // 重新加载任务元数据，确保海报和元数据能热更新
                      await this.loadTasklistMetadata();
                    }
                  }
                } catch (e) {
                  console.warn('任务列表后台监听检查失败:', e);
                }
              }, 60000);
            }
          } catch (e) {}
        },
        // 确保全局 SSE 单例存在（仅建立一次）
        ensureGlobalSSE() {
          try {
            if (this.appSSEInitialized && this.appSSE) return;
            if (!this.appSSE) {
              this.appSSE = new EventSource('/api/calendar/stream');
            }
            this.appSSEInitialized = true;
            // 统一 onopen：SSE 成功后停止两侧轮询
            this.appSSE.onopen = () => {
              try {
                if (this.calendarAutoWatchTimer) {
                  clearInterval(this.calendarAutoWatchTimer);
                  this.calendarAutoWatchTimer = null;
                }
              } catch (e) {}
              try {
                if (this.tasklistAutoWatchTimer) {
                  clearInterval(this.tasklistAutoWatchTimer);
                  this.tasklistAutoWatchTimer = null;
                }
              } catch (e) {}
            };
            // 统一 onerror：关闭SSE并回退双侧轮询
            this.appSSE.onerror = () => {
              try { this.appSSE.close(); } catch (e) {}
              this.appSSE = null;
              this.appSSEInitialized = false;
              // 日历回退：若没有轮询定时器，则恢复轮询并立即执行一次
              try {
                if (!this.calendarAutoWatchTimer && this.calendarAutoWatchTickRef) {
                  const baseIntervalMs = this.calendar && this.calendar.manageMode ? 5 * 1000 : 60 * 1000;
                  this.calendarAutoWatchTimer = setInterval(this.calendarAutoWatchTickRef, baseIntervalMs);
                  this.calendarAutoWatchTickRef();
                }
              } catch (e) {}
              // 任务列表回退：若没有轮询定时器，则恢复轮询
              try {
                if (!this.tasklistAutoWatchTimer) {
                  this.startTasklistPollingFallback();
                }
              } catch (e) {}
            };
            // ping 心跳占位
            try { this.appSSE.addEventListener('ping', () => {}); } catch (e) {}
          } catch (e) {
            // 忽略失败，后续可回退轮询
          }
        },
        // 任务列表海报标题（悬停：#编号 任务名称 · 状态）
        getTasklistPosterTitle(task, index) {
          try {
            const num = String(((task && task.__originalIndex !== undefined) ? task.__originalIndex : index) + 1).padStart(2, '0');
            const name = task && task.taskname ? String(task.taskname).trim() : '';
            const status = this.getTaskShowStatus(name);
            return `#${num} ${name}${status ? ' · ' + status : ''}`;
          } catch (e) { return ''; }
        },
        // 切换任务列表视图模式并持久化
        toggleTasklistViewMode() {
          try {
            this.tasklist.viewMode = this.tasklist.viewMode === 'list' ? 'poster' : 'list';
            try { localStorage.setItem('tasklist_view_mode', this.tasklist.viewMode); } catch (e) {}
          } catch (e) {}
        },
        // 根据任务名获取对应的日历任务对象（若已加载）
        getCalendarTaskByName(taskName) {
          try {
            const key = (taskName || '').trim();
            if (!key || !this.calendar || !this.calendar.taskMapByName) return null;
            return this.calendar.taskMapByName[key] || null;
          } catch (e) { return null; }
        },
        // 将任务列表项适配为获取海报所需的 episode-like 对象
        getTasklistPosterLikeEpisode(task) {
          try {
            const calTask = this.getCalendarTaskByName(task && task.taskname);
            // 复用管理视图的取海报逻辑（优先匹配海报）
            if (calTask) return this.getTaskPosterLikeEpisode(calTask);
            // 兜底：仅返回空对象，getEpisodePosterUrl 内部会处理默认图
            return { poster_local_path: '' };
          } catch (e) { return { poster_local_path: '' }; }
        },
        // 任务列表排序：获取显示名称
        getTasklistSortDisplayName(key) {
          if (key === 'name') return '任务名称';
          if (key === 'progress') return '任务进度';
          if (key === 'update_time') return '更新时间';
          return '任务编号';
        },
        // 任务列表排序：保存到localStorage
        saveTasklistSort() {
          try {
            localStorage.setItem('tasklist_sort_options', JSON.stringify({ by: this.tasklistSort.by, order: this.tasklistSort.order }));
          } catch (e) {}
        },
        // 任务列表排序：切换排序字段
        // 仅切换排序字段，不改变当前升降序
        changeTasklistSortBy(by) {
          if (!['index','name','progress','update_time'].includes(by)) return;
          if (this.tasklistSort.by !== by) {
            this.tasklistSort.by = by;
            // 不重置、不切换 order，保持现状
            this.saveTasklistSort();
            // 不再调用 applyTasklistSort，因为现在使用计算属性
          }
        },
        // 任务列表排序：切换升降序
        toggleTasklistSortOrder() {
          this.tasklistSort.order = this.tasklistSort.order === 'asc' ? 'desc' : 'asc';
          this.saveTasklistSort();
          // 不再调用 applyTasklistSort，因为现在使用计算属性
        },
        // 获取排序后的任务列表（不修改原始数据，避免触发 configModified）
        getSortedTasklist() {
          try {
            const { by, order } = this.tasklistSort || { by: 'index', order: 'asc' };
            const factor = order === 'desc' ? -1 : 1;
            // 建立稳定排序：附带原始索引，避免相等时抖动
            const withIndex = (this.formData.tasklist || []).map((t, idx) => ({ t, idx }));
            withIndex.sort((a, b) => {
              let cmp = 0;
              if (by === 'name') {
                const an = (a.t.taskname || '').toString();
                const bn = (b.t.taskname || '').toString();
                try {
                  const ak = pinyinPro.pinyin(an, { toneType: 'none', type: 'string' }).toLowerCase();
                  const bk = pinyinPro.pinyin(bn, { toneType: 'none', type: 'string' }).toLowerCase();
                  if (ak < bk) cmp = -1; else if (ak > bk) cmp = 1; else cmp = 0;
                } catch (e) { cmp = an.localeCompare(bn); }
              } else if (by === 'progress') {
                const ap = (this.getTaskProgress && this.getTaskProgress(a.t.taskname) != null) ? Number(this.getTaskProgress(a.t.taskname)) : -1;
                const bp = (this.getTaskProgress && this.getTaskProgress(b.t.taskname) != null) ? Number(this.getTaskProgress(b.t.taskname)) : -1;
                // 1) 主排序：任务进度（支持升降序）
                const pDiff = ap - bp;
                if (pDiff !== 0) {
                  cmp = pDiff;
                } else {
                  // 2) 次排序：节目状态优先级（与主排序方向一致，通过最终 factor 生效）
                  const getStatusPriority = (task) => {
                    try {
                      // 任务对象可能为任务配置，需要先从日历任务映射中取状态
                      const calTask = this.getCalendarTaskByName(task && (task.taskname || task.task_name));
                      const status = this.getTaskShowStatus(calTask || task) || '';
                      if (status === '本季终') return 1;
                      if (status === '已取消') return 2;
                      if (status === '已完结') return 3;
                      return 0; // 播出中/无状态
                    } catch (e) { return 0; }
                  };
                  const aStatus = getStatusPriority(a.t);
                  const bStatus = getStatusPriority(b.t);
                  const sDiff = aStatus - bStatus;
                  if (sDiff !== 0) {
                    cmp = sDiff; // 顺序：播出中/无 < 本季终 < 已取消 < 已完结（方向由最终 factor 决定）
                  } else {
                    // 3) 三次排序：节目的播出进度（已播出/总集数），与主排序同方向
                    const getBroadcastPct = (task) => {
                      try {
                        const calTask = this.getCalendarTaskByName(task && (task.taskname || task.task_name));
                        const aired = this.getTaskAiredCount(calTask || {});
                        const total = this.getTaskTotalCount(calTask || {});
                        if (!total || total <= 0) return 0;
                        return aired / total;
                      } catch (e) { return 0; }
                    };
                    const aPct = getBroadcastPct(a.t);
                    const bPct = getBroadcastPct(b.t);
                    const pctDiff = aPct - bPct;
                    if (pctDiff !== 0) {
                      cmp = pctDiff;
                    } else {
                      // 3.1) 百分比也相同：按节目总集数排序（与主排序同方向）
                      const aTotal = this.getTaskTotalCount(this.getCalendarTaskByName(a.t && (a.t.taskname || a.t.task_name)) || {});
                      const bTotal = this.getTaskTotalCount(this.getCalendarTaskByName(b.t && (b.t.taskname || b.t.task_name)) || {});
                      const totalDiff = aTotal - bTotal;
                      if (totalDiff !== 0) {
                        cmp = totalDiff;
                      } else {
                        // 4) 末级排序：任务编号（#XX），与主排序同方向
                        const aNum = parseInt((a.t.taskname || '').match(/^#?(\d+)/)?.[1] || '0');
                        const bNum = parseInt((b.t.taskname || '').match(/^#?(\d+)/)?.[1] || '0');
                        cmp = aNum - bNum;
                      }
                    }
                  }
                }
              } else if (by === 'update_time') {
                // 按任务最近转存时间排序
                const aRecord = this.taskLatestRecords[a.t.taskname];
                const bRecord = this.taskLatestRecords[b.t.taskname];
                const aTime = aRecord && aRecord.full ? new Date(aRecord.full).getTime() : 0;
                const bTime = bRecord && bRecord.full ? new Date(bRecord.full).getTime() : 0;
                cmp = aTime - bTime;
              } else {
                // 按任务名称中的编号排序（解析 #XX 格式）
                const aNum = parseInt((a.t.taskname || '').match(/^#?(\d+)/)?.[1] || '0');
                const bNum = parseInt((b.t.taskname || '').match(/^#?(\d+)/)?.[1] || '0');
                cmp = aNum - bNum;
              }
              if (cmp === 0) cmp = a.idx - b.idx; // 稳定
              return cmp * factor;
            });
            // 返回带原始索引的任务对象，确保直接引用原始数据
            return withIndex.map(x => {
              const task = x.t;
              task.__originalIndex = x.idx;
              return task;
            });
          } catch (e) {
            return (this.formData.tasklist || []).map((t, idx) => {
              t.__originalIndex = idx;
              return t;
            });
          }
        },
        // 选择日历日期
        selectCalendarDate(day) {
          try {
            if (!day || !day.date) return;
            this.calendar.selectedDate = day.date;
          } catch (e) {}
        },
        // 根据日期字符串获取当日剧集数组
        getEpisodesByDate(dateStr) {
          if (!dateStr) return [];
          // 在 monthWeeks 结构里查找对应日期的 episodes
          for (const week of (this.calendar.monthWeeks || [])) {
            for (const d of (week.days || [])) {
              if (d.date === dateStr) {
                return Array.isArray(d.episodes) ? d.episodes : [];
              }
            }
          }
          return [];
        },
        // --- 显示设置拖拽排序 ---
        onDisplayDragStart(e, key) {
          try {
            e.dataTransfer.setData('text/plain', key);
            // 为原位置元素添加半透明视觉，表示“已被拖起”
            const origin = e.target.closest('.draggable-item');
            if (origin) origin.classList.add('drag-origin');
          } catch (err) {}
        },
        onDisplayDrop(e, targetKey) {
          try {
            const sourceKey = (e.dataTransfer && e.dataTransfer.getData('text/plain')) || '';
            if (!sourceKey || sourceKey === targetKey) return;
            const order = (this.formData && this.formData.button_display_order) ? this.formData.button_display_order.slice() : [];
            const from = order.indexOf(sourceKey);
            const to = order.indexOf(targetKey);
            if (from === -1 || to === -1) return;
            order.splice(from, 1);
            order.splice(to, 0, sourceKey);
            this.formData.button_display_order = order;
          } catch (err) {}
        },
        onDisplayDragOver(e) {
          try {
            // 使用“move”效果，避免浏览器默认的“copy/+”指示
            e.dataTransfer.dropEffect = 'move';
          } catch (err) {}
        },
        onDisplayDragEnd(e) {
          try {
            // 拖拽结束时移除所有占位/高亮样式
            const items = document.querySelectorAll('#display-setting-draggable .draggable-item');
            items.forEach(el => el.classList.remove('drag-origin', 'drag-over'));
          } catch (err) {}
        },
        getDisplayLabel(key) {
          const map = {
            refresh_plex: '刷新 Plex 媒体库',
            refresh_alist: '刷新 AList 目录',
            run_task: '运行此任务',
            delete_task: '删除此任务',
            latest_transfer_file: '最近转存文件',
            season_counts: '集数信息统计',
            latest_update_date: '最近更新日期',
            task_progress: '当前任务进度',
            show_status: '电视节目状态',
            today_update_indicator: '当日更新标识'
          };
          return map[key] || key;
        },
        // 显示设置悬停说明
        getDisplayHelp(key) {
          const help = {
            // 按钮类
            refresh_plex: '手动刷新Plex媒体库按钮，仅在正确配置Plex插件时显示并生效',
            refresh_alist: '手动刷新AList目录按钮，仅在正确配置AList插件时显示并生效',
            run_task: '手动运行任务按钮，仅在任务资源有效时显示并生效',
            delete_task: '删除任务按钮，操作不可撤回，请谨慎使用',

            // 信息类
            latest_transfer_file: '任务最近一次成功转存的文件名（剧集编号），需该任务有过成功转存记录才会显示',
            season_counts: '已转存集数/已播出集数/节目总集数，依赖TMDB匹配的元数据，仅在配置了TMDB API且任务成功匹配到条目时显示',
            latest_update_date: '任务最近一次成功转存的日期，需该任务有过成功转存记录才会显示',
            task_progress: '已转存集数占已播出集数的百分比，依赖TMDB匹配的元数据，仅在配置了TMDB API且任务成功匹配到条目时显示',
            show_status: '电视节目的完播状态（如：本季终/已完结/已取消），依赖TMDB匹配的元数据，仅在配置了TMDB API、任务成功匹配到条目且节目已完播时显示',
            today_update_indicator: '任务在当日产生转存记录后，将显示当日更新标识'
          };
          return help[key] || '设置该项目在任务列表中的显示行为：始终显示/悬停显示/禁用，个别项目受插件或TMDB匹配状态影响是否生效或展示';
        },
        // ----- 任务列表新增显示：集数统计/任务进度/节目状态 -----
        getTaskSeasonCounts(taskName) {
          try {
            if (!taskName || !this.calendar || !Array.isArray(this.calendar.tasks)) return null;
            const t = this.calendar.tasks.find(x => (x.task_name || x.taskname) === taskName);
            if (!t || !t.season_counts) return null;
            const sc = t.season_counts || {};
            const transferred = Number(sc.transferred_count || 0);
            const aired = Number(sc.aired_count || 0);
            const total = Number(sc.total_count || 0);
            if (transferred === 0 && aired === 0 && total === 0) return null;
            return { transferred, aired, total };
          } catch (e) { return null; }
        },
        formatSeasonCounts(sc) {
          try {
            if (!sc) return '';
            // 为斜杠包裹span，便于单独微调位置
            return `${sc.transferred} <span class="count-slash">/</span> ${sc.aired} <span class=\"count-slash\">/</span> ${sc.total}`;
          } catch (e) { return ''; }
        },
        getTaskProgress(taskName) {
          try {
            const sc = this.getTaskSeasonCounts(taskName);
            if (!sc) return null;
            if (sc.aired <= 0) return 0;
            const pct = Math.floor((sc.transferred / sc.aired) * 100);
            return Math.max(0, Math.min(100, pct));
          } catch (e) { return null; }
        },
        getTaskShowStatus(taskNameOrTask) {
          try {
            // 如果传入的是任务对象，直接使用其状态
            if (taskNameOrTask && typeof taskNameOrTask === 'object') {
              const s = (taskNameOrTask.matched_status ? String(taskNameOrTask.matched_status) : '').trim();
              if (!s) return '';
              // 仅在这些状态展示
              if (['本季终', '已完结', '已取消'].includes(s)) return s;
              return '';
            }
            
            // 如果传入的是任务名字符串，从任务列表中查找
            if (!taskNameOrTask || !this.calendar || !Array.isArray(this.calendar.tasks)) return '';
            const t = this.calendar.tasks.find(x => (x.task_name || x.taskname) === taskNameOrTask);
            const s = (t && t.matched_status ? String(t.matched_status) : '').trim();
            if (!s) return '';
            // 仅在这些状态展示
            if (['本季终', '已完结', '已取消'].includes(s)) return s;
            return '';
          } catch (e) { return ''; }
        },
        // 获取任务节目状态的悬停提示
        getTaskShowStatusTooltip(task) {
          try {
            if (!task) return '';
            const taskName = task.task_name || task.taskname || '';
            const status = this.getTaskShowStatus(task);
            if (status) {
              return `${taskName} · ${status}`;
            }
            return taskName;
          } catch (e) { 
            return task.task_name || task.taskname || '';
          }
        },
        seasonInputComputedWidth() {
          try {
            const val = String(this.editMetadata && this.editMetadata.form ? (this.editMetadata.form.season_number ?? '') : '');
            const len = val.length || 1;
            const px = Math.max(31, len * 9 + 12);
            return px + 'px';
          } catch (e) {
            return '31px';
          }
        },
        // 获取管理视图卡片展示用：实时“已转存集数”（优先使用进度映射，其次回退到任务自带的 season_counts）
        getTaskTransferredCount(task) {
          try {
            if (!task) return 0;
            return Number((task && task.season_counts && task.season_counts.transferred_count) || 0);
          } catch (e) { return 0; }
        },
        // 获取管理视图卡片展示用：已播出集数（直接来自任务元数据）
        getTaskAiredCount(task) {
          try { return Number((task && task.season_counts && task.season_counts.aired_count) || 0); } catch (e) { return 0; }
        },
        // 获取管理视图卡片展示用：总集数（直接来自任务元数据）
        getTaskTotalCount(task) {
          try { return Number((task && task.season_counts && task.season_counts.total_count) || 0); } catch (e) { return 0; }
        },
        // —— 任务列表：类型筛选 ——
        selectTasklistType(type) {
          this.tasklist.selectedType = type;
          try { localStorage.setItem('tasklist_selected_type', type); } catch (e) {}
        },
        tasklistFilterByType(task) {
          try {
            if (!task) return true;
            if (!this.tasklist || !this.tasklist.selectedType || this.tasklist.selectedType === 'all') return true;
            const name = task.taskname || task.task_name || '';
            let contentType = '';
            
            // 优先从任务配置中读取内容类型
            if (task.content_type) {
              contentType = task.content_type;
            } else if (task.calendar_info && task.calendar_info.extracted && task.calendar_info.extracted.content_type) {
              contentType = task.calendar_info.extracted.content_type;
            } else {
              // 如果任务配置中没有，则从 calendar.tasks 中查找
              const t = (this.calendar.tasks || []).find(x => (x.task_name || x.taskname) === name);
              contentType = (t && t.content_type) || 'other';
            }
            
            return contentType === this.tasklist.selectedType;
          } catch (e) { return true; }
        },
        // 获取根据当前视图筛选条件过滤后的任务中编号最大的任务
        getLastTaskByCurrentFilter() {
          try {
            if (!this.formData.tasklist || this.formData.tasklist.length === 0) {
              return null;
            }
            
            // 如果当前视图是"全部"，返回所有任务中编号最大的任务
            if (!this.tasklist || !this.tasklist.selectedType || this.tasklist.selectedType === 'all') {
              return this.formData.tasklist[this.formData.tasklist.length - 1];
            }
            
            // 根据当前筛选条件过滤任务
            const filteredTasks = this.formData.tasklist.filter(task => {
              return this.tasklistFilterByType(task);
            });
            
            // 如果没有匹配的任务，返回所有任务中编号最大的任务
            if (filteredTasks.length === 0) {
              return this.formData.tasklist[this.formData.tasklist.length - 1];
            }
            
            // 返回过滤后任务中编号最大的任务（最后一个）
            return filteredTasks[filteredTasks.length - 1];
          } catch (e) {
            // 出错时返回所有任务中编号最大的任务
            return this.formData.tasklist && this.formData.tasklist.length > 0 
              ? this.formData.tasklist[this.formData.tasklist.length - 1] 
              : null;
          }
        },
        // 计算转存进度（已转存/已播出 的百分比，取整），优先使用实时映射
        getTransferProgress(task) {
          try {
            if (!task || !task.season_counts) return 0;
            const transferred = this.getTaskTransferredCount(task);
            const aired = this.getTaskAiredCount(task);
            if (aired <= 0) return 0;
            const pct = Math.floor((transferred / aired) * 100);
            return Math.max(0, Math.min(100, pct));
          } catch (e) { return 0; }
        },
        // 根据节目状态返回徽标颜色类（使用状态名便于直接在CSS中调整颜色）
        getProgressBadgeClass(task) {
          try {
            const status = (task && task.matched_status ? String(task.matched_status) : '').trim();
            if (!status) return '';
            if (status === '本季终') return 'status-finale';
            if (status === '已完结') return 'status-ended';
            // 其他状态：统一用绿色
            return 'status-other';
          } catch (e) {
            return '';
          }
        },
        // 仅当有有效信息时返回悬停提示，否则返回null以不显示
        getSuggestionHoverTitle(suggestion) {
          if (!suggestion) return null;
          let content = (suggestion.content || '').trim();
          if (!content) return null;
          // 统一标点为英文冒号，统一逗号
          const normalized = content
            .replace(/：/g, ':')
            .replace(/，/g, ',')
            .replace(/\s+/g, ' ')
            .trim();
          // 仅在明确的占位文本时隐藏：
          // 1) 全文就是“大小:-”
          if (/^大小\s*:\s*-$/i.test(normalized)) return null;
          // 2) 完全匹配“类别:xx, 文件类型:yy, 大小:-”这类占位
          if (/^类别\s*:[^,]*,\s*文件类型\s*:[^,]*,\s*大小\s*:\s*-$/i.test(normalized)) return null;
          return content;
        },
        // 获取插件展示名称（支持别名，仅用于WebUI显示）
        getPluginDisplayName(pluginName) {
          return this.pluginDisplayAliases[pluginName] || pluginName;
        },
        // 设置移动端任务列表展开/收起状态监听
        setupMobileTaskListToggle() {
          // 监听所有collapse事件
          $(document).on('show.bs.collapse', '[id^="collapse_"]', (e) => {
            const collapseId = e.target.id;
            const taskIndex = collapseId.replace('collapse_', '');
            const taskElement = $(e.target).closest('.task');
            if (taskElement.length) {
              taskElement.addClass('task-expanded');
            }
          });

          $(document).on('hide.bs.collapse', '[id^="collapse_"]', (e) => {
            const collapseId = e.target.id;
            const taskIndex = collapseId.replace('collapse_', '');
            const taskElement = $(e.target).closest('.task');
            if (taskElement.length) {
              taskElement.removeClass('task-expanded');
            }
          });
        },
        // 获取文件图标类名
        getFileIconClass(fileName, isDir = false) {
          return getFileIconClass(fileName, isDir);
        },
        // 拼音排序辅助函数
        sortTaskNamesByPinyin(taskNames) {
          return taskNames.sort((a, b) => {
            const aKey = pinyinPro.pinyin(a, { toneType: 'none', type: 'string' }).toLowerCase();
            const bKey = pinyinPro.pinyin(b, { toneType: 'none', type: 'string' }).toLowerCase();
            return aKey > bKey ? 1 : -1;
          });
        },
        // 添加格式化分享链接警告信息的方法
        formatShareUrlBanMessage(message) {
          if (!message) return message;
          
          // 检查是否为可恢复的网络错误或服务端临时错误
          if (message.includes("inner error") || 
              message.includes("request error") ||
              message.includes("网络错误") ||
              message.includes("服务端错误") ||
              message.includes("临时错误")) {
            // 对于可恢复错误，返回null表示不应该设置shareurl_ban
            return null;
          }
          
          if (message.includes("分享者用户封禁链接查看受限") || 
              message.includes("文件涉及违规内容") ||
              message.includes("分享地址已失效")) {
            return "该分享已失效，不可访问";
          } else if (message.includes("好友已取消了分享")) {
            return "该分享已被取消，无法访问";
          } else if (message.includes("文件已被分享者删除") || message === "文件已被分享者删除或文件夹为空") {
            return "该分享已被删除，无法访问";
          }
          
          return message;
        },

        // 追剧日历相关方法
        // 加载追剧日历数据
        async loadCalendarData() {
          if (this.calendar.hasLoaded) return;
          
          try {
            console.log('开始加载追剧日历数据...');
            
            // 直接加载任务信息（本地数据，无需缓存）
            const tasksResponse = await axios.get('/api/calendar/tasks');
            console.log('任务信息响应:', tasksResponse.data);
            
            if (tasksResponse.data.success) {
              this.calendar.tasks = tasksResponse.data.data.tasks;
              // 规范化并排序内容类型：tv、anime、variety、documentary、other（其他始终最后）
              const rawTypes = tasksResponse.data.data.content_types || [];
              this.updateContentTypes(rawTypes);
              this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);

              // 从任务列表页面接口读取最近转存文件，构建进度映射
              try {
                const latestRes = await axios.get('/task_latest_info');
                if (latestRes.data && latestRes.data.success) {
                  const latestFiles = latestRes.data.data.latest_files || {};
                  // 同步到任务面板缓存，避免重复请求
                  this.taskLatestFiles = latestFiles;
                  // 构建映射
                  this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(latestFiles);
                  this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks, this.calendar.progressByTaskName);
                } else {
                  this.calendar.progressByTaskName = {};
                  this.calendar.progressByShowName = {};
                }
              } catch (e) {
                console.warn('读取任务最近转存文件失败:', e);
                this.calendar.progressByTaskName = {};
                this.calendar.progressByShowName = {};
              }

              // 读取持久化的内容类型选择并校验有效性
              try {
                const savedType = localStorage.getItem('calendar_selected_type') || 'all';
                const validTypes = ['all', ...this.calendar.contentTypes];
                if (validTypes.includes(savedType)) {
                  this.calendar.selectedType = savedType;
                } else {
                  this.calendar.selectedType = 'all';
                }
              } catch (e) {
                this.calendar.selectedType = 'all';
              }
              // 读取管理模式持久化（支持三态记忆：管理/海报/月历）
              try {
                const savedManage = localStorage.getItem('calendar_manage_mode');
                if (savedManage === 'true') {
                  this.calendar.manageMode = true;
                } else if (savedManage === 'false') {
                  this.calendar.manageMode = false;
                }
              } catch (e) {}
              
              console.log('任务数据:', this.calendar.tasks);
              console.log('内容类型:', this.calendar.contentTypes);
              
              // 首屏优先渲染本地缓存，保证页面快速可见
              await this.loadCalendarEpisodesLocal();
              // 同步加载今日更新数据
              try { await this.loadTodayUpdatesLocal(); } catch (e) {}
              // 背景增量刷新（带节流），完成后热更新
              if (this.shouldAutoRefreshCalendar()) {
                this.refreshCalendarData().catch(() => {});
              }
              // 启动后台轻量监听，近实时发现“最近转存文件”变化
              this.startCalendarAutoWatch();
              
              this.calendar.hasLoaded = true;
              
              // 初始化日期数据
              this.initializeCalendarDates();
            } else {
              this.calendar.error = tasksResponse.data.message;
              console.error('加载任务信息失败:', tasksResponse.data.message);
            }
          } catch (error) {
            this.calendar.error = '加载追剧日历数据失败';
            console.error('加载追剧日历数据失败:', error);
          }
        },

        // 切换内容管理模式
        toggleCalendarManageMode() {
          this.calendar.manageMode = !this.calendar.manageMode;
          try {
            localStorage.setItem('calendar_manage_mode', this.calendar.manageMode ? 'true' : 'false');
          } catch (e) {}
          // 进入或退出管理模式时触发布局重算
          this.calendar.layoutTick = Date.now();
        },

        // 将任务转为与海报视图一致的episode-like对象：优先使用后端返回的匹配海报
        getTaskPosterLikeEpisode(task) {
          const show = (task && task.show_name) ? String(task.show_name).trim() : '';
          // 1) 后端提供的 matched_poster_local_path（真实匹配结果）
          let poster = (task && task.matched_poster_local_path) || '';
          // 2) 若无，则根据 show_name 在已加载episodes中兜底
          if (!poster && show && this.posterByShowName && this.posterByShowName[show]) {
            poster = this.posterByShowName[show];
          }
          // 3) 仍无则使用任务自身字段或默认逻辑
          if (!poster) {
            poster = (task && task.poster_local_path) || '';
          }
          return {
            poster_local_path: poster,
            task_info: { task_name: task && task.task_name, content_type: task && task.content_type },
            show_name: task && task.show_name
          };
        },

        // 内容类型中文名
        getContentTypeCN(type) {
          const map = { tv: '剧集', anime: '动画', variety: '综艺', documentary: '纪录片', other: '其他' };
          return map[type] || '其他';
        },

        // 基于最近转存文件构建：task_name -> { episode_number, air_date }
        buildProgressByTaskNameFromLatestFiles(latestFiles) {
          const result = {};
          if (!latestFiles || typeof latestFiles !== 'object') return result;
          const patterns = [
            /S(\d{1,2})E(\d{1,3})/i, // S01E01
            /E(\d{1,3})/i,            // E01
            /第(\d{1,3})集/,           // 第1集
            /第(\d{1,3})期/,           // 第1期（综艺）
            /(\d{1,3})集/,            // 1集
            /(\d{1,3})期/,            // 1期
          ];
          const datePatterns = [
            /(\d{4})-(\d{1,2})-(\d{1,2})/,   // 2025-01-01
            /(\d{4})\/(\d{1,2})\/(\d{1,2})/,
            /(\d{4})\.(\d{1,2})\.(\d{1,2})/,
          ];
          const parseOne = (txt) => {
            if (!txt) return null;
            
            // 特殊处理：检测"日期 连接符 第x期"格式，优先使用日期
            // 支持各种连接符号：空格、-、_、.、/等
            const dateEpisodePatterns = [
              /(\d{4})-(\d{1,2})-(\d{1,2})\s*[-\s_\.\/]\s*第(\d{1,3})期/,  // 2025-09-08 - 第128期, 2025-09-08 第128期, 2025-09-08_第128期, 2025-09-08.第128期
              /(\d{4})\/(\d{1,2})\/(\d{1,2})\s*[-\s_\.\/]\s*第(\d{1,3})期/,   // 2025/09/08 - 第128期, 2025/09/08 第128期, 2025/09/08_第128期, 2025/09/08.第128期
              /(\d{4})\.(\d{1,2})\.(\d{1,2})\s*[-\s_\.\/]\s*第(\d{1,3})期/,  // 2025.09.08 - 第128期, 2025.09.08 第128期, 2025.09.08_第128期, 2025.09.08.第128期
            ];
            
            for (const pattern of dateEpisodePatterns) {
              const match = String(txt).match(pattern);
              if (match) {
                const y = match[1];
                const mm = String(match[2]).padStart(2, '0');
                const dd = String(match[3]).padStart(2, '0');
                return { episode_number: null, air_date: `${y}-${mm}-${dd}` };
              }
            }
            
            // 原有的逻辑：先尝试提取集数信息
            for (const re of patterns) {
              const m = String(txt).match(re);
              if (m) {
                if (/S\d+E\d+/i.test(m[0])) {
                  return { episode_number: parseInt(m[2]), air_date: null };
                }
                return { episode_number: parseInt(m[1]), air_date: null };
              }
            }
            
            // 最后尝试提取日期信息
            for (const re of datePatterns) {
              const m = String(txt).match(re);
              if (m) {
                const y = m[1];
                const mm = String(m[2]).padStart(2, '0');
                const dd = String(m[3]).padStart(2, '0');
                return { episode_number: null, air_date: `${y}-${mm}-${dd}` };
              }
            }
            return null;
          };
          Object.keys(latestFiles || {}).forEach(taskName => {
            if (taskName && taskName !== '__calendar_shows__' && taskName !== '__calendar_tmdb_ids__') {
              const parsed = parseOne(latestFiles[taskName]);
              if (parsed && (parsed.episode_number != null || parsed.air_date)) {
                result[taskName] = parsed;
              }
            }
          });
          return result;
        },

        // 基于任务列表信息构建：show_name -> 任务进度（同一剧取更大集号或更晚日期）
        buildProgressByShowNameFromTasks(tasks, progressByTaskName) {
          const result = {};
          if (!Array.isArray(tasks)) return result;
          const pickNewer = (a, b) => {
            if (!a) return b; if (!b) return a;
            if (a.episode_number != null && b.episode_number != null) {
              return b.episode_number > a.episode_number ? b : a;
            }
            if (a.episode_number != null) return a;
            if (b.episode_number != null) return b;
            if ((b.air_date || '') > (a.air_date || '')) return b; return a;
          };
          tasks.forEach(t => {
            const tname = t.task_name;
            const sname = t.show_name;
            const prog = progressByTaskName[tname];
            if (sname && prog) {
              result[sname] = pickNewer(result[sname], prog);
            }
          });
          return result;
        },
        
        // 加载本地剧集数据；如无则自动 bootstrap + refresh 再读
        async loadCalendarEpisodesLocal() {
          const tryReadLocal = async () => {
            const res = await axios.get('/api/calendar/episodes_local');
            if (res.data && res.data.success) {
              this.calendar.episodes = res.data.data.episodes || [];
              return this.calendar.episodes.length;
            }
            return 0;
          };

          let count = await tryReadLocal();
          if (count > 0) return;

          try {
            await axios.post('/api/calendar/bootstrap');
          } catch (e) {
            console.warn('bootstrap 失败:', e);
          }

          const tasklist = (this.formData && this.formData.tasklist) ? this.formData.tasklist : [];
          const tmdbIds = [];
          tasklist.forEach(t => {
            const cal = (t && t.calendar_info) ? t.calendar_info : {};
            const match = cal.match || {};
            if (match.tmdb_id) tmdbIds.push(match.tmdb_id);
          });

          for (const id of tmdbIds) {
            try {
              await axios.get('/api/calendar/refresh_latest_season', { params: { tmdb_id: id } });
            } catch (e) {
              console.warn('refresh 失败:', id, e);
            }
          }

          await tryReadLocal();
        },
        
        // 初始化日历日期数据
        initializeCalendarDates() {
          this.updateWeekDates();
          this.updateMonthWeeks();
        },
        
        // 更新周视图日期
        updateWeekDates() {
          const today = new Date();
          const currentDay = new Date(this.calendar.currentDate);
          
          // 动态计算显示的列数，根据主内容区域宽度自动调整
          const availableWidth = this.getCalendarAvailableWidth();
          const minColumnWidth = 140; // 最小列宽
          const columnGap = 20; // 列间距
          // 计算列数：n = floor((W + gap - eps) / (minWidth + gap))，避免边界像素/滚动条导致的多列
          const eps = 0.1;
          const maxColumns = Math.max(2, Math.floor((availableWidth + columnGap - eps) / (minColumnWidth + columnGap)));
          

          
          // 从当前日期开始，向后扩展日期范围（今天始终在第一列）
          const startDate = new Date(currentDay);
          
          this.calendar.weekDates = [];
          for (let i = 0; i < maxColumns; i++) {
            const date = new Date(startDate);
            date.setDate(startDate.getDate() + i);
            
            const isToday = date.toDateString() === today.toDateString();
            const dayOfWeek = date.getDay();
            const weekdayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
            const weekday = weekdayNames[dayOfWeek];
            const day = date.getDate().toString().padStart(2, '0');
            
            this.calendar.weekDates.push({
              date: this.formatDateToYYYYMMDD(date),
              weekday: isToday ? '今天' : weekday,
              day: `${(date.getMonth() + 1).toString().padStart(2, '0')}/${day}`,
              isToday: isToday
            });
          }
        },
        
        // 更新月视图周数据
        updateMonthWeeks() {
          const currentDay = new Date(this.calendar.currentDate);
          const year = currentDay.getFullYear();
          const month = currentDay.getMonth();
          
          const firstDay = new Date(year, month, 1);
          const lastDay = new Date(year, month + 1, 0);
          const firstDayOfWeek = firstDay.getDay() || 7; // 转换为周一到周日（1-7）
          
          const weeks = [];
          let currentWeek = [];
          
          // 添加上个月的日期
          for (let i = firstDayOfWeek - 1; i > 0; i--) {
            const date = new Date(year, month, 1 - i);
            currentWeek.push({
              date: this.formatDateToYYYYMMDD(date),
              dayNumber: date.getDate(),
              isCurrentMonth: false,
              isToday: false,
              episodes: []
            });
          }
          
          // 添加当前月的日期
          for (let day = 1; day <= lastDay.getDate(); day++) {
            const date = new Date(year, month, day);
            const isToday = date.toDateString() === new Date().toDateString();
            
            currentWeek.push({
              date: this.formatDateToYYYYMMDD(date),
              dayNumber: day,
              isCurrentMonth: true,
              isToday: isToday,
              episodes: this.getEpisodesByDate(this.formatDateToYYYYMMDD(date))
            });
            
            if (currentWeek.length === 7) {
              weeks.push({
                weekIndex: weeks.length,
                days: currentWeek
              });
              currentWeek = [];
            }
          }
          
          // 添加下个月的日期
          if (currentWeek.length > 0) {
            for (let day = 1; currentWeek.length < 7; day++) {
              const date = new Date(year, month + 1, day);
                          currentWeek.push({
              date: this.formatDateToYYYYMMDD(date),
              dayNumber: date.getDate(),
              isCurrentMonth: false,
              isToday: false,
              episodes: []
            });
            }
            weeks.push({
              weekIndex: weeks.length,
              days: currentWeek
            });
          }
          
          this.calendar.monthWeeks = weeks;
        },
        
        // 根据日期获取剧集
        getEpisodesByDate(date) {
          // 从已加载的剧集中筛选指定日期的剧集
          if (!this.calendar.episodes || this.calendar.episodes.length === 0) {
            return [];
          }
          
          // 先进行基础筛选
          let filteredEpisodes = this.calendar.episodes.filter(episode => {
            // 根据筛选条件过滤
            if (this.calendar.selectedType !== 'all') {
              // 通过剧集名称匹配任务，获取内容类型
              const matchedTask = this.findTaskByShowName(episode.show_name);
              const episodeContentType = matchedTask ? matchedTask.content_type : 'other';
              if (episodeContentType !== this.calendar.selectedType) {
                return false;
              }
            }
            
            // 名称筛选：检查剧集名称
            if (this.calendar.nameFilter && this.calendar.nameFilter.trim() !== '') {
              const nameFilter = this.calendar.nameFilter.toLowerCase();
              const showName = (episode.show_name || '').toLowerCase();
              if (!showName.includes(nameFilter)) {
                return false;
              }
            }
            
            // 任务筛选：检查任务名称
            if (this.calendar.taskFilter && this.calendar.taskFilter.trim() !== '') {
              const matchedTask = this.findTaskByShowName(episode.show_name);
              const taskName = matchedTask ? matchedTask.task_name : '';
              if (taskName !== this.calendar.taskFilter) {
                return false;
              }
            }
            
            return episode.air_date === date;
          });
          
          // 如果启用了合并集功能，则进行合并处理
          let result = this.calendar.mergeEpisodes
            ? this.mergeEpisodesByShow(filteredEpisodes)
            : filteredEpisodes;

          // 统一对同一天节目按节目名称拼音排序（与内容管理一致）
          try {
            result = result.slice().sort((a, b) => {
              const an = (a && a.show_name) ? String(a.show_name) : '';
              const bn = (b && b.show_name) ? String(b.show_name) : '';
              const ak = pinyinPro.pinyin(an, { toneType: 'none', type: 'string' }).toLowerCase();
              const bk = pinyinPro.pinyin(bn, { toneType: 'none', type: 'string' }).toLowerCase();
              if (ak === bk) return 0;
              return ak > bk ? 1 : -1;
            });
          } catch (e) {}

          return result;
        },
        
        // 根据剧集名称查找对应的任务
        findTaskByShowName(showName) {
          if (!this.calendar.tasks || !showName) return null;
          
          // 首先尝试精确匹配
          let matchedTask = this.calendar.tasks.find(task => 
            task.show_name === showName || 
            task.matched_show_name === showName
          );
          
          if (matchedTask) return matchedTask;
          
          // 如果精确匹配失败，尝试模糊匹配
          matchedTask = this.calendar.tasks.find(task => 
            (task.show_name && task.show_name.includes(showName)) ||
            (task.matched_show_name && task.matched_show_name.includes(showName))
          );
          
          return matchedTask || null;
        },
        // 判断剧集是否为 finale 集（支持合并集）
        isFinaleEpisode(episode) {
          try {
            if (!episode) return false;
            // 合并集：检查任一原始集是否 finale
            if (episode.is_merged && Array.isArray(episode.original_episodes) && episode.original_episodes.length > 0) {
              return episode.original_episodes.some(ep => {
                const t = String((ep && (ep.type || ep.ep_type || ep.episode_type)) || '').toLowerCase();
                return t.includes('finale');
              });
            }
            // 单集：直接检查类型字段
            const tp = String((episode.type || episode.ep_type || episode.episode_type || '')).toLowerCase();
            return tp.includes('finale');
          } catch (e) {
            return false;
          }
        },
        // 获取需要在 finale 集显示的节目状态文案
        getEpisodeFinaleStatus(episode) {
          try {
            if (!this.isFinaleEpisode(episode)) return '';
            const task = this.findTaskByShowName(episode && episode.show_name);
            const status = task && task.matched_status ? String(task.matched_status).trim() : '';
            // 规范：若节目为“已完结/已取消”，则在 finale 集显示对应状态；否则显示“本季终”
            if (status === '已完结' || status === '已取消') return status;
            return '本季终';
          } catch (e) {
            return '';
          }
        },
        // 返回用于悬停提示的 文本：剧名 或 剧名 · 状态
        getEpisodeShowTitleWithStatus(episode) {
          try {
            const name = (episode && episode.show_name) ? String(episode.show_name).trim() : '';
            const status = this.getEpisodeFinaleStatus(episode);
            return status ? `${name} · ${status}` : name;
          } catch (e) {
            return (episode && episode.show_name) || '';
          }
        },
        
        // 合并同一天同一节目的多集
        mergeEpisodesByShow(episodes) {
          if (!episodes || episodes.length === 0) {
            return [];
          }
          
          // 按节目名称和任务信息分组
          const groupedEpisodes = {};
          
          episodes.forEach(episode => {
            // 验证episode对象的基本结构
            if (!episode || typeof episode !== 'object') {
              return;
            }
            
            // 创建唯一键：节目名称 + 任务名称 + 季数
            const showName = episode.show_name || 'unknown';
            const taskName = (episode.task_info && episode.task_info.task_name) ? episode.task_info.task_name : 'unknown';
            const seasonNumber = episode.season_number || 1;
            
            // 清理键值，防止特殊字符导致问题
            const cleanShowName = String(showName).replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_');
            const cleanTaskName = String(taskName).replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_');
            const showKey = `${cleanShowName}_${cleanTaskName}_${seasonNumber}`;
            
            if (!groupedEpisodes[showKey]) {
              groupedEpisodes[showKey] = [];
            }
            groupedEpisodes[showKey].push(episode);
          });
          
          // 对每个分组进行合并处理
          const mergedEpisodes = [];
          
          Object.values(groupedEpisodes).forEach(episodeGroup => {
            if (episodeGroup.length === 1) {
              // 只有一集，直接添加
              mergedEpisodes.push(episodeGroup[0]);
            } else {
              // 多集，进行合并
              const mergedEpisode = this.createMergedEpisode(episodeGroup);
              mergedEpisodes.push(mergedEpisode);
            }
          });
          
          return mergedEpisodes;
        },
        
        // 创建合并后的剧集对象
        createMergedEpisode(episodeGroup) {
          // 按集数排序
          episodeGroup.sort((a, b) => {
            const episodeA = parseInt(a.episode_number) || 0;
            const episodeB = parseInt(b.episode_number) || 0;
            return episodeA - episodeB;
          });
          
          // 使用最后一集的数据作为基础
          const baseEpisode = episodeGroup[episodeGroup.length - 1];
          const firstEpisode = episodeGroup[0];
          const lastEpisode = episodeGroup[episodeGroup.length - 1];
          
          // 创建合并后的剧集对象
          const mergedEpisode = {
            ...baseEpisode,
            // 合并集数显示
            episode_number: this.formatMergedEpisodeNumbers(episodeGroup),
            // 合并集数范围（用于显示）
            episode_range: {
              start: parseInt(firstEpisode.episode_number) || 0,
              end: parseInt(lastEpisode.episode_number) || 0,
              count: episodeGroup.length
            },
            // 标记为合并集
            is_merged: true,
            // 原始剧集列表（用于调试或其他用途）
            original_episodes: episodeGroup
          };
          
          return mergedEpisode;
        },
        
        // 格式化合并集数显示
        formatMergedEpisodeNumbers(episodeGroup) {
          if (!episodeGroup || episodeGroup.length === 0) {
            return '';
          }
          
          if (episodeGroup.length === 1) {
            return episodeGroup[0].episode_number || '';
          }
          
          const firstEpisode = parseInt(episodeGroup[0].episode_number) || 0;
          const lastEpisode = parseInt(episodeGroup[episodeGroup.length - 1].episode_number) || 0;
          const seasonNumber = parseInt(episodeGroup[0].season_number) || 1;
          
          // 确保集数和季数都是有效的数字
          if (isNaN(firstEpisode) || isNaN(lastEpisode) || isNaN(seasonNumber)) {
            return episodeGroup[0].episode_number || '';
          }
          
          if (firstEpisode === lastEpisode) {
            return `S${String(seasonNumber).padStart(2, '0')}E${String(firstEpisode).padStart(2, '0')}`;
          } else {
            return `S${String(seasonNumber).padStart(2, '0')}E${String(firstEpisode).padStart(2, '0')}-E${String(lastEpisode).padStart(2, '0')}`;
          }
        },
        
        // 获取内容类型显示名称
        getContentTypeDisplayName(type) {
          const typeNames = {
            'all': '全部',
            'tv': '剧集',
            'anime': '动画',
            'variety': '综艺',
            'documentary': '纪录片',
            'other': '其他'
          };
          return typeNames[type] || type;
        },
        
        // 缓存管理方法
        setCachedData(key, data) {
          try {
            localStorage.setItem(`quark_calendar_${key}`, JSON.stringify(data));
          } catch (error) {
            console.warn('缓存数据失败:', error);
          }
        },
        
        getCachedData(key) {
          try {
            const data = localStorage.getItem(`quark_calendar_${key}`);
            return data ? JSON.parse(data) : null;
          } catch (error) {
            console.warn('读取缓存数据失败:', error);
            return null;
          }
        },
        
        
        
        // 增量刷新追剧日历数据：仅对最新季执行增量拉取，然后本地读取
        async refreshCalendarData() {
          try {
            const tasklist = (this.formData && this.formData.tasklist) ? this.formData.tasklist : [];
            const tmdbIds = [];
            tasklist.forEach(t => {
              const cal = (t && t.calendar_info) ? t.calendar_info : {};
              const match = cal.match || {};
              if (match.tmdb_id) tmdbIds.push(match.tmdb_id);
            });
            for (const id of tmdbIds) {
              try {
                await axios.get('/api/calendar/refresh_latest_season', { params: { tmdb_id: id } });
              } catch (e) {
                console.warn('refresh 失败:', id, e);
              }
            }
            
            // 重新加载任务数据，确保内容管理页面能热更新
            try {
              const tasksResponse = await axios.get('/api/calendar/tasks');
              if (tasksResponse.data.success) {
                this.calendar.tasks = tasksResponse.data.data.tasks;
                this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                // 重新构建进度映射
                this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                
                // 重新计算内容类型，确保类型按钮能热更新
                const rawTypes = tasksResponse.data.data.content_types || [];
                this.updateContentTypes(rawTypes);
              }
            } catch (e) {}
            
            // 刷新内存数据，不清空缓存键，直接本地读取覆盖
            await this.loadCalendarEpisodesLocal();
            // 热更新当前视图（不打断用户操作）
            this.initializeCalendarDates();
            // 并行刷新今日更新数据，确保当日更新标识正常显示
            try { await this.loadTodayUpdatesLocal(); } catch (e) {}
            // 记录自动刷新时间戳，用于节流
            try { localStorage.setItem('calendar_last_auto_refresh', String(Date.now())); } catch (e) {}
          } catch (error) {
            console.warn('增量刷新失败:', error);
          }
        },
        
        // 刷新单集或合并集的元数据（海报视图使用）
        async refreshEpisodeMetadata(episode) {
          try {
            if (!episode || !episode.tmdb_id || !episode.season_number) {
              console.warn('刷新单集元数据失败：缺少必要参数', episode);
              return;
            }
            
            // 检查是否为合并集
            if (episode.is_merged && episode.original_episodes && episode.original_episodes.length > 0) {
              // 合并集：刷新所有原始集
              console.log('检测到合并集，将刷新所有原始集:', episode.original_episodes.length, '集');
              
              let successCount = 0;
              let failCount = 0;
              const successEpisodes = [];
              const failEpisodes = [];
              const errors = [];
              
              // 并行刷新所有原始集
              const refreshPromises = episode.original_episodes.map(async (originalEpisode) => {
                try {
                  const response = await axios.get('/api/calendar/refresh_episode', {
                    params: {
                      tmdb_id: episode.tmdb_id,
                      season_number: episode.season_number,
                      episode_number: originalEpisode.episode_number
                    }
                  });
                  
                  if (response.data.success) {
                    successCount++;
                    successEpisodes.push(originalEpisode.episode_number);
                    return { success: true, episode: originalEpisode.episode_number };
                  } else {
                    failCount++;
                    failEpisodes.push(originalEpisode.episode_number);
                    errors.push(`第${originalEpisode.episode_number}集: ${response.data.message}`);
                    return { success: false, episode: originalEpisode.episode_number, error: response.data.message };
                  }
                } catch (error) {
                  failCount++;
                  failEpisodes.push(originalEpisode.episode_number);
                  const errorMsg = error.response?.data?.message || error.message;
                  errors.push(`第${originalEpisode.episode_number}集: ${errorMsg}`);
                  return { success: false, episode: originalEpisode.episode_number, error: errorMsg };
                }
              });
              
              // 等待所有刷新完成
              await Promise.all(refreshPromises);
              
              // 显示结果提示
              const showName = episode.show_name || '未知剧集';
              const seasonNumber = episode.season_number;
              
              if (successCount > 0 && failCount === 0) {
                // 全部成功
                const episodeRange = this.formatEpisodeRange(successEpisodes);
                this.showToast(`《${showName}》第 ${seasonNumber} 季 · ${episodeRange}刷新成功`);
              } else if (successCount > 0 && failCount > 0) {
                // 部分成功：使用顿号明确区分成功和失败的集数
                const successRange = this.formatEpisodeRange(successEpisodes, true);
                const failRange = this.formatEpisodeRange(failEpisodes, true);
                this.showToast(`《${showName}》第 ${seasonNumber} 季 · ${successRange}刷新成功，${failRange}刷新失败`);
                console.warn('部分刷新失败:', errors);
              } else {
                // 全部失败
                const episodeRange = this.formatEpisodeRange(failEpisodes);
                this.showToast(`《${showName}》第 ${seasonNumber} 季 · ${episodeRange}刷新失败`);
                console.error('集刷新失败:', errors);
              }
              
            } else {
              // 单集：直接刷新
              if (!episode.episode_number) {
                console.warn('单集缺少集号参数', episode);
                this.showToast('缺少集号信息');
                return;
              }
              
              const response = await axios.get('/api/calendar/refresh_episode', {
                params: {
                  tmdb_id: episode.tmdb_id,
                  season_number: episode.season_number,
                  episode_number: episode.episode_number
                }
              });
              
              if (response.data.success) {
                this.showToast(response.data.message || '单集刷新成功');
              } else {
                this.showToast(response.data.message || '单集刷新失败');
              }
            }
            
            // 热更新数据
            await this.refreshCalendarData();
            
          } catch (error) {
            console.error('刷新单集元数据失败:', error);
            this.showToast('刷新失败：' + (error.response?.data?.message || error.message));
          }
        },
        
        // 刷新整个季的元数据（内容管理视图使用）
        async refreshSeasonMetadata(task) {
          try {
            if (!task || !task.match_tmdb_id) {
              console.warn('刷新季元数据失败：缺少必要参数', task);
              return;
            }
            
            // 获取季数：仅使用任务匹配到的季（未匹配则不继续）
            const season_number = task.matched_latest_season_number;
            if (!season_number) {
              this.showToast('该任务未匹配季信息');
              return;
            }
            
            // 先刷新剧级别详情（更新节目状态/最新季/海报等）
            try {
              await axios.get('/api/calendar/refresh_show', {
                params: { tmdb_id: task.match_tmdb_id }
              });
            } catch (e) {
              // 忽略失败，继续刷新季
              console.warn('刷新剧详情失败（忽略继续）:', e);
            }

            const response = await axios.get('/api/calendar/refresh_season', {
              params: {
                tmdb_id: task.match_tmdb_id,
                season_number: season_number
              }
            });
            
            if (response.data.success) {
              // 显示成功提示
              this.showToast(response.data.message || '季元数据刷新成功');
              
              // 刷新任务数据，确保节目状态（如 本季终/已完结/已取消）及时更新
              try {
                const tasksResponse = await axios.get('/api/calendar/tasks');
                if (tasksResponse.data && tasksResponse.data.success) {
                  this.calendar.tasks = tasksResponse.data.data.tasks;
                  this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                }
              } catch (e) {}

              // 热更新日历与本地剧集数据
              await this.refreshCalendarData();
            } else {
              this.showToast(response.data.message || '季元数据刷新失败');
            }
          } catch (error) {
            console.error('刷新季元数据失败:', error);
            this.showToast('季元数据刷新失败：' + (error.response?.data?.message || error.message));
          }
        },

        // 打开编辑元数据模态框
        openEditMetadataModal(task) {
          try {
            if (!task) return;
            // 预填充表单数据
            const currentName = task.task_name || '';
            const currentType = this.getContentTypeCN(task.content_type) || '';
            const currentTmdbId = (task.match && task.match.tmdb_id) || task.match_tmdb_id || (task.calendar_info && task.calendar_info.match && task.calendar_info.match.tmdb_id) || '';
            // 仅使用匹配季：若未匹配则为空，由界面表现为未匹配
            const currentSeason = task.matched_latest_season_number || '';
            const matchedName = task.matched_show_name || '';
            const matchedYear = task.matched_year || '';

            this.editMetadata = {
              visible: true,
              original: {
                task_name: currentName,
                content_type: task.content_type || '',
                tmdb_id: currentTmdbId || '',
                season_number: currentSeason || ''
              },
              form: {
                task_name: currentName,
                content_type: task.content_type || '',
                tmdb_id: '',
                season_number: currentSeason || 1,
                custom_poster_url: ''
              },
              display: {
                matched_label: matchedName ? `${matchedName}${matchedYear ? ' (' + matchedYear + ')' : ''}` : '未匹配',
                matched_tmdb_id: currentTmdbId || '',
                matched_season_number: currentSeason || '',
                seasonInputWidth: '32px'
              },
              hint: `若匹配结果不正确，请前往 <a href="https://www.themoviedb.org/" target="_blank" class="tmdb-link">TMDB</a> 搜索对应的正确条目，并使用该条目网址末尾的数字（即 TMDB ID）来修正匹配`
            };

            $('#editMetadataModal').modal('show');
            // 初始化季数输入宽度
            this.$nextTick(() => {
              try {
                const initVal = String(this.editMetadata.form.season_number || '1');
                const px = this.measureSeasonInputWidth(initVal);
                this.$set(this.editMetadata.display, 'seasonInputWidth', px + 'px');
              } catch (e) {}
            });
            // 若已匹配但没有季数信息，则从后端获取最新季数用于展示
            try {
              const tid = this.editMetadata.display.matched_tmdb_id;
              const hasSeason = !!this.editMetadata.display.matched_season_number;
              if (tid && !hasSeason) {
                axios.get('/api/calendar/show_info', { params: { tmdb_id: tid } })
                  .then(res => {
                    if (res.data && res.data.success && res.data.data) {
                      const sn = res.data.data.latest_season_number;
                      if (sn) {
                        this.$set(this.editMetadata.display, 'matched_season_number', sn);
                      }
                    }
                  })
                  .catch(() => {});
              }
            } catch (e) {}
          } catch (e) {
            this.showToast('打开编辑失败');
          }
        },

        // 保存编辑元数据
        async saveEditMetadata() {
          try {
            this.editMetadata.loading = true;
            // 立即刷新一次视图，确保标题处 spinner 及时渲染
            await this.$nextTick();
            // 直接强制显示并触发一次重绘，确保首帧可见
            try {
              var __sp = document.querySelector('#editMetadataModal .modal-title .spinner-border');
              if (__sp) {
                __sp.style.display = 'inline-block';
                void __sp.offsetHeight;
              }
            } catch (e) {}
            // 再让出到宏任务队列，确保浏览器先完成一次绘制
            await new Promise(function(resolve){ setTimeout(resolve, 0); });
            if (!this.editMetadata || !this.editMetadata.form) return;
            const payload = {
              task_name: this.editMetadata.original.task_name,
              new_task_name: this.editMetadata.form.task_name,
              new_content_type: this.editMetadata.form.content_type,
              new_tmdb_id: this.editMetadata.form.tmdb_id,
              new_season_number: this.editMetadata.form.season_number,
              custom_poster_url: this.editMetadata.form.custom_poster_url
            };

            // 如果没有任何变化则直接关闭
            const noNameChange = (payload.new_task_name || '') === (this.editMetadata.original.task_name || '');
            const noTypeChange = (payload.new_content_type || '') === (this.editMetadata.original.content_type || '');
            const noRematch = !(payload.new_tmdb_id && String(payload.new_tmdb_id).trim()) && !(payload.new_season_number && String(payload.new_season_number).trim());
            const noPosterChange = !(payload.custom_poster_url && String(payload.custom_poster_url).trim());
            if (noNameChange && noTypeChange && noRematch && noPosterChange) {
              $('#editMetadataModal').modal('hide');
              return;
            }

            const res = await axios.post('/api/calendar/edit_metadata', payload);
            if (res.data && res.data.success) {
              this.showToast(res.data.message || '保存成功');
              $('#editMetadataModal').modal('hide');
              // 热更新任务与日历
              // 避免触发“未保存修改”提示：本次更新由后端变更引发
              this.suppressConfigModifiedOnce = true;
              // 本次操作来自编辑元数据，不应提示未保存
              this.configModified = false;
              // 触发海报缓存穿透（按节目维度）：尽可能多地命中该节目的不同名称键
              try {
                const nowTick = Date.now();
                const eid = (this.editMetadata.display && this.editMetadata.display.matched_tmdb_id) || (this.editMetadata.original && this.editMetadata.original.tmdb_id);
                const taskName = (this.editMetadata.original && this.editMetadata.original.task_name) || '';
                const matchedLabel = (this.editMetadata.display && this.editMetadata.display.matched_label) || '';
                const normalizeMatchedName = (label) => {
                  try {
                    // 去掉类似 "名称 (2023)" 的年份后缀
                    const m = String(label).trim().match(/^(.*?)(\s*\(\d{4}\))?$/);
                    return (m && m[1]) ? m[1].trim() : String(label).trim();
                  } catch (e) { return String(label || '').trim(); }
                };
                const normalizedMatched = normalizeMatchedName(matchedLabel);
                // 从任务映射中找 show_name
                let showName = '';
                try {
                  if (taskName && this.calendar && this.calendar.taskMapByName && this.calendar.taskMapByName[taskName]) {
                    showName = (this.calendar.taskMapByName[taskName].show_name || '').trim();
                  }
                } catch (e) {}

                if (eid) { this.$set(this.imageCacheBustById, eid, nowTick); }
                if (taskName) { this.$set(this.imageCacheBustByShowName, taskName, nowTick); }
                if (showName) { this.$set(this.imageCacheBustByShowName, showName, nowTick); }
                if (normalizedMatched) { this.$set(this.imageCacheBustByShowName, normalizedMatched, nowTick); }
                if (!eid && !taskName && !showName && !normalizedMatched) { this.imageCacheBustTick = nowTick; }
              } catch (e) { this.imageCacheBustTick = Date.now(); }
              // 并行刷新：优先尽快更新任务列表的类型按钮与映射
              try {
                const tasksPromise = axios.get('/api/calendar/tasks');
                // 同时后台刷新日历数据（不阻塞UI）
                const refreshPromise = this.refreshCalendarData().catch(() => {});
                const tasksResponse = await tasksPromise;
                if (tasksResponse.data && tasksResponse.data.success) {
                  this.calendar.tasks = tasksResponse.data.data.tasks || [];
                  try {
                    this.calendar.taskMapByName = {};
                    (this.calendar.tasks || []).forEach(t => {
                      const key = (t.task_name || t.taskname || '').trim();
                      if (key) this.calendar.taskMapByName[key] = t;
                    });
                  } catch (e) { this.calendar.taskMapByName = {}; }
                  // 同步更新任务列表类型集合（热更新左上角类型按钮）
                  try {
                    let rawTypes = (tasksResponse.data && tasksResponse.data.data && tasksResponse.data.data.content_types) || [];
                    if (!rawTypes || rawTypes.length === 0) {
                      rawTypes = Array.from(new Set((this.calendar.tasks || []).map(t => (t && t.content_type) || '').filter(Boolean)));
                    }
                    const known = ['tv','anime','variety','documentary'];
                    const uniq = Array.from(new Set(rawTypes.filter(Boolean)));
                    const ordered = uniq.sort((a,b) => {
                      const ia = known.indexOf(a); const ib = known.indexOf(b);
                      if (ia === -1 && ib === -1) return 0;
                      if (ia === -1) return 1;
                      if (ib === -1) return -1;
                      return ia - ib;
                    });
                    this.tasklist.contentTypes = ordered;
                    const validSet = ['all', ...ordered];
                    if (!validSet.includes(this.tasklist.selectedType)) {
                      this.tasklist.selectedType = 'all';
                    }
                  } catch (e) { /* 忽略类型热更新异常 */ }
                }
                await refreshPromise;
                // 再次触发缓存穿透，保证刷新后的数据也使用新时间戳
                try {
                  const nowTick = Date.now();
                  const eid = (this.editMetadata.display && this.editMetadata.display.matched_tmdb_id) || (this.editMetadata.original && this.editMetadata.original.tmdb_id);
                  const taskName = (this.editMetadata.original && this.editMetadata.original.task_name) || '';
                  const matchedLabel = (this.editMetadata.display && this.editMetadata.display.matched_label) || '';
                  const normalizeMatchedName = (label) => {
                    try {
                      const m = String(label).trim().match(/^(.*?)(\s*\(\d{4}\))?$/);
                      return (m && m[1]) ? m[1].trim() : String(label).trim();
                    } catch (e) { return String(label || '').trim(); }
                  };
                  const normalizedMatched = normalizeMatchedName(matchedLabel);
                  let showName = '';
                  try {
                    if (taskName && this.calendar && this.calendar.taskMapByName && this.calendar.taskMapByName[taskName]) {
                      showName = (this.calendar.taskMapByName[taskName].show_name || '').trim();
                    }
                  } catch (e) {}

                  if (eid) { this.$set(this.imageCacheBustById, eid, nowTick); }
                  if (taskName) { this.$set(this.imageCacheBustByShowName, taskName, nowTick); }
                  if (showName) { this.$set(this.imageCacheBustByShowName, showName, nowTick); }
                  if (normalizedMatched) { this.$set(this.imageCacheBustByShowName, normalizedMatched, nowTick); }
                  if (!eid && !taskName && !showName && !normalizedMatched) { this.imageCacheBustTick = nowTick; }
                } catch (e) { this.imageCacheBustTick = Date.now(); }
                // 刷新链路结束后，确保未保存标记为false
                this.configModified = false;
              } catch (e) { /* 忽略后台刷新异常，前端不报错 */ }
            } else {
              this.showToast(res.data.message || '保存失败');
            }
          } catch (e) {
            this.showToast('保存失败：' + (e.response?.data?.message || e.message));
          } finally {
            this.editMetadata.loading = false;
          }
        },
        // 根据输入内容自适应季数输入框宽度，最小32px
        autoSizeSeasonInput(e) {
          try {
            const el = e && e.target;
            if (!el) return;
            const val = String(el.value || '');
            // 使用隐藏量尺精确测量当前文本宽度
            let ruler = document.getElementById('season-width-ruler');
            if (!ruler) {
              ruler = document.createElement('span');
              ruler.id = 'season-width-ruler';
              ruler.style.position = 'absolute';
              ruler.style.visibility = 'hidden';
              ruler.style.whiteSpace = 'pre';
              ruler.style.font = window.getComputedStyle(el).font;
              document.body.appendChild(ruler);
            }
            ruler.style.font = window.getComputedStyle(el).font;
            ruler.textContent = val || '1';
            const textWidth = ruler.getBoundingClientRect().width;
            // 左右 padding(16) + 边框(2)
            const px = Math.max(32, Math.ceil(textWidth + 16 + 2));
            if (this.editMetadata && this.editMetadata.display) {
              this.$set(this.editMetadata.display, 'seasonInputWidth', px + 'px');
            } else {
              el.style.width = px + 'px';
            }
          } catch (err) {}
        },
        // 供初始化时测量使用
        measureSeasonInputWidth(val) {
          try {
            const el = document.querySelector('#editMetadataModal .edit-season-number');
            if (!el) return 32;
            let ruler = document.getElementById('season-width-ruler');
            if (!ruler) {
              ruler = document.createElement('span');
              ruler.id = 'season-width-ruler';
              ruler.style.position = 'absolute';
              ruler.style.visibility = 'hidden';
              ruler.style.whiteSpace = 'pre';
              document.body.appendChild(ruler);
            }
            const style = window.getComputedStyle(el);
            ruler.style.font = style.font;
            ruler.textContent = String(val || '1');
            const textWidth = ruler.getBoundingClientRect().width;
            return Math.max(32, Math.ceil(textWidth + 16 + 2));
          } catch (e) { return 32; }
        },
        
        // 格式化集数范围显示
        formatEpisodeRange(episodeNumbers, useComma = false) {
          if (!episodeNumbers || episodeNumbers.length === 0) {
            return '';
          }
          
          // 排序集数
          const sortedEpisodes = [...episodeNumbers].sort((a, b) => a - b);
          
          if (sortedEpisodes.length === 1) {
            return `第 ${sortedEpisodes[0]} 集`;
          }
          
          // 如果强制使用顿号（用于部分成功的情况）
          if (useComma) {
            return `第 ${sortedEpisodes.join('、')} 集`;
          }
          
          // 检查是否为连续集数
          const isConsecutive = sortedEpisodes.every((ep, index) => {
            if (index === 0) return true;
            return ep === sortedEpisodes[index - 1] + 1;
          });
          
          if (isConsecutive) {
            // 连续集数：第 33 至 35 集
            return `第 ${sortedEpisodes[0]} 至 ${sortedEpisodes[sortedEpisodes.length - 1]} 集`;
          } else {
            // 非连续集数：第 33、35、37 集
            return `第 ${sortedEpisodes.join('、')} 集`;
          }
        },
        
        // 是否需要自动刷新：默认30分钟节流（可根据需要调整）
        shouldAutoRefreshCalendar() {
          const key = 'calendar_last_auto_refresh';
          const throttleMs = 30 * 60 * 1000; // 30分钟
          try {
            const last = parseInt(localStorage.getItem(key) || '0');
            if (!last || (Date.now() - last) > throttleMs) return true;
          } catch (e) {
            return true;
          }
          return false;
        },

        // 计算“最近转存文件”的签名，便于快速判断是否有变化
        calcLatestFilesSignature(mapObj) {
          try {
            if (!mapObj) return '';
            const entries = Object.keys(mapObj).sort().map(k => `${k}:${mapObj[k]}`);
            return entries.join('|');
          } catch (e) { return ''; }
        },

        // 启动后台监听：每60秒轻量检查一次 task_latest_info，变更则触发热更新
        startCalendarAutoWatch() {
          try {
            if (this.calendarAutoWatchTimer) return;
            // 初始化当前签名（若任务面板已加载过最新信息可直接使用；否则置空）
            this.calendarLatestFilesSignature = this.calcLatestFilesSignature(this.taskLatestFiles);
            const tick = async () => {
              try {
                const res = await axios.get('/task_latest_info');
                if (res.data && res.data.success) {
                  const latestFiles = res.data.data.latest_files || {};
                  const sig = this.calcLatestFilesSignature(latestFiles);
                  if (sig && sig !== this.calendarLatestFilesSignature) {
                    // 更新签名，触发热更新
                    this.calendarLatestFilesSignature = sig;
                    // 先更新任务面板数据（若有显示）
                    this.taskLatestFiles = latestFiles;
                    this.taskLatestRecords = res.data.data.latest_records || {};
                    // 同步重建“已转存”判定所需的进度映射，确保UI立即反映
                    try {
                      this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(latestFiles);
                      this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                    } catch (e) {}
                    // 拉取并热更新日历
                    await this.refreshCalendarData();
                    // 同步“今日更新”
                    try { await this.loadTodayUpdatesLocal(); } catch (e) {}
                  }
                }
              } catch (e) {
                // 忽略错误，下一轮继续
              }
            };
            this.calendarAutoWatchTickRef = tick;
            // 立即执行一次检查（不阻塞UI），随后每60秒执行
            setTimeout(tick, 0);
            const baseIntervalMs = this.calendar && this.calendar.manageMode ? 5 * 1000 : 60 * 1000;
            if (!document.hidden) {
              this.calendarAutoWatchTimer = setInterval(tick, baseIntervalMs);
            }
            // 页面可见性变化：隐藏时暂停，显示时恢复并立刻检查（SSE 在时不重复启轮询）
            const onFocusOrVisible = () => tick();
            const onVisibilityChange = () => {
              if (document.hidden) {
                if (this.calendarAutoWatchTimer) {
                  clearInterval(this.calendarAutoWatchTimer);
                  this.calendarAutoWatchTimer = null;
                }
              } else {
                if (!this.appSSE && !this.calendarAutoWatchTimer && this.calendarAutoWatchTickRef) {
                  const baseIntervalMs = this.calendar && this.calendar.manageMode ? 5 * 1000 : 60 * 1000;
                  this.calendarAutoWatchTimer = setInterval(this.calendarAutoWatchTickRef, baseIntervalMs);
                  this.calendarAutoWatchTickRef();
                }
              }
            };
            this.calendarAutoWatchFocusHandler = onFocusOrVisible;
            this.calendarAutoWatchVisibilityHandler = onVisibilityChange;
            window.addEventListener('focus', this.calendarAutoWatchFocusHandler);
            document.addEventListener('visibilitychange', this.calendarAutoWatchVisibilityHandler);

            // 建立/复用全局 SSE 连接（成功建立后停用轮询，失败时回退轮询）
            try {
              this.ensureGlobalSSE();
              if (this.appSSE && !this.calendarSSEListenerAdded) {
                const onChanged = async (ev) => {
                  try {
                    // 解析变更原因（后端通过 SSE data 传递）
                    let changeReason = '';
                    try { changeReason = JSON.parse(ev && ev.data || '{}').reason || ''; } catch (e) {}

                    // 如果是海报更新通知（poster_updated:<tmdb_id>），为该节目设置缓存穿透，避免使用旧缓存
                    try {
                      if (typeof changeReason === 'string' && changeReason.startsWith('poster_updated:')) {
                        const idStr = changeReason.split(':')[1] || '';
                        const eid = parseInt(idStr, 10);
                        if (!isNaN(eid)) {
                          const nowTick = Date.now();
                          this.$set(this.imageCacheBustById, eid, nowTick);
                        }
                      }
                    } catch (e) {}

                    // 先拉取最新转存信息并重建映射（用于管理视图与进度判定）
                    try {
                      const latestRes = await axios.get('/task_latest_info');
                      if (latestRes.data && latestRes.data.success) {
                        const latestFiles = latestRes.data.data.latest_files || {};
                        this.taskLatestFiles = latestFiles;
                        this.taskLatestRecords = latestRes.data.data.latest_records || {};
                        this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(latestFiles);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    
                    // 重新加载任务数据，确保内容管理页面能热更新
                    try {
                      const tasksResponse = await axios.get('/api/calendar/tasks');
                      if (tasksResponse.data.success) {
                        this.calendar.tasks = tasksResponse.data.data.tasks;
                        this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                        // 重新构建进度映射
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                        
                        // 重新计算内容类型，确保类型按钮能热更新
                        const rawTypes = tasksResponse.data.data.content_types || [];
                        this.updateContentTypes(rawTypes);
                      }
                    } catch (e) {}
                    
                    // 若为任务编辑/保存操作，刷新任务列表 formData.tasklist，实现任务列表页面热更新
                    // 注意：task_updated 事件不重新加载任务列表，因为用户刚刚保存的数据就是最新的
                    try {
                      if (changeReason === 'edit_metadata') {
                        const dataRes = await axios.get('/data');
                        if (dataRes.data && dataRes.data.success) {
                          const cfg = dataRes.data.data || {};
                          const oldTaskCount = (this.formData.tasklist || []).length;
                          // 后端推送导致的任务列表更新，不应触发"未保存修改"提示
                          this.suppressConfigModifiedOnce = true;
                          this.formData.tasklist = cfg.tasklist || [];
                          // 同步任务名集合用于筛选
                          this.calendar.taskNames = (this.formData.tasklist || []).map(t => t.taskname).filter(Boolean);
                          // 如任务数量变化，重建与任务相关的最新文件映射的键集合
                          if ((this.formData.tasklist || []).length !== oldTaskCount) {
                            try {
                              this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(this.taskLatestFiles || {});
                            } catch (e) {}
                          }
                        }
                      }
                    } catch (e) {}

                    // 如果是转存记录相关的通知，立即刷新今日更新数据
                    if (changeReason === 'transfer_record_created' || changeReason === 'transfer_record_updated' || changeReason === 'batch_rename_completed') {
                        try { await this.loadTodayUpdatesLocal(); } catch (e) {}
                    }
                    
                    // 再仅本地读取并热更新日历/海报视图
                    await this.loadCalendarEpisodesLocal();
                    this.initializeCalendarDates();
                    // 并行刷新今日更新数据
                    try { await this.loadTodayUpdatesLocal(); } catch (e) {}
                  } catch (e) {}
                };
                this.onCalendarChangedHandler = onChanged;
                this.appSSE.addEventListener('calendar_changed', onChanged);
                this.calendarSSEListenerAdded = true;
                // onopen/onerror 已集中在 ensureGlobalSSE，无需重复设置
              }
            } catch (e) {
              // 忽略 SSE 失败，继续使用轮询
            }
          } catch (e) {
            // ignore
          }
        },

        // 停止后台监听并移除事件
        stopCalendarAutoWatch() {
          try {
            if (this.calendarAutoWatchTimer) {
              clearInterval(this.calendarAutoWatchTimer);
              this.calendarAutoWatchTimer = null;
            }
            if (this.calendarAutoWatchFocusHandler) {
              window.removeEventListener('focus', this.calendarAutoWatchFocusHandler);
              this.calendarAutoWatchFocusHandler = null;
            }
            if (this.calendarAutoWatchVisibilityHandler) {
              document.removeEventListener('visibilitychange', this.calendarAutoWatchVisibilityHandler);
              this.calendarAutoWatchVisibilityHandler = null;
            }
            // 不再关闭全局 SSE；仅移除本地监听（如有需要）
          } catch (e) {
            // ignore
          }
        },
        
        // 选择内容类型
        selectCalendarType(type) {
          this.calendar.selectedType = type;
          try {
            localStorage.setItem('calendar_selected_type', type);
          } catch (e) {
            // ignore
          }
          // 重新构建月视图数据以应用筛选（天内episodes依赖selectedType）
          this.initializeCalendarDates();
        },
        
        // 清除筛选器
        clearCalendarFilter(filterType) {
          if (filterType === 'nameFilter') {
            this.calendar.nameFilter = '';
          } else if (filterType === 'taskFilter') {
            this.calendar.taskFilter = '';
          }
        },
        
        // 更新内容类型列表（用于热更新）
        updateContentTypes(rawTypes) {
          const knownOrderWithoutOther = ['tv', 'anime', 'variety', 'documentary'];
          const hasOther = rawTypes.includes('other');
          const typeSet = new Set(rawTypes);
          const orderedKnown = knownOrderWithoutOther.filter(t => typeSet.has(t));
          const unknownTypes = rawTypes.filter(t => !knownOrderWithoutOther.concat(['other']).includes(t));
          this.calendar.contentTypes = [
            ...orderedKnown,
            ...unknownTypes,
            ...(hasOther ? ['other'] : [])
          ];
        },
        
        // 切换视图模式
        toggleCalendarViewMode() {
          this.calendar.viewMode = this.calendar.viewMode === 'poster' ? 'month' : 'poster';
          try {
            localStorage.setItem('calendar_view_mode', this.calendar.viewMode);
          } catch (e) {
            console.warn('无法持久化保存日历视图模式:', e);
          }
          this.initializeCalendarDates();
        },
        
        // 切换合并集模式
        toggleCalendarMergeEpisodes() {
          this.calendar.mergeEpisodes = !this.calendar.mergeEpisodes;
          try {
            localStorage.setItem('calendar_merge_episodes', this.calendar.mergeEpisodes.toString());
          } catch (e) {
            console.warn('无法持久化保存合并集设置:', e);
          }
          // 重新初始化日历数据以应用新的合并集设置
          this.initializeCalendarDates();
        },
        
        // 更改日期
        changeCalendarDate(action) {
          const currentDate = new Date(this.calendar.currentDate);
          
          switch (action) {
            case 'prevDay':
              currentDate.setDate(currentDate.getDate() - 1);
              break;
            case 'nextDay':
              currentDate.setDate(currentDate.getDate() + 1);
              break;
            case 'prevWeek':
              currentDate.setDate(currentDate.getDate() - 7);
              break;
            case 'nextWeek':
              currentDate.setDate(currentDate.getDate() + 7);
              break;
            case 'prevMonth':
              currentDate.setMonth(currentDate.getMonth() - 1);
              break;
            case 'nextMonth':
              currentDate.setMonth(currentDate.getMonth() + 1);
              break;
          }
          
          this.calendar.currentDate = currentDate;
          this.initializeCalendarDates();
        },
        
        // 回到今天
        goToToday() {
          this.calendar.currentDate = new Date();
          this.initializeCalendarDates();
        },
        
        // 处理海报悬停
        handleCalendarPosterHover(event, episode) {
          // 从图片提取颜色并为覆盖层生成渐变背景（与影视发现页一致）
          const posterElement = event.currentTarget;
          const imgElement = posterElement.querySelector('img');
          const overlayElement = posterElement.querySelector('.calendar-poster-overlay');
          const overviewElement = overlayElement ? overlayElement.querySelector('.info-line.overview') : null;

          if (imgElement && overlayElement) {
            if (imgElement.complete) {
              const gradient = this.createGradientFromImage(imgElement);
              overlayElement.style.background = gradient;
            } else {
              overlayElement.style.background = 'var(--dark-text-color)';
            }
          }

          // 按行数截断：根据宽度估算每行字符数与行高，保证最后一行完整并追加省略号
          if (overviewElement) {
            try {
              const maxHeightRatio = 0.6; // 简介最高占比60%
              const posterHeight = posterElement.clientHeight || 0;
              const overlayStyles = window.getComputedStyle(overviewElement);
              const lineHeight = parseFloat(overlayStyles.lineHeight) || 18; // 兜底18px
              const fontSize = parseFloat(overlayStyles.fontSize) || 14; // 兜底14px
              const containerWidth = posterElement.clientWidth || 0;
              if (!posterHeight || !containerWidth || !lineHeight) return;

              const maxHeight = Math.floor(posterHeight * maxHeightRatio);
              const maxLines = Math.max(1, Math.floor(maxHeight / lineHeight));

              // 基于经验的单行字符估算（中文字符大约 ~0.55 * 宽度/字体大小）
              const charsPerLine = Math.max(6, Math.floor(containerWidth / (fontSize * 0.55)));

              const fullText = overviewElement.getAttribute('data-fulltext') || overviewElement.textContent || '';
              // 如果天然高度未超过限制则不处理
              overviewElement.textContent = fullText;
              overviewElement.style.maxHeight = '';
              const naturalHeight = overviewElement.scrollHeight;
              if (naturalHeight <= maxHeight) {
                return; // 文本本来就不超出，无需截断
              }

              // 目标字符上限（留出省略号空间）
              const targetChars = Math.max(1, (maxLines * charsPerLine) - 1);

              // 二分查找截断位置，确保不超过最大高度
              let left = 0, right = fullText.length, best = 0;
              while (left <= right) {
                const mid = Math.floor((left + right) / 2);
                const trial = fullText.slice(0, Math.min(mid, targetChars)) + '…';
                overviewElement.textContent = trial;
                if (overviewElement.scrollHeight <= maxHeight) {
                  best = mid;
                  left = mid + 1;
                } else {
                  right = mid - 1;
                }
              }

              const finalText = fullText.length > best ? (fullText.slice(0, Math.min(best, targetChars)) + '…') : fullText;
              overviewElement.textContent = finalText;
            } catch (e) {
              // ignore
            }
          }
        },
        
        // 隐藏海报悬停
        hideCalendarPosterHover() {
          // 仅由CSS控制透明度，不重置背景以保留取色渐变
        },
        
        // 获取剧集海报URL
        getEpisodePosterUrl(episode) {
          // 优先使用本地缓存海报
          if (episode.poster_local_path) {
            // 确保路径以 / 开头，避免相对路径问题
            const path = episode.poster_local_path.startsWith('/') 
              ? episode.poster_local_path 
              : '/' + episode.poster_local_path;
            // 优先使用按节目维度的缓存穿透参数
            let tick = 0;
            try {
              const eid = (episode.tmdb_id || episode.tv_id || episode.id);
              const sname = (episode.show_name || '').trim();
              if (eid && this.imageCacheBustById && this.imageCacheBustById[eid]) {
                tick = this.imageCacheBustById[eid];
              } else if (sname && this.imageCacheBustByShowName && this.imageCacheBustByShowName[sname]) {
                tick = this.imageCacheBustByShowName[sname];
              } else {
                // 优化：任务列表海报视图下，未命中特定节目/名称的情况下不使用全局穿透参数，避免本地海报反复绕过浏览器缓存
                // 仅对日历等视图保留全局 bust（以确保热更新及时生效）
                if (this.activeTab === 'calendar') {
                  tick = this.imageCacheBustTick || 0;
                } else {
                  tick = 0;
                }
              }
            } catch (e) { tick = this.imageCacheBustTick || 0; }
            return tick ? `${path}?t=${tick}` : path;
          }
          
          // 如果没有海报，返回默认图片
          return '/static/images/no-poster.svg';
        },
        
        // 处理图片加载错误
        handleCalendarImageError(event) {
          console.warn('海报加载失败:', event.target.src);
          event.target.src = '/static/images/no-poster.svg';
        },
        
        // ===== 精简版“已转存”判定：仅依据任务列表最近转存文件构建的映射 =====
        // 提取进度（优先按任务名命中，其次按剧名命中）
        getSimpleProgress(episode) {
          if (!episode) return null;
          // 1) 任务名映射
          const tname = episode.task_info && episode.task_info.task_name ? episode.task_info.task_name : null;
          if (tname && this.calendar.progressByTaskName && this.calendar.progressByTaskName[tname]) {
            const p = this.calendar.progressByTaskName[tname] || {};
            const epNum = p.episode_number != null ? parseInt(p.episode_number) : null;
            const air = p.air_date || null;
            return { episode_number: isNaN(epNum) ? null : epNum, air_date: air };
          }
          // 2) 剧名映射
          const sname = episode.show_name || null;
          if (sname && this.calendar.progressByShowName && this.calendar.progressByShowName[sname]) {
            const p = this.calendar.progressByShowName[sname] || {};
            const epNum = p.episode_number != null ? parseInt(p.episode_number) : null;
            const air = p.air_date || null;
            return { episode_number: isNaN(epNum) ? null : epNum, air_date: air };
          }
          return null;
        },

        // 用于比较的集号（合并集取末尾一集；否则取自身集号）
        getEpisodeCompareNumber(episode) {
          if (!episode) return null;
          if (episode.is_merged && episode.episode_range && episode.episode_range.end != null) {
            const n = parseInt(episode.episode_range.end);
            return isNaN(n) ? null : n;
          }
          if (episode.episode_number != null) {
            const n = parseInt(episode.episode_number);
            return isNaN(n) ? null : n;
          }
          return null;
        },

        // 统一判断“是否已转存/已完成”
        isEpisodeReachedProgress(episode) {
          const prog = this.getSimpleProgress(episode);
          if (!prog) return false;
          const lastNum = this.getEpisodeCompareNumber(episode);
          if (prog.episode_number != null && lastNum != null) {
            return lastNum <= prog.episode_number;
          }
          if (prog.air_date && episode && episode.air_date) {
            return episode.air_date <= prog.air_date;
          }
          return false;
        },

        
        
        // 获取剧集显示集数
        getEpisodeDisplayNumber(episode) {
          if (episode.is_merged) {
            // 合并集直接使用格式化后的集数
            return episode.episode_number;
          } else {
            // 普通集数按原格式显示
            return `S${episode.season_number.toString().padStart(2, '0')}E${episode.episode_number.toString().padStart(2, '0')}`;
          }
        },
        
        // 获取仅集数格式（省略季）
        getEpisodeOnlyNumber(episode) {
          if (episode.is_merged) {
            // 合并集：S01E34-E38 -> E34-E38
            return episode.episode_number.replace(/^S\d+/, '');
          } else {
            // 普通集数：S01E06 -> E06
            return `E${episode.episode_number.toString().padStart(2, '0')}`;
          }
        },
        
        // 获取纯数字格式
        getNumberOnly(episode) {
          if (episode.is_merged) {
            // 合并集：S01E34-E38 -> 34-38
            return episode.episode_number.replace(/^S\d+E/, '').replace(/E/g, '');
          } else {
            // 普通集数：S01E06 -> 06
            return episode.episode_number.toString().padStart(2, '0');
          }
        },
        
        // 获取剧集提示信息
        getEpisodeTooltip(episode) {
          // 规范：集数悬停显示对应集在TMDB上的单集标题
          // 普通：SxxExx 标题
          // 合并：逐行列出每一集：SxxExx 标题
          const pad2 = n => String(n).padStart(2, '0');
          if (episode.is_merged && Array.isArray(episode.original_episodes) && episode.original_episodes.length) {
            const lines = episode.original_episodes.map(ep => {
              const s = ep.season_number ? pad2(ep.season_number) : pad2(episode.season_number || 1);
              const e = ep.episode_number ? pad2(ep.episode_number) : '';
              const title = ep.name || '';
              return `S${s}E${e} ${title}`.trim();
            });
            return lines.join('\n');
          }
          const s = episode.season_number ? pad2(episode.season_number) : '01';
          const e = episode.episode_number ? pad2(episode.episode_number) : '01';
          const title = episode.name || '';
          return `S${s}E${e} ${title}`.trim();
        },

        // 打开剧的TMDB页面
        openShowTmdbPage(episode) {
          try {
            const tmdbId = episode.tmdb_id || (episode.task_info && episode.task_info.match && episode.task_info.match.tmdb_id) || (episode.task_info && episode.task_info.tmdb_id);
            if (tmdbId) {
              const url = `https://www.themoviedb.org/tv/${tmdbId}`;
              window.open(url, '_blank');
            }
          } catch (e) {
            console.warn('打开剧TMDB页面失败', e);
          }
        },

        // 打开"内容管理"任务匹配到的节目的 TMDB 页面
        openTaskMatchedTmdbPage(task) {
          try {
            if (!task) return;
            const tmdbId = (task.match && task.match.tmdb_id) || task.match_tmdb_id || (task.calendar_info && task.calendar_info.match && task.calendar_info.match.tmdb_id) || task.tmdb_id;
            if (tmdbId) {
              const url = `https://www.themoviedb.org/tv/${tmdbId}`;
              window.open(url, '_blank');
            }
          } catch (e) {
            console.warn('打开任务匹配TMDB页面失败', e);
          }
        },
        // 获取任务的 TMDB ID（用于任务列表海报视图）
        getTaskTmdbId(task) {
          try {
            if (!task) return null;
            // 优先从 calendar.tasks 中获取匹配的任务信息
            const calendarTask = this.getCalendarTaskByName(task.taskname);
            if (calendarTask) {
              return (calendarTask.match && calendarTask.match.tmdb_id) || calendarTask.match_tmdb_id || calendarTask.tmdb_id;
            }
            // 如果 calendar.tasks 中没有，则从任务配置中获取
            const cal = (task.calendar_info) || {};
            const match = cal.match || {};
            return match.tmdb_id || task.tmdb_id;
          } catch (e) {
            return null;
          }
        },
        // 获取任务的季数（用于任务列表海报视图）
        getTaskSeasonNumber(task) {
          try {
            if (!task) return null;
            // 优先从 calendar.tasks 中获取匹配的任务信息
            const calendarTask = this.getCalendarTaskByName(task.taskname);
            if (calendarTask) {
              return (calendarTask.match && calendarTask.match.latest_season_number) || 
                     calendarTask.matched_latest_season_number || 
                     calendarTask.season_number;
            }
            // 如果 calendar.tasks 中没有，则从任务配置中获取
            const cal = (task.calendar_info) || {};
            const match = cal.match || {};
            return match.latest_season_number || task.season_number;
          } catch (e) {
            return null;
          }
        },
        // 打开任务列表海报视图中任务的 TMDB 页面
        openTaskTmdbPage(task) {
          try {
            if (!task) return;
            const tmdbId = this.getTaskTmdbId(task);
            if (tmdbId) {
              // 获取匹配的季数
              const seasonNumber = this.getTaskSeasonNumber(task);
              if (seasonNumber) {
                // 打开特定季的页面
                const url = `https://www.themoviedb.org/tv/${tmdbId}/season/${seasonNumber}`;
                window.open(url, '_blank');
              } else {
                // 如果没有季数信息，则打开整个节目的页面
                const url = `https://www.themoviedb.org/tv/${tmdbId}`;
                window.open(url, '_blank');
              }
            }
          } catch (e) {
            console.warn('打开任务TMDB页面失败', e);
          }
        },

        // 打开集的TMDB页面（合并集打开最后一集）
        openEpisodeTmdbPage(episode) {
          try {
            const tmdbId = episode.tmdb_id || (episode.task_info && episode.task_info.match && episode.task_info.match.tmdb_id) || (episode.task_info && episode.task_info.tmdb_id);
            if (!tmdbId) return;
            const season = episode.season_number || (episode.original_episodes && episode.original_episodes.length ? episode.original_episodes[0].season_number : 1);
            const episodeNum = episode.is_merged && episode.episode_range ? episode.episode_range.end : episode.episode_number;
            if (!season || !episodeNum) return;
            const url = `https://www.themoviedb.org/tv/${tmdbId}/season/${parseInt(season)}/episode/${parseInt(episodeNum)}`;
            window.open(url, '_blank');
          } catch (e) {
            console.warn('打开集TMDB页面失败', e);
          }
        },

        // 获取插件配置的占位符文本
        getPluginConfigPlaceholder(pluginName, key) {
          const placeholders = {
            aria2: {
              host_port: "Aria2 RPC 地址，如：192.168.1.100:6800",
              secret: "Aria2 RPC 密钥",
              dir: "下载目录，需要 Aria2 拥有读写权限，如：/downloads"
            },
            alist: {
              url: "AList 服务器地址，如：http://192.168.1.100:5244",
              token: "AList 访问令牌，在 AList 的管理 - 设置 - 其他中获取", 
              storage_id: "AList 服务器夸克存储的 ID，多个 ID 用逗号分隔，如：1, 2, 3"
            },
            alist_strm: {
              url: "AList Strm 服务器地址，如：http://192.168.1.100:5000",
              cookie: "AList Strm 的 Cookie，通过 F12 抓取，关键参数：session=ey***",
              config_id: "要触发运行的配置 ID，多个 ID 用逗号分隔，如：1, 2, 3"
            },
            alist_strm_gen: {
              url: "AList 服务器地址，如：http://192.168.1.100:5244",
              token: "AList 访问令牌，在 AList 的管理 - 设置 - 其他中获取",
              storage_id: "AList 服务器夸克存储的 ID",
              strm_save_dir: "生成的 strm 文件的保存路径，如：/volume1/media/strm，请确保宿主机路径和容器路径映射一致",
              strm_replace_host: "strm 文件内链接的主机地址，可选，缺省时使用 url"
            },
            plex: {
              url: "Plex 服务器地址，如：http://192.168.1.100:32400",
              token: "Plex 访问令牌，通过 F12 抓取（或通过 XML 地址获取）",
              quark_root_path: "夸克根目录在 Plex 中的路径，多个路径用逗号分隔，如：/volume1/media/quark1, /volume1/media/quark2"
            },
            emby: {
              url: "Emby 服务器地址，如：http://192.168.1.100:8096",
              token: "Emby 的 API 密钥，在 Emby 的管理 - 高级 - API 密钥中生成"
            }
          };
          
          return placeholders[pluginName]?.[key] || '';
        },

        // 获取插件配置的帮助文本
        getPluginConfigHelp(pluginName, key) {
          const helpTexts = {
            aria2: {
              host_port: "Aria2 RPC服务地址，确保网络可达且端口开放",
              secret: "Aria2 RPC密钥，用于安全认证",
              dir: "下载文件的保存目录，确保Aria2有读写权限"
            },
            alist: {
              url: "AList服务器地址，用于获取存储信息",
              token: "AList访问令牌，用于API调用",
              storage_id: "多账号支持：多个存储ID用逗号分隔，顺序与Cookie顺序对应，如：1, 2, 3"
            },
            alist_strm: {
              url: "AList Strm服务器地址",
              cookie: "AList Strm的Cookie",
              config_id: "要触发运行的配置ID，多个ID用逗号分隔，如：1, 2, 3"
            },
            alist_strm_gen: {
              url: "AList服务器地址，用于获取存储信息",
              token: "AList访问令牌，用于API调用",
              storage_id: "夸克网盘在AList中的存储ID，在AList的存储管理中查看",
              strm_save_dir: "生成的strm文件的保存路径，确保有写入权限",
              strm_replace_host: "strm文件内链接的主机地址，用于替换默认的AList地址"
            },
            plex: {
              url: "Plex服务器地址",
              token: "Plex访问令牌",
              quark_root_path: "夸克根目录在Plex中的路径，多个路径用逗号分隔，如：/volume1/media/quark1, /volume1/media/quark2"
            },
            emby: {
              url: "Emby服务器地址",
              token: "Emby的API密钥"
            }
          };
          
          return helpTexts[pluginName]?.[key] || '';
        },

        // 获取插件任务配置
        getPluginTaskConfig(pluginName) {
          const taskConfigs = {
            aria2: {
              auto_download: true,
              pause: false,
              auto_delete_quark_files: false
            },
            alist_strm_gen: {
              auto_gen: true
            },
            emby: {
              try_match: true,
              media_id: ""
            }
          };
          return taskConfigs[pluginName] || {};
        },

        // 获取插件配置模式的帮助文本
        getPluginConfigModeHelp(pluginName) {
          return "选择插件的配置模式：独立配置允许每个任务单独设置，全局配置则所有任务共享同一套设置，且只能在系统配置页面修改";
        },

        // 获取插件任务配置的帮助文本
        getPluginTaskConfigHelp(pluginName, key) {
          const helpTexts = {
            aria2: {
              auto_download: "是否自动添加下载任务",
              pause: "添加任务后为暂停状态，不自动开始（手动下载）",
              auto_delete_quark_files: "是否在添加下载任务后自动删除夸克网盘文件"
            },
            alist_strm_gen: {
              auto_gen: "是否自动生成strm文件"
            },
            emby: {
              try_match: "是否尝试自动匹配媒体",
              media_id: "指定要刷新的媒体ID，留空则自动匹配，0表示不刷新"
            }
          };
          return helpTexts[pluginName]?.[key] || '';
        },

        // 获取插件任务配置的占位符文本
        getPluginTaskConfigPlaceholder(pluginName, key) {
          const placeholders = {
            aria2: {
              auto_download: "",
              pause: "",
              auto_delete_quark_files: ""
            },
            alist_strm_gen: {
              auto_gen: ""
            },
            emby: {
              try_match: "",
              media_id: "输入媒体 ID，留空则自动匹配，0 表示不刷新"
            }
          };
          return placeholders[pluginName]?.[key] || '';
        },

        // 检查插件配置是否被禁用
        isPluginConfigDisabled(task) {
          for (const pluginName of ['aria2', 'alist_strm_gen', 'emby']) {
            if (this.formData.plugin_config_mode[pluginName] === 'global') {
              return true;
            }
          }
          return false;
        },

        // 插件配置模式改变时的处理
        onPluginConfigModeChange(pluginName) {
          if (this.formData.plugin_config_mode[pluginName] === 'global') {
            // 切换到全局模式时，初始化全局配置
            if (!this.formData.global_plugin_config[pluginName]) {
              this.formData.global_plugin_config[pluginName] = { ...this.getPluginTaskConfig(pluginName) };
            }
          }
          
          // 更新新任务的配置，应用全局配置
          this.applyGlobalPluginConfig(this.newTask);
          
          // 更新影视发现页面创建任务的配置，应用全局配置
          if (this.createTask && this.createTask.taskData) {
            this.applyGlobalPluginConfig(this.createTask.taskData);
          }
        },

        // 全局插件配置改变时的处理
        onGlobalPluginConfigChange() {
          // 更新新任务的配置，应用全局配置
          this.applyGlobalPluginConfig(this.newTask);
          
          // 更新影视发现页面创建任务的配置，应用全局配置
          if (this.createTask && this.createTask.taskData) {
            this.applyGlobalPluginConfig(this.createTask.taskData);
          }
        },

        // 应用全局插件配置到任务
        applyGlobalPluginConfig(task) {
          if (!task.addition) {
            task.addition = {};
          }
          
          for (const pluginName of ['aria2', 'alist_strm_gen', 'emby']) {
            if (this.formData.plugin_config_mode[pluginName] === 'global') {
              // 应用全局配置到任务
              task.addition[pluginName] = { ...this.formData.global_plugin_config[pluginName] };
            }
          }
        },



        // 获取插件配置的悬停提示文本
        getPluginConfigTitle(task) {
          if (this.isPluginConfigDisabled(task)) {
            return `单个任务的插件配置，具体键值由插件定义，当前有部分插件使用了全局配置模式，在该模式下对应的配置选项将被锁定，若要修改配置，请前往系统配置页面进行操作，查阅Wiki了解详情`;
          }
          return "单个任务的插件配置，具体键值由插件定义，查阅Wiki了解详情";
        },

        // 获取创建任务时的插件配置悬停提示文本
        getCreateTaskPluginConfigTitle() {
          if (this.isPluginConfigDisabled(this.createTask.taskData)) {
            return `单个任务的插件配置，具体键值由插件定义，当前有部分插件使用了全局配置模式，在该模式下对应的配置选项将被锁定，若要修改配置，请前往系统配置页面进行操作，查阅Wiki了解详情`;
          }
          return "单个任务的插件配置，具体键值由插件定义，查阅Wiki了解详情";
        },

        // 获取Cookie状态悬停提示信息
        getCookieStatusTooltip(userInfo) {
          if (!userInfo) {
            return "账号未验证，请获取Cookie";
          }
          
          // 有昵称且账号激活
          if (userInfo.nickname && userInfo.is_active) {
            if (userInfo.has_mparam) {
              // 第一个账号支持转存和签到，其他账号只支持签到
              if (userInfo.index === 0) {
                return "账号已登录，支持转存和签到";
              } else {
                return "账号已登录，仅支持签到功能";
              }
            } else {
              // 第一个账号支持转存，其他账号不支持任何功能
              if (userInfo.index === 0) {
                return "账号已登录，仅支持转存功能";
              } else {
                return "账号已登录，不支持转存和签到";
              }
            }
          }
          
          // 有昵称但账号未激活
          if (userInfo.nickname && !userInfo.is_active) {
            return "账号已失效，请重新获取Cookie";
          }
          
          // 没有昵称但有移动端参数
          if (!userInfo.nickname && userInfo.has_mparam) {
            return "仅支持签到功能";
          }
          
          // 没有昵称也没有移动端参数
          return "账号未登录，请获取Cookie";
        },

        fetchUserInfo() {
          // 获取所有cookie对应的用户信息
          axios.get('/get_user_info')
            .then(response => {
              if (response.data.success) {
                this.userInfoList = response.data.data;
              }
            })
            .catch(error => {
              console.error('获取用户信息失败:', error);
            });
        },

        fetchAccountsDetail() {
          // 获取所有账号的详细信息，包括空间使用情况
          axios.get('/get_accounts_detail')
            .then(response => {
              if (response.data.success) {
                this.accountsDetail = response.data.data;
                // 验证当前选中的账号索引是否有效
                if (this.fileManager.selectedAccountIndex >= this.accountsDetail.length) {
                  this.fileManager.selectedAccountIndex = 0;
                  localStorage.setItem('quarkAutoSave_selectedAccountIndex', '0');
                }
              }
            })
            .catch(error => {
              console.error('获取账号详细信息失败:', error);
            });
        },

        // 迁移旧的localStorage数据到新格式（为每个账号单独存储目录）
        migrateFileManagerFolderData() {
          const oldFolderKey = 'quarkAutoSave_fileManagerLastFolder';
          const oldFolderValue = localStorage.getItem(oldFolderKey);

          if (oldFolderValue) {
            // 如果存在旧数据，将其迁移到当前选中账号的新格式
            const currentAccountIndex = this.fileManager.selectedAccountIndex;
            const newFolderKey = `quarkAutoSave_fileManagerLastFolder_${currentAccountIndex}`;

            // 只有当新格式的数据不存在时才进行迁移
            if (!localStorage.getItem(newFolderKey)) {
              localStorage.setItem(newFolderKey, oldFolderValue);
            }

            // 删除旧的数据
            localStorage.removeItem(oldFolderKey);
          }
        },

        onAccountChange() {
          // 保存选中的账号索引到localStorage
          localStorage.setItem('quarkAutoSave_selectedAccountIndex', this.fileManager.selectedAccountIndex.toString());

          // 获取新账号最后一次访问的目录
          const newAccountLastFolder = localStorage.getItem(`quarkAutoSave_fileManagerLastFolder_${this.fileManager.selectedAccountIndex}`) || 'root';

          // 重置页码并切换到新账号的最后访问目录
          this.fileManager.currentPage = 1;
          this.loadFileListWithFallback(newAccountLastFolder);

          // 如果移动文件模态框正在显示，需要重新加载目录
          if ($('#fileSelectModal').hasClass('show')) {
            const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
            if (modalType === 'move') {
              // 重置路径并重新加载根目录
              this.fileSelect.paths = [];
              this.getSavepathDetail(0);
            }
          }
        },

        refreshCurrentFolderCache(retryCount = 0) {
          // 刷新当前目录的缓存，强制重新请求最新的文件列表
          // 调用后端接口，添加强制刷新参数
          const params = {
            folder_id: this.fileManager.currentFolder || 'root',
            sort_by: this.fileManager.sortBy,
            order: this.fileManager.sortOrder,
            page_size: this.fileManager.pageSize,
            page: this.fileManager.currentPage,
            account_index: this.fileManager.selectedAccountIndex,
            force_refresh: true,  // 强制刷新参数
            timestamp: Date.now() // 添加时间戳避免缓存
          };

          axios.get('/file_list', { params })
            .then(response => {
              if (response.data.success) {
                this.fileManager.fileList = response.data.data.list;
                this.fileManager.total = response.data.data.total;
                this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
                this.fileManager.paths = response.data.data.paths || [];
                this.fileManager.gotoPage = this.fileManager.currentPage;
                // 移除成功通知

                // 检测当前的命名模式
                this.detectFileManagerNamingMode();
              } else {
                // 如果刷新失败且重试次数少于2次，则重试
                if (retryCount < 2) {
                  setTimeout(() => {
                    this.refreshCurrentFolderCache(retryCount + 1);
                  }, 1000);
                } else {
                  this.showToast('刷新失败：' + response.data.message);
                }
              }
            })
            .catch(error => {
              console.error('刷新缓存失败:', error);
              // 如果网络错误且重试次数少于2次，则重试
              if (retryCount < 2) {
                setTimeout(() => {
                  this.refreshCurrentFolderCache(retryCount + 1);
                }, 1000);
              } else {
                this.showToast('刷新缓存失败，请稍后重试');
              }
            });
        },
        // 获取指定文件夹的上级目录ID
        getParentFolderId(folderId) {
          // 如果是根目录，没有上级目录
          if (folderId === 'root' || folderId === '0') {
            return null;
          }

          // 如果是当前目录（源目录），从fileManager.paths中获取上级目录
          if (folderId === this.fileManager.currentFolder || folderId === (this.fileManager.currentFolder || "0")) {
            if (this.fileManager.paths.length === 0) {
              // 当前目录是根目录的直接子目录，上级是根目录
              return 'root';
            } else {
              // 当前目录的上级是paths中的最后一个目录
              return this.fileManager.paths[this.fileManager.paths.length - 1].fid;
            }
          }

          // 如果是目标目录，从fileSelect.paths中获取上级目录
          if (this.fileSelect.paths && this.fileSelect.paths.length > 0) {
            // 目标目录就是fileSelect.paths的最后一个目录，其上级目录是倒数第二个
            if (this.fileSelect.paths.length === 1) {
              // 目标目录是根目录的直接子目录
              return 'root';
            } else {
              // 目标目录的上级是paths中的倒数第二个目录
              return this.fileSelect.paths[this.fileSelect.paths.length - 2].fid;
            }
          } else {
            // 如果fileSelect.paths为空，说明目标目录是根目录，没有上级
            return null;
          }
        },
        // 刷新指定文件夹的缓存
        refreshFolderCache(folderId) {
          // 刷新指定目录的缓存，强制重新请求最新的文件列表
          // 调用后端接口，添加强制刷新参数
          const params = {
            folder_id: folderId || 'root',
            sort_by: this.fileManager.sortBy,
            order: this.fileManager.sortOrder,
            page_size: this.fileManager.pageSize,
            page: 1, // 使用第一页来刷新缓存
            account_index: this.fileManager.selectedAccountIndex,
            force_refresh: true  // 强制刷新参数
          };

          axios.get('/file_list', { params })
            .then(response => {
              // 如果刷新的是当前显示的文件夹，则更新显示
              if (folderId === this.fileManager.currentFolder) {
                if (response.data.success) {
                  this.fileManager.fileList = response.data.data.list;
                  this.fileManager.total = response.data.data.total;
                  this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
                  this.fileManager.paths = response.data.data.paths || [];
                  this.fileManager.gotoPage = this.fileManager.currentPage;

                  // 检测当前的命名模式
                  this.detectFileManagerNamingMode();
                } else {
                  this.showToast('刷新失败：' + response.data.message);
                }
              }
              // 如果不是当前文件夹，只是刷新缓存，不更新显示
            })
            .catch(error => {
              console.error('刷新文件夹缓存失败:', error);
              // 只有在刷新当前文件夹时才显示错误提示
              if (folderId === this.fileManager.currentFolder) {
                this.showToast('刷新缓存失败，请稍后重试');
              }
            });
        },
        // 新建文件夹
        createNewFolder() {
          // 检查是否有文件正在编辑状态
          const editingFile = this.fileManager.fileList.find(file => file._editing);
          if (editingFile) {
            // 如果有其他文件正在编辑，先保存编辑状态
            this.saveRenameFile(editingFile);
            return;
          }

          // 生成带时间戳的文件夹名称
          const timestamp = new Date().toLocaleString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
          }).replace(/[\/\s:]/g, '');
          const folderName = `新建文件夹 ${timestamp}`;

          // 调用后端API创建文件夹
          axios.post('/create_folder', {
            parent_folder_id: this.fileManager.currentFolder || '0',
            folder_name: folderName,
            account_index: this.fileManager.selectedAccountIndex
          })
            .then(response => {
              if (response.data.success) {
                // 创建成功，将新文件夹添加到文件列表
                const newFolder = response.data.data;

                // 设置编辑状态
                this.$set(newFolder, '_editing', true);
                this.$set(newFolder, '_editingName', newFolder.file_name);

                // 将新文件夹添加到列表开头（文件夹通常显示在前面）
                this.fileManager.fileList.unshift(newFolder);
                this.fileManager.total += 1;

                // 下一个tick后聚焦输入框并全选文本
                this.$nextTick(() => {
                  const input = this.$refs['renameInput_' + newFolder.fid];
                  if (input) {
                    input.focus();
                    input.select();
                  }
                });

                this.showToast('文件夹创建成功');
              } else {
                this.showToast(response.data.message || '创建文件夹失败');
              }
            })
            .catch(error => {
              console.error('创建文件夹失败:', error);
              this.showToast('创建文件夹失败');
            });
        },
        // 添加一个检查分享链接状态的方法
        checkShareUrlStatus() {
          // 只在任务列表页面检查
          if (this.activeTab !== 'tasklist') return;

          // 遍历所有任务
          this.formData.tasklist.forEach((task, index) => {
            // 如果任务有分享链接且没有设置shareurl_ban
            if (task.shareurl && !task.shareurl_ban) {
              // 检查分享链接
              axios.get('/get_share_detail', { params: { shareurl: task.shareurl } })
                .then(response => {
                  const share_detail = response.data.data;
                  if (!response.data.success) {
                    // 检查是否是可恢复的网络错误或服务端临时错误
                    if (share_detail.error && (
                        share_detail.error.includes("request error") ||
                        share_detail.error.includes("inner error") ||
                        share_detail.error.includes("网络错误") ||
                        share_detail.error.includes("服务端错误") ||
                        share_detail.error.includes("临时错误"))) {
                      // 忽略可恢复的错误，不设置 shareurl_ban
                      console.log('检查分享链接时出现可恢复错误，忽略此错误:', share_detail.error);
                      return;
                    }
                    // 使用格式化函数处理其他错误信息
                    const formattedError = this.formatShareUrlBanMessage(share_detail.error);
                    if (formattedError) {
                      this.$set(task, "shareurl_ban", formattedError);
                    }
                  } else if (share_detail.list !== undefined && share_detail.list.length === 0) {
                    // 检查文件列表是否为空，确保列表存在且为空
                    this.$set(task, "shareurl_ban", "该分享已被删除，无法访问");
                  }
                })
                .catch(error => {
                  // 网络请求失败，忽略错误，不设置 shareurl_ban
                  console.log('检查分享链接状态时网络请求失败，忽略此错误:', error);
                });
            }
          });
        },
        toggleSidebar() {
          this.sidebarCollapsed = !this.sidebarCollapsed;
          // 保存侧边栏状态到本地存储
          localStorage.setItem('quarkAutoSave_sidebarCollapsed', this.sidebarCollapsed);
          
          // 如果当前在追剧日历页面且为海报模式，重新计算列数
          if (this.activeTab === 'calendar' && this.calendar.viewMode === 'poster') {
            // 延迟一点时间，等待DOM更新完成
            this.$nextTick(() => {
              this.updateWeekDates();
            });
          }
        },
        cleanTaskNameForSearch(taskName) {
          if (!taskName) return '';
          
          // 清理任务名称中的连续空格和特殊符号
          let cleanedName = taskName.replace(/\u3000/g, ' ').replace(/\t/g, ' ');
          cleanedName = cleanedName.replace(/\s+/g, ' ').trim();
          
          // 匹配常见的季数格式
          const seasonPatterns = [
            /^(.*?)[\s\.\-_]+S\d+$/i,  // 黑镜 - S07、折腰.S01、音你而来-S02
            /^(.*?)[\s\.\-_]+Season\s*\d+$/i,  // 黑镜 - Season 1
            /^(.*?)\s+S\d+$/i,  // 快乐的大人 S02
            /^(.*?)[\s\.\-_]+S\d+E\d+$/i,  // 处理 S01E01 格式
            /^(.*?)\s+第\s*\d+\s*季$/i,  // 处理 第N季 格式
            /^(.*?)[\s\.\-_]+第\s*\d+\s*季$/i,  // 处理 - 第N季 格式
            /^(.*?)\s+第[一二三四五六七八九十零]+季$/i,  // 处理 第一季、第二季 格式
            /^(.*?)[\s\.\-_]+第[一二三四五六七八九十零]+季$/i,  // 处理 - 第一季、- 第二季 格式
          ];
          
          for (const pattern of seasonPatterns) {
            const match = cleanedName.match(pattern);
            if (match) {
              let showName = match[1].trim();
              // 去除末尾可能残留的分隔符
              showName = showName.replace(/[\s\.\-_]+$/, '');
              return encodeURIComponent(showName);
            }
          }
          
          // 如果没有匹配到季数格式，执行原有的清理逻辑
          // 移除孤立的特殊符号
          cleanedName = cleanedName.replace(/\s*[-–—_]\s*$/, '');
          // 移除开头和结尾的特殊符号
          cleanedName = cleanedName.trim().replace(/^[-–—_\s]+|[-–—_\s]+$/g, '');
          
          return encodeURIComponent(cleanedName);
        },
        changeTab(tab) {
          this.activeTab = tab;
          // 在本地存储中保存当前标签页状态
          localStorage.setItem('quarkAutoSave_activeTab', tab);
          if (window.innerWidth <= 768) {
            $('#sidebarMenu').collapse('toggle')
          }

          // 当切换到任务列表标签时刷新任务最新信息和元数据
          if (tab === 'tasklist') {
            this.loadTaskLatestInfo();
            // 加载任务元数据信息，确保海报和元数据能热更新
            this.loadTasklistMetadata();
            // 启动任务列表的后台监听
            this.startTasklistAutoWatch();
            // 该分支的系统性加载不应触发“未保存”提示
            try {
              this.suppressConfigModifiedOnce = true;
              // 异步队列尾部再置一次，覆盖可能的深层变更
              this.$nextTick(() => {
                this.suppressConfigModifiedOnce = true;
                this.configModified = false;
              });
            } catch (e) {}
          } else if (this.activeTab === 'tasklist') {
            // 离开任务列表页面时停止后台监听
            this.stopTasklistAutoWatch();
          }

          // 当切换到历史记录标签时加载数据
          if (tab === 'history') {
            this.loadHistoryRecords();
            // 加载所有任务名称用于筛选
            this.loadAllTaskNames();
          }

          // 当切换到文件整理标签时加载文件列表
          if (tab === 'filemanager') {
            // 从本地存储中恢复文件管理器的分页大小设置
            const savedFileManagerPageSize = localStorage.getItem('quarkAutoSave_fileManagerPageSize');
            if (savedFileManagerPageSize) {
              this.fileManager.pageSize = savedFileManagerPageSize === 'all' ? 99999 : parseInt(savedFileManagerPageSize);
            }
            // 获取账号详细信息
            this.fetchAccountsDetail();
            this.loadFileListWithoutLoading(this.fileManager.currentFolder);
          }
          
          // 当切换到追剧日历标签时加载日历数据
          if (tab === 'calendar') {
            this.loadCalendarData();
            // 添加窗口大小变化监听器，自动调整列数
            this.addCalendarResizeListener();
          }
        },
        checkNewVersion() {
          // 移除本地版本中的v前缀
          const localVersionClean = this.version.replace(/^v/i, '');
          
          // 显示时也去掉v前缀
          this.versionTips = localVersionClean; 
          
          axios.get('https://api.github.com/repos/x1ao4/quark-auto-save-x/tags')
            .then(response => {
              const latestVersion = response.data[0].name; // GitHub的版本不带v，不需要replace
              
              // 使用处理后的版本号进行比较
              if (latestVersion !== localVersionClean) {
                this.versionTips += ` <sup><span class="badge badge-pill badge-danger">${latestVersion}</span></sup>`;
              }
            })
            .catch(error => {
              console.error('Error:', error);
            });
        },
        fetchData() {
          axios.get('/data')
            .then(response => {
              config_data = response.data.data
              // cookie兼容
              if (typeof config_data.cookie === 'string')
                config_data.cookie = [config_data.cookie];
              // 添加星期预设和执行周期模式
              config_data.tasklist = config_data.tasklist.map(task => {
                if (!task.hasOwnProperty('runweek')) {
                  task.runweek = [1, 2, 3, 4, 5, 6, 7];
                }
                // 确保execution_mode有默认值
                if (!task.hasOwnProperty('execution_mode')) {
                  task.execution_mode = config_data.execution_mode || 'manual';
                }
                
                // 格式化已有的警告信息
                if (task.shareurl_ban) {
                  const formattedError = this.formatShareUrlBanMessage(task.shareurl_ban);
                  if (formattedError) {
                    task.shareurl_ban = formattedError;
                  }
                }
                
                return task;
              });
              
              // 直接赋值，排序通过计算属性处理（属于系统性加载，抑制未保存提示）
              this.suppressConfigModifiedOnce = true;
              this.formData.tasklist = config_data.tasklist || [];

              // 获取所有任务父目录
              this.formData.tasklist.forEach(item => {
                parentDir = this.getParentDirectory(item.savepath)
                if (!this.taskDirs.includes(parentDir))
                  this.taskDirs.push(parentDir);
              });
              // 初始化新任务的插件配置，应用全局配置
              this.newTask.addition = { ...config_data.task_plugins_config_default };
              this.applyGlobalPluginConfig(this.newTask);
              // --- 新增：预加载追剧日历任务元数据，支持任务列表直接显示 season_counts / matched_status ---
              axios.get('/api/calendar/tasks')
                .then(res => {
                  if (res.data && res.data.success) {
                    this.calendar.tasks = res.data.data.tasks || [];
                    try {
                      this.calendar.taskMapByName = {};
                      (this.calendar.tasks || []).forEach(t => {
                        const key = (t.task_name || t.taskname || '').trim();
                        if (key) this.calendar.taskMapByName[key] = t;
                      });
                    } catch (e) {}
                    // 同步任务列表可用类型集合（复用日历的类型热更新逻辑）
                    try {
                      const rawTypes = (res.data.data && res.data.data.content_types) || [];
                      // 与日历相同的排序：tv、anime、variety、documentary、other（其他最后）
                      const known = ['tv','anime','variety','documentary'];
                      const uniq = Array.from(new Set(rawTypes.filter(Boolean)));
                      const ordered = uniq.sort((a,b) => {
                        const ia = known.indexOf(a); const ib = known.indexOf(b);
                        if (ia === -1 && ib === -1) return 0;
                        if (ia === -1) return 1;
                        if (ib === -1) return -1;
                        return ia - ib;
                      });
                      this.tasklist.contentTypes = ordered;
                      // 校验已选项有效性
                      const validSet = ['all', ...ordered];
                      if (!validSet.includes(this.tasklist.selectedType)) {
                        this.tasklist.selectedType = 'all';
                      }
                    } catch (e) { this.tasklist.contentTypes = []; }
                  } else {
                    this.calendar.tasks = [];
                    this.calendar.taskMapByName = {};
                    this.tasklist.contentTypes = [];
                  }
                })
                .catch(() => {
                  this.calendar.tasks = [];
                  this.calendar.taskMapByName = {};
                  this.tasklist.contentTypes = [];
                });
              // 确保source配置存在
              if (!config_data.source) {
                config_data.source = {};
              }
              if (!config_data.source.cloudsaver) {
                config_data.source.cloudsaver = {
                  server: "",
                  username: "",
                  password: "",
                  token: ""
                };
              }
              // 确保剧集识别模式存在
              if (!config_data.episode_patterns) {
                config_data.episode_patterns = [];
              }
              // 确保任务设置存在
              if (!config_data.task_settings) {
                config_data.task_settings = {
                  movie_save_path: "电影目录前缀/片名 (年份)",
                  tv_save_path: "剧集目录前缀/剧名 (年份)/剧名 - S季数",
                  anime_save_path: "动画目录前缀/剧名 (年份)/剧名 - S季数",
                  variety_save_path: "综艺目录前缀/剧名 (年份)/剧名 - S季数",
                  documentary_save_path: "纪录片目录前缀/剧名 (年份)/剧名 - S季数",
                  movie_naming_pattern: "^(.*)\\.([^.]+)",
                  movie_naming_replace: "片名 (年份).\\2",
                  tv_naming_rule: "剧名 - S季数E[]",
                  tv_ignore_extension: true,
                  subtitle_naming_rule: "zh",
                  subtitle_add_language_code: false,
                  auto_search_resources: "enabled"
                };
              }
              // 确保电影命名规则字段存在，只在字段不存在时设置默认值，允许用户设置为空字符串
              if (config_data.task_settings.movie_naming_pattern === undefined) {
                config_data.task_settings.movie_naming_pattern = "^(.*)\\.([^.]+)";
              }
              if (config_data.task_settings.movie_naming_replace === undefined) {
                config_data.task_settings.movie_naming_replace = "片名 (年份).\\2";
              }
              // 确保电视忽略后缀设置存在
              if (config_data.task_settings.tv_ignore_extension === undefined) {
                config_data.task_settings.tv_ignore_extension = true;
              }
              // 为字幕命名规则添加默认值（向后兼容）
              if (config_data.task_settings.subtitle_naming_rule === undefined) {
                config_data.task_settings.subtitle_naming_rule = "zh";
              }
              if (config_data.task_settings.subtitle_add_language_code === undefined) {
                config_data.task_settings.subtitle_add_language_code = false;
              }
              // 确保自动搜索资源设置存在
              if (!config_data.task_settings.auto_search_resources) {
                config_data.task_settings.auto_search_resources = "enabled";
              }
              // 确保按钮显示配置存在
              if (!config_data.button_display) {
                config_data.button_display = {
                  run_task: "always",
                  delete_task: "always",
                  refresh_plex: "always",
                  refresh_alist: "always",
                  season_counts: "always",
                  latest_update_date: "always",
                  task_progress: "always",
                  show_status: "always",
                  latest_transfer_file: "always",
                  today_update_indicator: "always"
                };
              }
              if (!config_data.button_display.season_counts) {
                config_data.button_display.season_counts = "always";
              }
              if (!config_data.button_display.task_progress) {
                config_data.button_display.task_progress = "always";
              }
              if (!config_data.button_display.show_status) {
                config_data.button_display.show_status = "always";
              }
              // 确保最近更新日期配置存在（向后兼容）
              if (!config_data.button_display.latest_update_date) {
                config_data.button_display.latest_update_date = "always";
              }
              // 确保最近转存文件配置存在（向后兼容）
              if (!config_data.button_display.latest_transfer_file) {
                config_data.button_display.latest_transfer_file = "always";
              }
              // 确保当日更新图标配置存在（向后兼容）
              if (!config_data.button_display.today_update_indicator) {
                config_data.button_display.today_update_indicator = "always";
              }
              // 确保显示顺序配置存在
              if (!config_data.button_display_order || !Array.isArray(config_data.button_display_order)) {
                config_data.button_display_order = [
                  "refresh_plex",
                  "refresh_alist",
                  "run_task",
                  "delete_task",
                  "latest_transfer_file",
                  "season_counts",
                  "latest_update_date",
                  "task_progress",
                  "show_status",
                  "today_update_indicator"
                ];
              }
              // 确保文件整理性能配置存在
              if (!config_data.file_performance) {
                config_data.file_performance = {
                  api_page_size: 200,
                  cache_expire_time: 30,
                  discovery_items_count: 30
                };
              } else {
                // 确保必要的字段存在，移除废弃的字段
                if (!config_data.file_performance.api_page_size) {
                  config_data.file_performance.api_page_size = 200;
                }
                if (!config_data.file_performance.cache_expire_time) {
                  config_data.file_performance.cache_expire_time = 30;
                }
                if (!config_data.file_performance.discovery_items_count) {
                  config_data.file_performance.discovery_items_count = 30;
                }
                // 移除废弃的字段
                delete config_data.file_performance.large_page_size;
                delete config_data.file_performance.cache_cleanup_interval;
              }
              // 确保性能设置存在并补默认值（单位：秒）
              if (!config_data.performance) {
                config_data.performance = { calendar_refresh_interval_seconds: 21600, aired_refresh_time: "00:00" };
              } else {
                if (config_data.performance.calendar_refresh_interval_seconds === undefined || config_data.performance.calendar_refresh_interval_seconds === null) {
                  config_data.performance.calendar_refresh_interval_seconds = 21600;
                }
                if (config_data.performance.aired_refresh_time === undefined || config_data.performance.aired_refresh_time === null) {
                  config_data.performance.aired_refresh_time = "00:00";
                }
              }
              // 确保execution_mode有默认值
              if (!config_data.execution_mode) {
                config_data.execution_mode = 'manual';
              }
              // 后端加载配置属于系统性赋值，不应触发未保存提示
              this.suppressConfigModifiedOnce = true;
              this.formData = config_data;
              
              setTimeout(() => {
                this.configModified = false;
              }, 100);
              this.configHasLoaded = true;

              // 加载任务最新信息（包括记录和文件）
              this.loadTaskLatestInfo();

              // 数据加载完成后检查分享链接状态
              if (this.activeTab === 'tasklist') {
                setTimeout(() => {
                  this.checkShareUrlStatus();
                }, 300);
              }
            })
            .catch(error => {
              // 错误处理
            });
        },
        handleKeyDown(event) {
          if (event.ctrlKey || event.metaKey) {
            if (event.keyCode === 83 || event.key === 's') {
              event.preventDefault();
              this.saveConfig();
            } else if (event.keyCode === 82 || event.key === 'r') {
              event.preventDefault();
              this.runScriptNow();
            }
          }
        },
        handleBeforeUnload(e) {
          if (this.configModified) {
            e.preventDefault();
            e.returnValue = '配置已修改但未保存，确定要离开吗？';
            return e.returnValue;
          }
        },
        saveConfig() {
          // 保存前处理每个任务的命名模式
          if (this.formData.tasklist && this.formData.tasklist.length > 0) {
            this.formData.tasklist.forEach(task => {
              // 如果是顺序命名模式，确保sequence_naming字段已正确设置
              if (task.use_sequence_naming && task.pattern && task.pattern.includes('{}')) {
                task.sequence_naming = task.pattern;
              }
              // 如果是剧集命名模式，确保episode_naming字段已正确设置
              if (task.use_episode_naming && task.pattern) {
                if (task.pattern === "[]" || task.pattern.includes('[]')) {
                  task.episode_naming = task.pattern;
                }
              }
              
              // 应用全局插件配置
              this.applyGlobalPluginConfig(task);
            });
          }
          
          axios.post('/update', this.formData)
            .then(response => {
              if (response.data.success) {
                this.configModified = false;
                // 使用Toast通知替代alert
                this.showToast(response.data.message);
                // 保存成功后更新用户信息
                this.fetchUserInfo();
                
                // 保存成功后触发热更新
                this.triggerTasklistHotUpdate();
              } else {
                // 错误信息仍然使用alert，确保用户看到
                alert(response.data.message);
              }
            })
            .catch(error => {
              // 错误处理
              alert("保存失败: " + (error.response?.data?.message || error.message || "未知错误"));
            });
        },
        addCookie() {
          this.formData.cookie.push("");
          // 添加cookie后，添加一个空的用户信息占位
          this.userInfoList.push({
            index: this.userInfoList.length,
            nickname: "",
            is_active: false
          });
        },
        removeCookie(index) {
          if (this.formData.cookie[index] == "" || confirm("确定要删除吗？")) {
            this.formData.cookie.splice(index, 1);
            // 删除对应的用户信息
            if (this.userInfoList.length > index) {
              this.userInfoList.splice(index, 1);
              // 更新剩余用户信息的索引
              this.userInfoList.forEach((user, idx) => {
                user.index = idx;
              });
            }
          }
        },
        addPush() {
          key = prompt("增加的键名", "");
          if (key != "" && key != null)
            this.$set(this.formData.push_config, key, "");
        },
        removePush(key) {
          if (confirm("确定要删除吗？"))
            this.$delete(this.formData.push_config, key);
        },
        addTask() {
          if (!this.formData.tasklist) 
            this.formData.tasklist = [];
          let newTask = { ...this.newTask };

          // 如果有上一个任务，继承保存路径和命名规则
          if (this.formData.tasklist.length > 0) {
            // 使用新的筛选逻辑：根据当前视图筛选条件获取对应类型中编号最大的任务
            const lastTask = this.getLastTaskByCurrentFilter();
            if (lastTask) {
              newTask.savepath = lastTask.savepath || "";
              newTask.pattern = lastTask.pattern || "";
              
              // 继承命名规则选择模式
              newTask.use_sequence_naming = lastTask.use_sequence_naming || false;
              newTask.use_episode_naming = lastTask.use_episode_naming || false;
              newTask.sequence_naming = lastTask.sequence_naming || "";
              newTask.episode_naming = lastTask.episode_naming || "";
              newTask.replace = lastTask.replace || "";
            }
          }

          // 根据当前视图筛选条件设置新任务的内容类型
          if (this.tasklist && this.tasklist.selectedType && this.tasklist.selectedType !== 'all') {
            // 确保 calendar_info 结构存在
            if (!newTask.calendar_info) {
              newTask.calendar_info = {};
            }
            if (!newTask.calendar_info.extracted) {
              newTask.calendar_info.extracted = {};
            }
            // 设置内容类型
            newTask.calendar_info.extracted.content_type = this.tasklist.selectedType;
            newTask.content_type = this.tasklist.selectedType;
          }

          // 应用全局插件配置到新任务
          this.applyGlobalPluginConfig(newTask);

          // 给新任务添加临时标识，用于后续定位
          newTask.__isNewlyAdded = true;
          this.formData.tasklist.push(newTask)
          const originalIndex = this.formData.tasklist.length - 1;
          
          // 清除之前任务的搜索记录，避免影响新任务
          this.smart_param.taskSuggestions = {
            success: false,
            data: []
          };
          
          // 等Vue更新DOM后，自动展开新添加的任务
          this.$nextTick(() => {
            // 找到新任务在排序后列表中的位置
            const sortedIndex = this.sortedTasklist.findIndex(task => task.__isNewlyAdded);
            if (sortedIndex !== -1) {
              // 展开新任务的编辑模块
              $(`#collapse_${sortedIndex}`).collapse('show');
              // 滚动到新任务位置
              this.scrollToX();
              // 移除临时标识
              delete newTask.__isNewlyAdded;
            }
          });
        },
        focusTaskname(index, task) {
          // 如果当前聚焦的任务不同于上一个任务，清除搜索记录
          if (this.smart_param.index !== index) {
            this.smart_param.taskSuggestions = {
              success: false,
              data: []
            };
          }
          
          this.smart_param.index = index
          this.smart_param.origin_savepath = task.savepath
          regex = new RegExp(`/${task.taskname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(/|$)`)
          if (task.savepath.includes('TASKNAME')) {
            this.smart_param.savepath = task.savepath;
          } else if (task.savepath.match(regex)) {
            this.smart_param.savepath = task.savepath.replace(task.taskname, 'TASKNAME');
          } else {
            this.smart_param.savepath = undefined;
          }
        },
        // Sxx季数信息和格式同步工具函数
        syncSeasonNumberAndFormat(sourceValue, targetValue) {
          if (!sourceValue || !targetValue) return targetValue;

          // 从源字符串中提取Sxx格式的季数和前缀格式
          const sourceMatch = sourceValue.match(/(.*?)([\s\.\-_]+)S(\d+)/i);
          if (!sourceMatch) return targetValue; // 源字符串没有Sxx，不同步

          const sourcePrefix = sourceMatch[1];
          const sourceSeparator = sourceMatch[2];
          const sourceSeasonNumber = sourceMatch[3];

          // 检查目标字符串是否包含Sxx格式
          const targetMatch = targetValue.match(/(.*?)([\s\.\-_]+)S(\d+)(.*)/i);
          if (!targetMatch) return targetValue; // 目标字符串没有Sxx，不同步

          const targetPrefix = targetMatch[1];
          const targetSeparator = targetMatch[2];
          const targetSeasonNumber = targetMatch[3];
          const targetSuffix = targetMatch[4]; // 保留后缀部分（如E[]、E{}等）

          // 检查前缀是否相似（去除空格后比较）
          const sourceCleanPrefix = sourcePrefix.replace(/\s+/g, '').toLowerCase();
          const targetCleanPrefix = targetPrefix.replace(/\s+/g, '').toLowerCase();

          // 如果前缀相似，则同步格式和季数
          if (sourceCleanPrefix === targetCleanPrefix ||
              sourceCleanPrefix.includes(targetCleanPrefix) ||
              targetCleanPrefix.includes(sourceCleanPrefix)) {

            // 同步格式：使用源的前缀+分隔符+季数+目标的后缀
            return sourcePrefix + sourceSeparator + 'S' + sourceSeasonNumber.padStart(2, '0') + targetSuffix;
          } else {
            // 前缀不相似，只同步季数，保持原格式
            return targetValue.replace(/S\d+/i, 'S' + sourceSeasonNumber.padStart(2, '0'));
          }
        },

        // 保存路径变化时的处理函数
        onSavepathChange(index, task) {
          // 同步Sxx和格式到命名规则
          if (task.pattern) {
            task.pattern = this.syncFormatFromSavepath(task.savepath, task.pattern);

            // 同步到相关的命名规则字段
            if (task.use_sequence_naming && task.sequence_naming) {
              task.sequence_naming = this.syncFormatFromSavepath(task.savepath, task.sequence_naming);
            }
            if (task.use_episode_naming && task.episode_naming) {
              task.episode_naming = this.syncFormatFromSavepath(task.savepath, task.episode_naming);
            }
          }
        },

        // 命名规则变化时的处理函数
        onPatternChange(index, task) {
          // 从命名规则同步格式到保存路径
          if (task.savepath) {
            task.savepath = this.syncFormatFromNamingToSavepath(task.pattern, task.savepath);
          }
        },

        // 从命名规则提取格式信息并应用到保存路径的辅助函数
        syncFormatFromNamingToSavepath(namingPattern, savepath) {
          if (!namingPattern || !savepath) return savepath;

          // 提取命名规则中的格式信息
          const patternMatch = namingPattern.match(/(.*?)([\s\.\-_]+)S(\d+)/i);
          if (!patternMatch) return savepath; // 命名规则没有Sxx格式，不同步

          const patternPrefix = patternMatch[1];
          const patternSeparator = patternMatch[2];
          const patternSeasonNumber = patternMatch[3];

          // 检查保存路径是否包含Sxx格式
          const pathParts = savepath.split('/');
          if (pathParts.length === 0) return savepath;

          const lastPart = pathParts[pathParts.length - 1];
          const pathMatch = lastPart.match(/(.*?)([\s\.\-_]+)S(\d+)/i);

          if (!pathMatch) return savepath; // 保存路径没有Sxx格式，不同步

          const pathPrefix = pathMatch[1];
          const pathSeparator = pathMatch[2];
          const pathSeasonNumber = pathMatch[3];

          // 检查前缀是否相似
          const patternCleanPrefix = patternPrefix.replace(/\s+/g, '').toLowerCase();
          const pathCleanPrefix = pathPrefix.replace(/\s+/g, '').toLowerCase();

          if (patternCleanPrefix === pathCleanPrefix ||
              patternCleanPrefix.includes(pathCleanPrefix) ||
              pathCleanPrefix.includes(patternCleanPrefix)) {

            // 同步格式：使用命名规则的格式更新保存路径
            const newLastPart = patternPrefix + patternSeparator + 'S' + patternSeasonNumber.padStart(2, '0');
            pathParts[pathParts.length - 1] = newLastPart;
            return pathParts.join('/');
          } else {
            // 前缀不相似，只同步季数
            const newLastPart = pathPrefix + pathSeparator + 'S' + patternSeasonNumber.padStart(2, '0');
            pathParts[pathParts.length - 1] = newLastPart;
            return pathParts.join('/');
          }
        },

        // 从保存路径提取格式信息并应用到命名规则的辅助函数
        syncFormatFromSavepath(savepath, namingPattern) {
          if (!savepath || !namingPattern) return namingPattern;

          // 从保存路径的最后一级提取格式信息
          const pathParts = savepath.split('/');
          if (pathParts.length === 0) return namingPattern;

          const lastPart = pathParts[pathParts.length - 1];
          const pathMatch = lastPart.match(/(.*?)([\s\.\-_]+)S(\d+)/i);

          if (!pathMatch) return namingPattern;

          const pathPrefix = pathMatch[1];
          const pathSeparator = pathMatch[2];
          const pathSeasonNumber = pathMatch[3];

          // 检查命名规则是否包含Sxx格式
          const namingMatch = namingPattern.match(/(.*?)([\s\.\-_]+)S(\d+)(.*)/i);
          if (!namingMatch) return namingPattern;

          const namingPrefix = namingMatch[1];
          const namingSeparator = namingMatch[2];
          const namingSeasonNumber = namingMatch[3];
          const namingSuffix = namingMatch[4]; // 保留后缀部分（如E[]、E{}等）

          // 检查前缀是否相似
          const pathCleanPrefix = pathPrefix.replace(/\s+/g, '').toLowerCase();
          const namingCleanPrefix = namingPrefix.replace(/\s+/g, '').toLowerCase();

          if (pathCleanPrefix === namingCleanPrefix ||
              pathCleanPrefix.includes(namingCleanPrefix) ||
              namingCleanPrefix.includes(pathCleanPrefix)) {

            // 同步格式：使用保存路径的格式更新命名规则
            return pathPrefix + pathSeparator + 'S' + pathSeasonNumber.padStart(2, '0') + namingSuffix;
          } else {
            // 前缀不相似，只同步季数
            return namingPattern.replace(/S\d+/i, 'S' + pathSeasonNumber.padStart(2, '0'));
          }
        },

        changeTaskname(index, task) {
          if (this.smart_param.searchTimer) {
            clearTimeout(this.smart_param.searchTimer);
          }
          this.smart_param.searchTimer = setTimeout(() => {
            this.searchSuggestions(index, task.taskname, 0);
          }, 1000);

          // 添加防抖机制，避免频繁的中间状态触发同步
          if (this.smart_param.syncTimer) {
            clearTimeout(this.smart_param.syncTimer);
          }
          this.smart_param.syncTimer = setTimeout(() => {
            this.performTasknameSync(index, task);
          }, 300); // 300ms 防抖延迟
        },

        performTasknameSync(index, task) {
          // 重新计算智能路径参数
          this.smart_param.index = index;
          this.smart_param.origin_savepath = task.savepath;
          const regex = new RegExp(`/${task.taskname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(/|$)`);
          if (task.savepath && task.savepath.includes('TASKNAME')) {
            this.smart_param.savepath = task.savepath;
          } else if (task.savepath && task.savepath.match(regex)) {
            this.smart_param.savepath = task.savepath.replace(task.taskname, 'TASKNAME');
          } else {
            this.smart_param.savepath = undefined;
          }

          // 清理任务名称中的连续空格和特殊符号
          const cleanTaskName = task.taskname.replace(/\s+/g, ' ').trim();

          // 检测是否是不完整的季数格式（如 S0, S, - S 等），如果是则不处理
          const incompleteSeasonPattern = /[\s\.\-_]+S\d{0,1}$/i;
          if (incompleteSeasonPattern.test(cleanTaskName)) {
            return; // 不处理不完整的季数格式
          }
          
          // 提取任务名称中的分隔符格式
          // 使用与cleanTaskNameForSearch相同的季数格式匹配模式
          let showName, seasonNumber, nameSeparator = ' - '; // 默认分隔符
          let isTVShow = false; // 标记是否为电视节目
          let separatorMatch = null;
          
          // 匹配常见的季数格式
          const seasonPatterns = [
            /^(.*?)([\s\.\-_]+)S(\d+)$/i,  // 黑镜 - S07、折腰.S01、音你而来-S02
            /^(.*?)([\s\.\-_]+)Season\s*(\d+)$/i,  // 黑镜 - Season 1
            /^(.*?)(\s+)S(\d+)$/i,  // 快乐的大人 S02
            /^(.*?)([\s\.\-_]+)S(\d+)E\d+$/i,  // 处理 S01E01 格式
            /^(.*?)(\s+)第\s*(\d+)\s*季$/i,  // 处理 第N季 格式
            /^(.*?)([\s\.\-_]+)第\s*(\d+)\s*季$/i,  // 处理 - 第N季 格式
            /^(.*?)(\s+)第([一二三四五六七八九十零]+)季$/i,  // 处理 第一季、第二季 格式
            /^(.*?)([\s\.\-_]+)第([一二三四五六七八九十零]+)季$/i,  // 处理 - 第一季、- 第二季 格式
          ];
          
          // 尝试匹配各种格式
          for (const pattern of seasonPatterns) {
            const match = cleanTaskName.match(pattern);
            if (match) {
              separatorMatch = match;
              // 根据不同的格式，确定季序号的位置
              if (match[3] && match[3].match(/[一二三四五六七八九十百千万零两]/)) {
                // 将中文数字转换为阿拉伯数字
                const arabicNumber = chineseToArabic(match[3]);
                seasonNumber = arabicNumber !== null ? String(arabicNumber) : '01';
              } else {
                seasonNumber = match[3] || '01';
              }
              break;
            }
          }
          
          // 判断保存路径是否包含电视节目相关的关键词
          if (task.savepath) {
            const tvKeywords = ["电视", "节目", "剧", "动漫", "动画", "番", "综艺", "真人秀", "TV", "Tv", "tv", "Series", "series", "Show", "show"];
            isTVShow = tvKeywords.some(keyword => task.savepath.includes(keyword));
          }

          // 当任务名称包含季信息时，也按电视节目处理
          const hasSeason = separatorMatch !== null;
          if (hasSeason) {
            isTVShow = true;
          }
          
          if (separatorMatch) {
            // 提取剧名（去除末尾空格和特殊符号）
            showName = separatorMatch[1].trim().replace(/[\s\.\-_]+$/, '');
            // 季序号已在上面的循环中提取
            // 提取实际使用的分隔符
            const rawSeparator = separatorMatch[2];
            
            // 处理特殊情况：如果是"黑镜 - 第五季"这种格式，需要保留完整的" - "分隔符
            // 检查原始字符串中剧名后面的分隔符
            const fullSeparator = cleanTaskName.substring(showName.length, cleanTaskName.length - (separatorMatch[3] ? separatorMatch[3].length + 1 : 0)).match(/^([\s\.\-_]+)/);
            
            if (fullSeparator && fullSeparator[1]) {
              // 使用完整的分隔符
              nameSeparator = fullSeparator[1];
            } else {
              // 规范化分隔符（如果是连续的空格，转为单个空格）
              nameSeparator = rawSeparator.replace(/\s+/g, ' ');
              // 如果只有一个点号，保留点号
              if (rawSeparator === '.') {
                nameSeparator = '.';
              }
              // 如果是单个空格，保留单个空格
              else if (rawSeparator === ' ') {
                nameSeparator = ' ';
              }
            }
            // 其他情况保持原样
          } else {
            // 如果没有匹配到季序号格式，处理删除季数信息的情况
            showName = cleanTaskName;

            // 检查保存路径中是否已有季数信息，如果有且是电视节目类型，则保留原有季数
            let preserveExistingSeason = false;

            if (task.savepath && isTVShow) {
              const existingSeasonMatch = task.savepath.match(/S(\d+)/i);

              if (existingSeasonMatch) {
                // 只有当季数是有效的（不是00或空）时才保留
                const existingSeasonNum = existingSeasonMatch[1];
                if (existingSeasonNum && existingSeasonNum !== '00' && existingSeasonNum !== '0') {
                  seasonNumber = existingSeasonNum;
                  preserveExistingSeason = true;

                  // 从保存路径中提取分隔符格式
                  const pathParts = task.savepath.split('/');
                  if (pathParts.length > 0) {
                    const lastPart = pathParts[pathParts.length - 1];
                    const separatorMatch = lastPart.match(/^(.*?)([\s\.\-_]+)S\d+/i);
                    if (separatorMatch) {
                      nameSeparator = separatorMatch[2];
                    }
                  }
                }
              }
            }

            // 如果没有保留现有季数，则使用默认值
            if (!preserveExistingSeason) {
              seasonNumber = '01';
            }
          }

          // 更新保存路径 - 无论是否使用智能路径，都确保倒数第二级目录更新
          if (task.savepath) {
            // 分割保存路径为各级目录
            const pathParts = task.savepath.split('/');

            if (pathParts.length >= 2) {
              // 如果智能路径已设置，使用原有逻辑更新最后一级
              if (this.smart_param.savepath) {
                // 更新最后一级目录，但保留前面的路径结构
                let replacementName;
                if (isTVShow) {
                  // 电视节目格式：剧名 + 分隔符 + S季序号
                  replacementName = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0');
                } else {
                  // 非电视节目直接使用任务名称
                  replacementName = cleanTaskName;
                }
                const newPath = this.smart_param.savepath.replace('TASKNAME', replacementName);
                const newPathParts = newPath.split('/');
                pathParts[pathParts.length - 1] = newPathParts[newPathParts.length - 1];
              } else {
                // 根据是否为电视节目决定处理方式
                if (isTVShow) {
                  // 电视节目格式：剧名 + 分隔符 + S季序号
                  const newLastPart = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0');
                  pathParts[pathParts.length - 1] = newLastPart;
                } else {
                  // 非电视节目直接使用任务名称
                  pathParts[pathParts.length - 1] = cleanTaskName;
                }
              }
              
              // 处理倒数第二级目录（剧名+年份）- 无论是否使用智能路径，都更新
              if (pathParts.length >= 3) {
                const parentDir = pathParts[pathParts.length - 2];
                // 检查倒数第二级是否符合 "名称 (年份)" 格式
                const yearMatch = parentDir.match(/^(.+?)\s*[\(（](\d{4})[\)）]$/);

                if (yearMatch) {
                  if (isTVShow) {
                    // 电视节目：更新倒数第二级为新的剧名 + 年份
                    const year = yearMatch[2];
                    pathParts[pathParts.length - 2] = showName + ' (' + year + ')';
                  } else {
                    // 非电视节目：去除倒数第二级目录
                    pathParts.splice(pathParts.length - 2, 1);
                  }
                }
              }

              // 更新保存路径
              task.savepath = pathParts.join('/');
            }
          }
          
          // 额外处理：检查保存路径最末级是否包含中文季序号，如果包含则转换为标准格式
          if (task.savepath) {
            const pathParts = task.savepath.split('/');
            if (pathParts.length > 0) {
              const lastPart = pathParts[pathParts.length - 1];
              // 匹配中文季序号格式
              const chineseSeasonMatch = lastPart.match(/^(.*?)([\s\.\-_]+)第([一二三四五六七八九十百千万零两]+)季$/);
              if (chineseSeasonMatch) {
                const showName = chineseSeasonMatch[1].trim();
                const separator = chineseSeasonMatch[2];
                const chineseSeason = chineseSeasonMatch[3];
                
                // 将中文数字转换为阿拉伯数字
                const arabicNumber = chineseToArabic(chineseSeason);
                const seasonNumber = arabicNumber !== null ? String(arabicNumber) : '1';
                
                // 更新最末级目录为标准格式
                pathParts[pathParts.length - 1] = showName + separator + 'S' + seasonNumber.padStart(2, '0');
                task.savepath = pathParts.join('/');
              }
              
              // 匹配阿拉伯数字季序号格式
              const arabicSeasonMatch = lastPart.match(/^(.*?)([\s\.\-_]+)第(\d+)季$/);
              if (arabicSeasonMatch) {
                const showName = arabicSeasonMatch[1].trim();
                const separator = arabicSeasonMatch[2];
                const seasonNumber = arabicSeasonMatch[3];
                
                // 更新最末级目录为标准格式
                pathParts[pathParts.length - 1] = showName + separator + 'S' + seasonNumber.padStart(2, '0');
                task.savepath = pathParts.join('/');
              }
            }
          }
          
          // 更新命名规则 - 始终使用当前任务名称中的信息
          if (task.pattern) {
            // 根据是否为电视节目决定处理方式
            if (isTVShow) {
              // 处理剧集命名模式
              if (task.use_episode_naming) {
                // 直接使用任务名称中提取的信息构建命名规则
                task.pattern = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0') + 'E[]';
                task.episode_naming = task.pattern;
              } 
              // 处理顺序命名模式
              else if (task.use_sequence_naming) {
                // 直接使用任务名称中提取的信息构建命名规则
                task.pattern = showName + nameSeparator + 'S' + seasonNumber.padStart(2, '0') + 'E{}';
                task.sequence_naming = task.pattern;
              }
            } else {
              // 非电视节目，仅在使用剧集或顺序命名时更新
              if (task.use_episode_naming) {
                // 如果原模式是纯E[]，直接使用任务名称
                if (task.pattern.trim() === 'E[]') {
                  task.pattern = cleanTaskName + 'E[]';
                } else if (task.pattern.includes('[]')) {
                  // 尝试保留原有格式，但更新剧名部分
                  const patternParts = task.pattern.split(/E\[\]/i);
                  if (patternParts.length > 0) {
                    task.pattern = cleanTaskName + 'E[]' + (patternParts[1] || '');
                  } else {
                    task.pattern = cleanTaskName + 'E[]';
                  }
                }
                task.episode_naming = task.pattern;
              } else if (task.use_sequence_naming) {
                // 同样处理顺序命名
                if (task.pattern.trim() === 'E{}' || task.pattern.trim() === '{}') {
                  task.pattern = cleanTaskName + 'E{}';
                } else if (task.pattern.includes('{}')) {
                  const patternParts = task.pattern.split(/E\{\}/i);
                  if (patternParts.length > 0) {
                    task.pattern = cleanTaskName + 'E{}' + (patternParts[1] || '');
                  } else {
                    task.pattern = cleanTaskName + 'E{}';
                  }
                }
                task.sequence_naming = task.pattern;
              }
            }
          }


        },
        removeTask(index) {
          if (!confirm("确定要删除任务 [#" + (index + 1) + ": " + this.formData.tasklist[index].taskname + "] 吗？")) return;
          const task = this.formData.tasklist[index];
          const taskName = task.taskname || task.task_name;
          // 一次性任务（skip_calendar）不做日历清理，直接删除并保存
          if (task.skip_calendar === true) {
            this.formData.tasklist.splice(index, 1);
            this.saveConfig();
            return;
          }
          // 非一次性任务：先请求后端清理（含数据库与海报文件），再删除任务并保存配置
          axios.post('/api/calendar/purge_by_task', { task_name: taskName })
            .then(() => {
              this.formData.tasklist.splice(index, 1);
              this.saveConfig();
            })
            .catch(() => {
              this.formData.tasklist.splice(index, 1);
              this.saveConfig();
            });
        },
        changeShareurl(task) {
          if (!task.shareurl)
            return;
          this.$set(task, "shareurl_ban", undefined);
          // 从URL中提取任务名
          try {
            const matches = decodeURIComponent(task.shareurl).match(/\/(\w{32})-([^\/]+)$/);
            if (matches) {
              task.taskname = task.taskname == "" ? matches[2] : task.taskname;
              task.savepath = task.savepath.replace(/TASKNAME/g, matches[2]);
            }
          } catch (e) {
            // 错误处理
          }
          // 从分享中提取任务名
          axios.get('/get_share_detail', { params: { shareurl: task.shareurl } })
            .then(response => {
              share_detail = response.data.data
              if (!response.data.success) {
                if (share_detail.error.includes("提取码")) {
                  const passcode = prompt("检查失败[" + share_detail.error + "]，请输入提取码：");
                  if (passcode != null) {
                    task.shareurl = task.shareurl.replace(/pan.quark.cn\/s\/(\w+)(\?pwd=\w*)*/, `pan.quark.cn/s/$1?pwd=${passcode}`);
                    this.changeShareurl(task);
                    return;
                  }
                }

                // 检查是否是可恢复的网络错误或服务端临时错误
                if (share_detail.error && (
                    share_detail.error.includes("request error") ||
                    share_detail.error.includes("inner error") ||
                    share_detail.error.includes("网络错误") ||
                    share_detail.error.includes("服务端错误") ||
                    share_detail.error.includes("临时错误"))) {
                  // 忽略可恢复的错误，不设置 shareurl_ban
                  console.log('修改分享链接时出现可恢复错误，忽略此错误:', share_detail.error);
                  return;
                }

                // 使用格式化函数处理其他错误信息
                const formattedError = this.formatShareUrlBanMessage(share_detail.error);
                if (formattedError) {
                  this.$set(task, "shareurl_ban", formattedError);
                }
              } else {
                // 检查文件列表是否为空
                if (share_detail.list !== undefined && share_detail.list.length === 0) {
                  this.$set(task, "shareurl_ban", "该分享已被删除，无法访问");
                } else {
                  task.taskname = task.taskname == "" ? share_detail.share.title : task.taskname;
                  task.savepath = task.savepath.replace(/TASKNAME/g, share_detail.share.title);
                  this.$set(task, "shareurl_ban", undefined);
                }
              }
            })
            .catch(error => {
              // 网络请求失败，忽略错误，不设置 shareurl_ban
              console.log('修改分享链接时网络请求失败，忽略此错误:', error);
            });
        },
        clearData(target) {
          this[target] = "";
        },
        async runScriptNow(task_index = null) {
          body = {};
          if (task_index != null) {
            task = { ...this.formData.tasklist[task_index] };
            delete task.runweek;
            delete task.enddate;
            body = {
              "tasklist": [task],
              "original_index": task_index + 1  // 添加原始索引，从1开始计数
            };
          } else if (this.configModified) {
            if (!confirm('配置已修改但未保存，是否继续运行？')) {
              return;
            }
          }
          $('#logModal').modal('toggle');
          this.modalLoading = true;
          this.run_log = '';
          try {
            // 1. 发送 POST 请求
            const response = await fetch(`/run_script_now`, {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json'
              },
              body: JSON.stringify(body)
            });
            if (!response.ok) {
              throw new Error(`HTTP error! Status: ${response.status}`);
            }
            // 2. 处理 SSE 流
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let partialData = '';
            while (true) {
              const { done, value } = await reader.read();
              if (done) {
                this.modalLoading = false;
                // 运行后刷新数据
                this.fetchData();
                break;
              }
              partialData += decoder.decode(value);
              const lines = partialData.split('\n').filter(line => line.trim() !== '');
              for (const line of lines) {
                if (line.startsWith('data:')) {
                  const eventData = line.substring(5).trim();
                  if (eventData === '[DONE]') {
                    this.modalLoading = false;
                    this.fetchData();
                    return;
                  }
                  this.run_log += eventData + '\n';
                  // 在更新 run_log 后将滚动条滚动到底部
                  this.$nextTick(() => {
                    const modalBody = document.querySelector('.modal-body');
                    modalBody.scrollTop = modalBody.scrollHeight;
                  });
                }
              }
              partialData = '';
            }
          } catch (error) {
            this.modalLoading = false;
          }
        },
        getParentDirectory(path) {
          parentDir = path.substring(0, path.lastIndexOf('/'))
          if (parentDir == "")
            parentDir = "/"
          return parentDir;
        },
        scrollToX(top = undefined) {
          if (top == undefined)
            top = document.documentElement.scrollHeight
          window.scrollTo({
            top: top,
            behavior: "smooth"
          });
        },
        getAvailablePlugins(plugins) {
          availablePlugins = {};
          const pluginsFlagsArray = this.plugin_flags.split(',');
          for (const pluginName in plugins) {
            if (!pluginsFlagsArray.includes(`-${pluginName}`)) {
              availablePlugins[pluginName] = plugins[pluginName];
            }
          }
          return availablePlugins;
        },
        toggleAllWeekdays(task) {
          if (task.runweek.length === 7) {
            task.runweek = [];
          } else {
            task.runweek = [1, 2, 3, 4, 5, 6, 7];
          }
        },
        onExecutionModeChange() {
          // 批量更新所有任务的execution_mode
          if (this.formData.tasklist && this.formData.tasklist.length > 0) {
            this.formData.tasklist.forEach(task => {
              if (!task.hasOwnProperty('execution_mode')) {
                this.$set(task, 'execution_mode', this.formData.execution_mode);
              } else {
                task.execution_mode = this.formData.execution_mode;
              }
            });
          }
        },
        searchSuggestions(index, taskname, deep = 1) {
          if (taskname.length < 2) {
            return;
          }
          // 开始搜索前清除之前的搜索结果
          this.smart_param.taskSuggestions = {
            success: false,
            data: []
          };
          this.smart_param.isSearching = true;
          this.smart_param.validating = false; // 重置验证状态
          this.smart_param.index = index;
          // 确保显示下拉菜单
          this.smart_param.showSuggestions = true;
          try {
            // 启动新的搜索会话，后续增量结果仅在会话一致时才渲染
            const sessionId = ++this.smart_param.searchSessionId;
            axios.get('/task_suggestions', {
              params: {
                q: taskname,
                d: deep
              }
            }).then(response => {
              // 接收到数据后，过滤无效链接
              if (sessionId !== this.smart_param.searchSessionId) return; // 旧会话结果忽略
              if (response.data.success && response.data.data && response.data.data.length > 0) {
                // 使用新增的方法验证链接有效性
                this.validateSearchResults(response.data, sessionId);
              } else {
                this.smart_param.taskSuggestions = response.data;
                // 重新确认设置为true
                this.smart_param.showSuggestions = true;
                this.smart_param.isSearching = false;
                this.smart_param.validating = false; // 重置验证状态
              }
            }).catch(error => {
              this.smart_param.isSearching = false;
              this.smart_param.validating = false; // 重置验证状态
              
            });
          } catch (e) {
            this.smart_param.taskSuggestions = {
              error: "网络异常"
            };
            this.smart_param.isSearching = false;
            this.smart_param.validating = false; // 重置验证状态
            
          }
        },
        // 添加新方法来验证搜索结果
        validateSearchResults(searchData, sessionId) {
          const invalidTerms = [
            "分享者用户封禁链接查看受限",
            "好友已取消了分享",
            "文件已被分享者删除",
            "文件夹为空",
            "分享地址已失效",
            "文件涉及违规内容"
          ];

          // 如果没有搜索结果，直接返回
          if (!searchData.data || searchData.data.length === 0) {
            this.smart_param.taskSuggestions = searchData;
            this.smart_param.isSearching = false;
            this.smart_param.validating = false;
            return;
          }
          
          // 设置验证状态
          this.smart_param.validating = true;
          
          // 初始化验证进度
          this.smart_param.validateProgress = {
            total: searchData.data.length,
            current: 0,
            valid: 0
          };

          // 初始化一个结果数组和待处理数组
          const validResults = [];
          const toProcess = [...searchData.data]; // 复制数组，保留原始顺序
          
          // 批量处理的大小，每批次最多处理5个链接
          const batchSize = 5;
          
          // 解析时间用于排序（降序：最新在前）
          const getItemTs = (item) => this.parsePublishTs(item && item.publish_date, item && item.source, item && item.pansou_source);
          
          // 处理单个链接的函数
          const processLink = (link) => {
            return new Promise((resolve) => {
              if (!link.shareurl) {
                // 没有分享链接，直接跳过
                
                resolve(null);
                return;
              }
              
              // 使用现有的get_share_detail接口验证链接
              axios.get('/get_share_detail', { 
                params: { shareurl: link.shareurl }
              })
              .then(response => {
                // 更新进度
                this.smart_param.validateProgress.current++;
                if (sessionId !== this.smart_param.searchSessionId) {
                  resolve(null);
                  return;
                }
                
                if (response.data.success) {
                  // 检查文件列表是否为空
                  const shareDetail = response.data.data;
                  if (shareDetail.list && shareDetail.list.length > 0) {
                    // 链接有效，添加到有效结果列表
                    this.smart_param.validateProgress.valid++;
                    
                    resolve(link);
                    return;
                  }
                } else {
                  // 检查是否包含已知的失效原因
                  const error = response.data.data.error || "";
                  let isInvalid = false;

                  // 如果是可恢复的网络错误或服务端临时错误，视为有效（网络问题，不是资源问题）
                  if (error.includes("request error") ||
                      error.includes("inner error") ||
                      error.includes("网络错误") ||
                      error.includes("服务端错误") ||
                      error.includes("临时错误")) {
                    this.smart_param.validateProgress.valid++;
                    resolve(link);
                    return;
                  }

                  for (const term of invalidTerms) {
                    if (error.includes(term)) {
                      isInvalid = true;
                      break;
                    }
                  }

                  // 如果不是已知的失效原因，保留该结果
                  if (!isInvalid) {
                    this.smart_param.validateProgress.valid++;
                    
                    resolve(link);
                    return;
                  }
                }
                
                // 链接无效
                
                resolve(null);
              })
              .catch(error => {
                // 验证出错，保守处理为有效
                this.smart_param.validateProgress.current++;
                this.smart_param.validateProgress.valid++;
                
                resolve(link);
              });
            });
          };
          
          // 修改processBatch函数，增加快速显示功能
          const processBatch = async () => {
            // 新会话已开始或已被取消（validating=false）则停止当前批次
            if (sessionId !== this.smart_param.searchSessionId || !this.smart_param.validating) return;
            // 取下一批处理
            const batch = toProcess.splice(0, batchSize);
            if (batch.length === 0) {
              // 所有批次处理完毕
              this.finishValidation(searchData, validResults);
              return;
            }
            
            // 并行处理当前批次
            const results = await Promise.all(batch.map(processLink));
            
            // 收集有效结果
            results.forEach(result => {
              if (result) {
                validResults.push(result);
              }
            });
            
            // 动态排序（按发布时间/日期降序）
            validResults.sort((a, b) => getItemTs(b) - getItemTs(a));
            
            // 每批次都增量更新到界面，显示当前有效数量并保持正在验证状态
            if (sessionId !== this.smart_param.searchSessionId || !this.smart_param.validating) return; // 渲染前再次检查会话
            this.smart_param.taskSuggestions = {
              success: searchData.success,
              source: searchData.source,
              data: [...validResults],
              message: `已找到${validResults.length}个有效链接，验证继续进行中...`
            };
            
            // 继续处理下一批
            processBatch();
          };
          
          // 开始批量处理前重置快速显示标记
          this.smart_param._hasShownInterimResults = false;
          processBatch();
          
          // 设置超时，避免永久等待
          setTimeout(() => {
            // 如果验证还在进行中，强制完成
            if (this.smart_param.validating && sessionId === this.smart_param.searchSessionId) {
              // 在收尾前立即取消会话，避免后续批次继续追加导致重复
              this.smart_param.validating = false;
              this.smart_param.searchSessionId++;
              // 将剩余未验证的链接添加到结果中
              const remaining = toProcess.filter(item => item.shareurl);
              validResults.push(...remaining);
              
              // 更新进度统计
              this.smart_param.validateProgress.current = this.smart_param.validateProgress.total;
              this.smart_param.validateProgress.valid = validResults.length;
              
              // 完成验证
              this.finishValidation(searchData, validResults);
              
            }
          }, 30000); // 30秒超时
        },
        // 完成验证并更新搜索结果
        finishValidation(searchData, validResults) {
          // 清除快速显示标记
          this.smart_param._hasShownInterimResults = false;
          
          // 结束前做一次排序，确保最终顺序正确
          const getItemTs = (item) => this.parsePublishTs(item && item.publish_date, item && item.source, item && item.pansou_source);
          validResults.sort((a, b) => getItemTs(b) - getItemTs(a));
          
          // 更新搜索结果
          const result = {
            success: searchData.success,
            source: searchData.source,
            data: validResults,
            message: validResults.length === 0 ? "未找到有效的分享链接" : searchData.message
          };
          
          
          this.smart_param.taskSuggestions = result;
          this.smart_param.isSearching = false;
          this.smart_param.validating = false;
          this.smart_param.showSuggestions = true;
        },
        selectSuggestion(index, suggestion) {
          // 不直接设置分享链接到输入框，只是打开文件选择模态框让用户浏览
          // 用户在模态框中导航后，最终的 this.fileSelect.shareurl 才是需要的地址

          // 记录当前选择的资源索引，用于连续浏览功能
          const resourceIndex = this.smart_param.taskSuggestions.data.findIndex(item => 
            item.shareurl === suggestion.shareurl && 
            item.taskname === suggestion.taskname
          );
          this.smart_param.currentResourceIndex = resourceIndex;

          // 确保显示的是选择需转存的文件夹界面，而不是命名预览界面
          this.fileSelect.previewRegex = false;
          this.fileSelect.selectDir = true;
          this.showShareSelect(index, suggestion.shareurl);
        },
        // 导航到上一个资源
        navigateToPreviousResource() {
          if (this.smart_param.currentResourceIndex > 0) {
            this.smart_param.currentResourceIndex--;
            const previousResource = this.smart_param.taskSuggestions.data[this.smart_param.currentResourceIndex];
            if (previousResource) {
              // 在切换资源时，重置展示相关状态，避免错误提示残留
              this.fileSelect.error = undefined;
              this.fileSelect.fileList = [];
              this.fileSelect.paths = [];
              // 确保处于“选择需转存的文件夹”界面
              this.fileSelect.previewRegex = false;
              this.fileSelect.selectDir = true;
              // 如果基础链接变化，清空stoken强制刷新
              try {
                const oldBase = this.getShareurl(this.fileSelect.shareurl);
                const newBase = this.getShareurl(previousResource.shareurl);
                if (oldBase !== newBase) {
                  this.fileSelect.stoken = "";
                }
              } catch (e) {
                this.fileSelect.stoken = "";
              }
              // 更新当前分享链接并重新加载内容
              this.fileSelect.shareurl = previousResource.shareurl;
              this.getShareDetail(0, 1);
            }
          }
        },
        // 导航到下一个资源
        navigateToNextResource() {
          if (this.smart_param.currentResourceIndex < this.smart_param.taskSuggestions.data.length - 1) {
            this.smart_param.currentResourceIndex++;
            const nextResource = this.smart_param.taskSuggestions.data[this.smart_param.currentResourceIndex];
            if (nextResource) {
              // 在切换资源时，重置展示相关状态，避免错误提示残留
              this.fileSelect.error = undefined;
              this.fileSelect.fileList = [];
              this.fileSelect.paths = [];
              // 确保处于“选择需转存的文件夹”界面
              this.fileSelect.previewRegex = false;
              this.fileSelect.selectDir = true;
              // 如果基础链接变化，清空stoken强制刷新
              try {
                const oldBase = this.getShareurl(this.fileSelect.shareurl);
                const newBase = this.getShareurl(nextResource.shareurl);
                if (oldBase !== newBase) {
                  this.fileSelect.stoken = "";
                }
              } catch (e) {
                this.fileSelect.stoken = "";
              }
              // 更新当前分享链接并重新加载内容
              this.fileSelect.shareurl = nextResource.shareurl;
              this.getShareDetail(0, 1);
            }
          }
        },
        // 判断当前资源是否在搜索结果中
        isCurrentResourceInSearchResults() {
          if (!this.fileSelect.selectShare || !this.smart_param.taskSuggestions.data || this.smart_param.taskSuggestions.data.length === 0) {
            return false;
          }
          
          // 如果currentResourceIndex有效，说明用户是从搜索结果进入的（包括下级目录）
          if (this.smart_param.currentResourceIndex >= 0 && this.smart_param.currentResourceIndex < this.smart_param.taskSuggestions.data.length) {
            return true;
          }
          
          // 通过资源ID来判断当前资源是否在搜索结果中
          const currentResourceId = this.getResourceIdFromUrl(this.fileSelect.shareurl);
          if (!currentResourceId) {
            return false;
          }
          
          const isInResults = this.smart_param.taskSuggestions.data.some(resource => {
            const resourceId = this.getResourceIdFromUrl(resource.shareurl);
            return resourceId && resourceId === currentResourceId;
          });
          
          return isInResults;
        },
        // 从分享链接中提取资源ID
        getResourceIdFromUrl(shareurl) {
          if (!shareurl) return null;
          
          try {
            // 夸克网盘分享链接格式：https://pan.quark.cn/s/资源ID
            const match = shareurl.match(/pan\.quark\.cn\/s\/([a-zA-Z0-9]+)/);
            return match ? match[1] : null;
          } catch (e) {
            return null;
          }
        },
        addMagicRegex() {
          const newKey = `$MAGIC_${Object.keys(this.formData.magic_regex).length + 1}`;
          this.$set(this.formData.magic_regex, newKey, { pattern: '', replace: '' });
        },
        updateMagicRegexKey(oldKey, newKey) {
          if (oldKey !== newKey) {
            if (this.formData.magic_regex[newKey]) {
              alert(`魔法名 [${newKey}] 已存在，请使用其他名称`);
              return;
            }
            this.$set(this.formData.magic_regex, newKey, this.formData.magic_regex[oldKey]);
            this.$delete(this.formData.magic_regex, oldKey);
          }
        },
        removeMagicRegex(key) {
          if (confirm(`确定要删除魔法匹配规则 [${key}] 吗？`)) {
            this.$delete(this.formData.magic_regex, key);
          }
        },
        deleteFileForSelect(fid, fname, isDir, deleteRecords = false) {
          let confirmMessage = deleteRecords 
            ? `确定要删除此项目及其关联记录吗？` 
            : `确定要删除此项目吗？`;
            
          if (!confirm(confirmMessage)) {
            return;
          }
          
          // 获取当前路径作为save_path参数
          let save_path = "";
          if (this.fileSelect && this.fileSelect.paths) {
            save_path = this.fileSelect.paths.map(item => item.name).join("/");
          }
          
          axios.post('/delete_file', { fid: fid, file_name: fname, delete_records: deleteRecords, save_path: save_path })
            .then(response => {
              if (response.data.code === 0) {
                // 从列表中移除文件
                this.fileSelect.fileList = this.fileSelect.fileList.filter(item => item.fid !== fid);
                
                // 如果文件在选中列表中，也从选中列表中移除
                if (this.fileSelect.selectedFiles.includes(fid)) {
                  this.fileSelect.selectedFiles = this.fileSelect.selectedFiles.filter(id => id !== fid);
                  
                  // 如果选中列表为空，重置最后选择的索引
                  if (this.fileSelect.selectedFiles.length === 0) {
                    this.fileSelect.lastSelectedFileIndex = -1;
                  }
                }
                
                // 显示成功消息，根据是否删除记录显示不同的消息
                if (deleteRecords) {
                  const deletedRecords = response.data.deleted_records || 0;
                  this.showToast(`成功删除 1 个项目${deletedRecords > 0 ? `及其关联的 ${deletedRecords} 条记录` : ''}`);
                } else {
                  this.showToast('成功删除 1 个项目');
                }
                
                // 如果同时删除了记录，无论当前在哪个页面，都刷新历史记录
                if (deleteRecords) {
                  this.loadHistoryRecords();
                }
                // 仅当删除了记录时，才触发任务/日历刷新（统计依赖记录）
                if (deleteRecords && (response.data.deleted_records || 0) > 0) {
                  (async () => {
                    try {
                      const latestRes = await axios.get('/task_latest_info');
                      if (latestRes.data && latestRes.data.success) {
                        const latestFiles = latestRes.data.data.latest_files || {};
                        this.taskLatestFiles = latestFiles;
                        this.taskLatestRecords = latestRes.data.data.latest_records || {};
                        this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(latestFiles);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    try {
                      const tasksResponse = await axios.get('/api/calendar/tasks');
                      if (tasksResponse.data && tasksResponse.data.success) {
                        this.calendar.tasks = tasksResponse.data.data.tasks || [];
                        this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                        const rawTypes = tasksResponse.data.data.content_types || [];
                        this.updateContentTypes(rawTypes);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    try {
                      await this.loadCalendarEpisodesLocal();
                      this.initializeCalendarDates();
                      await this.loadTodayUpdatesLocal();
                    } catch (e) {}
                  })();
                }
              } else {
                alert('删除失败: ' + response.data.message);
              }
            })
            .catch(error => {
              console.error('删除项目出错:', error);
              alert('删除项目出错: ' + (error.response?.data?.message || error.message));
            });
        },
        getSavepathDetail(params = 0, retryCount = 0, maxRetries = 1) {
          if (params === "" || params === null || params === undefined) {
            // 为空字符串时直接使用根目录fid
            params = 0;
          } else if (params.includes && params.includes('/')) {
            params = { path: params }
          } else {
            params = { fid: params }
          }
          this.modalLoading = true;

          // 根据模态框类型决定使用哪个账号
          // 任务配置相关的模态框始终使用主账号（索引0）
          // 文件整理页面的预览模态框和移动文件模态框使用选中的账号
          const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
          const accountIndex = (modalType === 'preview-filemanager' || modalType === 'move') ? this.fileManager.selectedAccountIndex : 0;

          // 添加账号索引参数
          if (typeof params === 'object' && params !== null) {
            params.account_index = accountIndex;
          } else {
            params = {
              fid: params,
              account_index: accountIndex
            };
          }
          axios.get('/get_savepath_detail', {
            params: params
          }).then(response => {
            this.fileSelect.fileList = response.data.data.list
            // 应用默认排序，但不改变当前的排序顺序
            this.manualSortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
            if (response.data.data.paths.length > 0) {
              this.fileSelect.paths = response.data.data.paths
            }
            this.modalLoading = false;
          }).catch(error => {
            // 增强版无感重试：添加缓存破坏参数，并仅在模态框仍打开时重试
            if (retryCount < maxRetries) {
              console.log(`获取文件夹列表失败，正在进行第 ${retryCount + 1} 次重试...`);
              setTimeout(() => {
                // 确保模态框仍处于打开状态，否则中止并结束loading
                const fileSelectModal = document.getElementById('fileSelectModal');
                if (!(fileSelectModal && fileSelectModal.classList.contains('show'))) {
                  this.modalLoading = false;
                  return;
                }
                // 为请求参数添加时间戳，避免潜在的缓存干扰
                if (typeof params === 'object' && params !== null) {
                  params._ts = Date.now();
                }
                this.getSavepathDetail(params, retryCount + 1, maxRetries);
              }, 600);
            } else {
              this.fileSelect.error = "获取文件夹列表失败，请关闭窗口再试一次";
              this.modalLoading = false;
            }
          });
        },
        showSavepathSelect(index) {
          this.fileSelect.selectShare = false;
          this.fileSelect.selectDir = true;
          this.fileSelect.previewRegex = false;
          this.fileSelect.error = undefined;
          this.fileSelect.fileList = [];
          this.fileSelect.paths = [];
          this.fileSelect.index = index;
          // 重置排序状态为默认值 - 选择需转存的文件夹模态框默认修改时间倒序
          this.fileSelect.sortBy = "updated_at";
          this.fileSelect.sortOrder = "desc";

          // 设置模态框类型为target（保存目标文件夹）
          document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'target');

          // 检查是否从创建任务模态框中打开，如果是则设置更高的z-index
          const createTaskModal = document.getElementById('createTaskModal');
          const fileSelectModal = document.getElementById('fileSelectModal');
          if (createTaskModal && createTaskModal.classList.contains('show')) {
            fileSelectModal.style.zIndex = '1060';
            // 查找并设置backdrop的z-index
            setTimeout(() => {
              const backdrop = document.querySelector('.modal-backdrop:last-child');
              if (backdrop) {
                backdrop.style.zIndex = '1055';
              }
            }, 50);
          } else {
            // 重置为默认值
            fileSelectModal.style.zIndex = '';
          }

          $('#fileSelectModal').modal('toggle');
          
          // 当savepath为空时，直接加载根目录
          // 处理创建任务模态框的情况（index为-1）
          const savepath = index === -1 ? this.createTask.taskData.savepath : this.formData.tasklist[index].savepath;
          if (!savepath || savepath === "") {
            this.getSavepathDetail(0); // 加载根目录
          } else {
            this.getSavepathDetail(savepath);
          }
        },
        getShareDetail(retryCount = 0, maxRetries = 1) {
          // 切换或重试前清理残留错误提示，避免覆盖新资源展示
          this.fileSelect.error = undefined;
          this.modalLoading = true;

          // 检查index是否有效，如果无效则使用默认值
          let regexConfig = {};
          if (this.fileSelect.index === -1) {
            // 创建任务模态框的情况
            const task = this.createTask.taskData;
            regexConfig = {
              pattern: task.pattern,
              replace: task.replace,
              taskname: task.taskname,
              filterwords: task.filterwords,
              magic_regex: this.formData.magic_regex,
              use_sequence_naming: task.use_sequence_naming,
              sequence_naming: task.sequence_naming,
              use_episode_naming: task.use_episode_naming,
              episode_naming: task.episode_naming,
              episode_patterns: this.formData.episode_patterns
            };
          } else if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
            const task = this.formData.tasklist[this.fileSelect.index];
            regexConfig = {
              pattern: task.pattern,
              replace: task.replace,
              taskname: task.taskname,
              filterwords: task.filterwords,
              magic_regex: this.formData.magic_regex,
              use_sequence_naming: task.use_sequence_naming,
              sequence_naming: task.sequence_naming,
              use_episode_naming: task.use_episode_naming,
              episode_naming: task.episode_naming,
              episode_patterns: this.formData.episode_patterns
            };
          } else {
            // 使用默认配置
            regexConfig = {
              pattern: '',
              replace: '',
              taskname: '',
              filterwords: '',
              magic_regex: this.formData.magic_regex,
              use_sequence_naming: false,
              sequence_naming: '',
              use_episode_naming: false,
              episode_naming: '',
              episode_patterns: this.formData.episode_patterns
            };
          }

          axios.post('/get_share_detail', {
            shareurl: this.fileSelect.shareurl,
            stoken: this.fileSelect.stoken,
            regex: regexConfig
          }).then(response => {
            if (response.data.success) {
              this.fileSelect.fileList = response.data.data.list;
              // 应用默认排序，但不改变当前的排序顺序
              this.manualSortFileList(this.fileSelect.sortBy, this.fileSelect.sortOrder);
              this.fileSelect.paths = response.data.data.paths;
              this.fileSelect.stoken = response.data.data.stoken;
              
              // 检查文件列表是否为空，仅在预览界面显示错误，不影响任务本身
              if (this.fileSelect.selectShare && (!this.fileSelect.fileList || this.fileSelect.fileList.length === 0)) {
                this.fileSelect.error = "该分享已被删除，无法访问";
                // 移除下面这段代码，避免在预览时直接设置任务的shareurl_ban
                // if (this.fileSelect.index !== null && this.formData.tasklist[this.fileSelect.index]) {
                //   this.$set(this.formData.tasklist[this.fileSelect.index], "shareurl_ban", "该分享已被删除，无法访问");
                // }
              }
              
              // 命名预览模式下，在数据加载完成后检查滚动条状态
              if (this.fileSelect.previewRegex) {
                this.$nextTick(() => {
                  this.checkPreviewScrollbar();
                });
              }
              // 成功加载后关闭加载状态
              this.modalLoading = false;
            } else {
              // 非法token等可恢复错误：执行一次无感自动重试
              const rawError = (response && response.data && response.data.data && response.data.data.error) ? response.data.data.error : '';
              const errorText = typeof rawError === 'string' ? rawError : String(rawError || '');
              const isIllegalToken = errorText.includes('非法token') || errorText.includes('Bad Parameter');
              const isRequestError = errorText.toLowerCase().includes('request error') ||
                                   errorText.toLowerCase().includes('inner error') ||
                                   errorText.toLowerCase().includes('网络错误') ||
                                   errorText.toLowerCase().includes('服务端错误') ||
                                   errorText.toLowerCase().includes('临时错误');
              if (isIllegalToken || isRequestError) {
                if (retryCount < maxRetries) {
                  console.log(`分享详情获取失败(${isIllegalToken ? '非法token' : 'request error'})，进行第 ${retryCount + 1} 次重试...`);
                  setTimeout(() => {
                    const fileSelectModal = document.getElementById('fileSelectModal');
                    if (!(fileSelectModal && fileSelectModal.classList.contains('show'))) {
                      this.modalLoading = false;
                      return;
                    }
                    // 清空stoken以强制刷新
                    this.fileSelect.stoken = "";
                    this.getShareDetail(retryCount + 1, maxRetries);
                  }, 600);
                  return; // 等待重试结果，不立刻结束loading
                }
                // 重试已用尽，给出统一提示
                this.fileSelect.error = "获取文件夹列表失败，请关闭窗口再试一次";
                this.modalLoading = false;
              } else {
                // 使用格式化函数处理不可恢复错误
                this.fileSelect.error = this.formatShareUrlBanMessage(rawError);
                this.modalLoading = false;
              }
            }
          }).catch(error => {
            // 增强版无感重试：清空 stoken 强制刷新令牌，并仅在模态框仍打开时重试
            if (retryCount < maxRetries) {
              console.log(`获取文件夹列表失败，正在进行第 ${retryCount + 1} 次重试...`);
              setTimeout(() => {
                const fileSelectModal = document.getElementById('fileSelectModal');
                if (!(fileSelectModal && fileSelectModal.classList.contains('show'))) {
                  this.modalLoading = false;
                  return;
                }
                // 清空stoken，促使后端重新获取有效的stoken
                this.fileSelect.stoken = "";
                this.getShareDetail(retryCount + 1, maxRetries);
              }, 600);
            } else {
              this.fileSelect.error = "获取文件夹列表失败，请关闭窗口再试一次";
              this.modalLoading = false;
            }
          });
        },
        showShareSelect(index, shareurl = null) {
          this.fileSelect.selectShare = true;
          this.fileSelect.fileList = [];
          this.fileSelect.paths = [];
          this.fileSelect.error = undefined;
          // 根据模式设置不同的默认排序
          if (this.fileSelect.previewRegex) {
            // 命名预览模式下使用重命名列倒序排序
            this.fileSelect.sortBy = "file_name_re";
            this.fileSelect.sortOrder = "desc";
          } else if (this.fileSelect.selectDir) {
            // 选择需转存的文件夹模态框：默认修改时间倒序
            this.fileSelect.sortBy = "updated_at";
            this.fileSelect.sortOrder = "desc";
          } else {
            // 选择起始文件模态框：使用文件名倒序排序（使用全局文件排序函数）
            this.fileSelect.sortBy = "file_name";
            this.fileSelect.sortOrder = "desc";
          }
          // 处理创建任务模态框的情况（index为-1）
          const currentShareurl = index === -1 ? this.createTask.taskData.shareurl : this.formData.tasklist[index].shareurl;
          if (this.getShareurl(this.fileSelect.shareurl) != this.getShareurl(currentShareurl)) {
            this.fileSelect.stoken = "";
          }
          this.fileSelect.shareurl = shareurl || currentShareurl;
          this.fileSelect.index = index;
          
          // 检查当前分享链接是否在搜索结果中，如果在则设置正确的索引
          const finalShareurl = shareurl || currentShareurl;
          if (this.smart_param.taskSuggestions.data && this.smart_param.taskSuggestions.data.length > 0) {
            const currentResourceId = this.getResourceIdFromUrl(finalShareurl);
            let resourceIndex = -1;
            
            if (currentResourceId) {
              resourceIndex = this.smart_param.taskSuggestions.data.findIndex(item => {
                const itemResourceId = this.getResourceIdFromUrl(item.shareurl);
                return itemResourceId && itemResourceId === currentResourceId;
              });
            }
            
            this.smart_param.currentResourceIndex = resourceIndex;
          } else {
            // 如果没有搜索结果，重置currentResourceIndex
            this.smart_param.currentResourceIndex = -1;
          }
          
          // 根据不同条件设置模态框类型
          if (this.fileSelect.previewRegex) {
            document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'preview');
          } else if (this.fileSelect.selectDir) {
            document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'source');
          } else {
            document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'start-file');
          }

          // 检查是否从创建任务模态框中打开，如果是则设置更高的z-index
          const createTaskModal = document.getElementById('createTaskModal');
          const fileSelectModal = document.getElementById('fileSelectModal');
          if (createTaskModal && createTaskModal.classList.contains('show')) {
            fileSelectModal.style.zIndex = '1060';
            // 查找并设置backdrop的z-index
            setTimeout(() => {
              const backdrop = document.querySelector('.modal-backdrop:last-child');
              if (backdrop) {
                backdrop.style.zIndex = '1055';
              }
            }, 50);
          } else {
            // 重置为默认值
            fileSelectModal.style.zIndex = '';
          }

          $('#fileSelectModal').modal('toggle');
          // 调用getShareDetail时不传递任何参数，使用默认的重试机制
          this.getShareDetail(0, 1);
          
          // 命名预览模式下，确保在模态框显示后检查滚动条状态
          if (this.fileSelect.previewRegex) {
            this.$nextTick(() => {
              this.checkPreviewScrollbar();
            });
          }
        },
        navigateTo(fid, name) {
          // 新增：命名预览模式下（文件整理页面），切换目录时刷新预览
          if (this.fileSelect.previewRegex && this.fileSelect.index === -1) {
            // 更新fileManager当前目录
            this.fileManager.currentFolder = fid;
            // 更新面包屑路径
            if (fid == "0") {
              this.fileSelect.paths = [];
            } else {
              const index = this.fileSelect.paths.findIndex(item => item.fid === fid);
              if (index !== -1) {
                this.fileSelect.paths = this.fileSelect.paths.slice(0, index + 1);
              } else {
                this.fileSelect.paths.push({ fid: fid, name: name });
              }
            }
            // 直接刷新命名预览（不关闭模态框）
            this.showFileManagerNamingPreview(fid);
            return;
          }
          path = { fid: fid, name: name }
          if (this.fileSelect.selectShare) {
            this.fileSelect.shareurl = this.getShareurl(this.fileSelect.shareurl, path);
            // 使用重试机制调用getShareDetail
            this.getShareDetail(0, 1);
          } else {
            if (fid == "0") {
              this.fileSelect.paths = []
            } else {
              index = this.fileSelect.paths.findIndex(item => item.fid === fid);
              if (index !== -1) {
                this.fileSelect.paths = this.fileSelect.paths.slice(0, index + 1)
              } else {
                this.fileSelect.paths.push({ fid: fid, name: name })
              }
            }
            // 使用重试机制调用getSavepathDetail
            this.getSavepathDetail(fid, 0, 1);
          }
        },
        selectCurrentFolder(addTaskname = false) {
          if (this.fileSelect.moveMode) {
            // 移动文件模式
            this.moveFilesToCurrentFolder();
          } else if (this.fileSelect.selectShare) {
            // 检查index是否有效
            if (this.fileSelect.index === -1) {
              // 创建任务模态框的情况
              this.createTask.taskData.shareurl_ban = undefined;
              // 如果是分享文件夹且文件列表为空，则设置shareurl_ban
              if (!this.fileSelect.fileList || this.fileSelect.fileList.length === 0) {
                this.$set(this.createTask.taskData, "shareurl_ban", "该分享已被删除，无法访问");
              }
              // 只有在用户点击"转存当前文件夹"时才更新分享链接
              // 使用用户最终访问的分享链接地址（包含用户导航后的路径）
              this.createTask.taskData.shareurl = this.fileSelect.shareurl;
            } else if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
              this.formData.tasklist[this.fileSelect.index].shareurl_ban = undefined;
              // 如果是分享文件夹且文件列表为空，则设置shareurl_ban
              if (!this.fileSelect.fileList || this.fileSelect.fileList.length === 0) {
                this.$set(this.formData.tasklist[this.fileSelect.index], "shareurl_ban", "该分享已被删除，无法访问");
              }
              // 只有在用户点击"转存当前文件夹"时才更新分享链接
              // 使用用户最终访问的分享链接地址（包含用户导航后的路径）
              this.formData.tasklist[this.fileSelect.index].shareurl = this.fileSelect.shareurl;
            }
          } else {
            // 检查index是否有效
            if (this.fileSelect.index === -1) {
              // 创建任务模态框的情况
              this.createTask.taskData.savepath = this.fileSelect.paths.map(item => item.name).join("/");
              if (addTaskname) {
                // 使用自定义文件夹路径
                const customFolderPath = this.generateCustomFolderPath(this.createTask.taskData);
                this.createTask.taskData.savepath += "/" + customFolderPath;
              }
            } else if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
              // 去掉前导斜杠，避免双斜杠问题
              this.formData.tasklist[this.fileSelect.index].savepath = this.fileSelect.paths.map(item => item.name).join("/");
              if (addTaskname) {
                this.formData.tasklist[this.fileSelect.index].savepath += "/" + this.formData.tasklist[this.fileSelect.index].taskname
              }
            }
          }
          if (!this.fileSelect.moveMode) {
            $('#fileSelectModal').modal('hide')
            // 用户已确认选择，自动关闭搜索结果下拉菜单
            this.smart_param.showSuggestions = false;
          }
        },
        // 移动文件到当前文件夹
        moveFilesToCurrentFolder() {
          if (!this.fileSelect.moveFileIds || this.fileSelect.moveFileIds.length === 0) {
            alert('没有选择要移动的文件');
            return;
          }

          // 获取目标文件夹ID
          const targetFolderId = this.fileSelect.paths.length > 0
            ? this.fileSelect.paths[this.fileSelect.paths.length - 1].fid
            : "0";

          // 记录源目录ID（文件原本所在的目录）
          const sourceFolderId = this.fileManager.currentFolder || "0";

          const fileCount = this.fileSelect.moveFileIds.length;
          const confirmMessage = fileCount === 1
            ? '确定要移动此文件吗？'
            : `确定要移动选中的 ${fileCount} 个文件吗？`;

          if (!confirm(confirmMessage)) {
            return;
          }

          // 调用移动文件API
          axios.post('/move_file', {
            file_ids: this.fileSelect.moveFileIds,
            target_folder_id: targetFolderId,
            account_index: this.fileManager.selectedAccountIndex
          })
          .then(response => {
            if (response.data.success) {
              this.showToast(`成功移动 ${response.data.moved_count} 个文件`);

              // 关闭模态框
              $('#fileSelectModal').modal('hide');

              // 重置移动模式相关参数
              this.fileSelect.moveMode = false;
              this.fileSelect.moveFileIds = [];
              this.fileSelect.index = null; // 重置index避免后续访问undefined

              // 清空选中的文件
              this.fileManager.selectedFiles = [];

              // 延迟刷新以确保后端处理完成
              setTimeout(() => {
                // 刷新当前页面显示（源目录）
                this.refreshCurrentFolderCache();

                // 如果目标目录不同于源目录，也刷新目标目录的缓存
                if (targetFolderId !== sourceFolderId) {
                  this.refreshFolderCache(targetFolderId); // 刷新目标目录缓存
                }

                // 刷新源目录的上级目录（更新源文件夹的项目数量）
                const sourceParentFolderId = this.getParentFolderId(sourceFolderId);
                if (sourceParentFolderId !== null) {
                  this.refreshFolderCache(sourceParentFolderId);
                }

                // 刷新目标目录的上级目录（更新目标文件夹的项目数量）
                if (targetFolderId !== sourceFolderId) {
                  const targetParentFolderId = this.getParentFolderId(targetFolderId);
                  if (targetParentFolderId !== null && targetParentFolderId !== sourceParentFolderId) {
                    this.refreshFolderCache(targetParentFolderId);
                  }
                }
              }, 500); // 延迟500ms确保后端处理完成
            } else {
              const errorMessage = response.data.message || '移动失败';
              // 对于"不能移动至相同目录"错误使用Toast通知
              if (errorMessage.includes('不能移动至相同目录')) {
                this.showToast('不能移动至相同目录');
              } else {
                this.showToast(errorMessage);
              }
            }
          })
          .catch(error => {
            console.error('移动文件失败:', error);
            this.showToast('移动文件失败');
          });
        },
        selectStartFid(fid) {
          // 检查index是否有效
          if (this.fileSelect.index === -1) {
            // 创建任务模态框的情况
            Vue.set(this.createTask.taskData, 'startfid', fid);
          } else if (this.fileSelect.index !== null && this.fileSelect.index >= 0 && this.formData.tasklist[this.fileSelect.index]) {
            Vue.set(this.formData.tasklist[this.fileSelect.index], 'startfid', fid);
          }
          $('#fileSelectModal').modal('hide')
        },
        getShareurl(shareurl, path = {}) {
          if (path == {} || path.fid == 0) {
            shareurl = shareurl.match(`.*s/[a-z0-9]+`)[0]
          } else if (shareurl.includes(path.fid)) {
            shareurl = shareurl.match(`.*/${path.fid}[^\/]*`)[0]
          } else if (shareurl.includes('#/list/share')) {
            shareurl = `${shareurl}/${path.fid}-${path.name}`
          } else {
            shareurl = `${shareurl}#/list/share/${path.fid}-${path.name}`
          }
          return shareurl;
        },
        detectNamingMode(task) {
          // 检测是否为顺序命名模式或剧集命名模式
          let isSequenceNaming = false;
          let isEpisodeNaming = false;
          const currentValue = task.pattern;

          if (currentValue !== undefined) {
            isSequenceNaming = currentValue.includes('{}');
            isEpisodeNaming = !isSequenceNaming && currentValue.includes('[]');
          }

          // 只要不是正则命名，强制清空 replace
          if (isSequenceNaming || isEpisodeNaming) {
            task.replace = '';
          }

          if (isSequenceNaming) {
            // 切换到顺序命名模式
            if (!task.use_sequence_naming) {
              task._pattern_backup = task.pattern;
              task._replace_backup = task.replace;
              task.use_sequence_naming = true;
              task.use_episode_naming = false;
            }
            task.sequence_naming = task.pattern;
          } else if (isEpisodeNaming) {
            // 切换到剧集命名模式
            if (!task.use_episode_naming) {
              task._pattern_backup = task.pattern;
              task._replace_backup = task.replace;
              task.use_episode_naming = true;
              task.use_sequence_naming = false;
            }
            task.episode_naming = task.pattern;
          } else {
            // 切换回正则命名模式
            if (task.use_sequence_naming || task.use_episode_naming) {
              task.use_sequence_naming = false;
              task.use_episode_naming = false;
              task.sequence_naming = null;
              task.episode_naming = null;
            }
          }

          // 保存当前值，用于下次比较
          task._lastPatternValue = currentValue;
          
          // 强制Vue更新视图
          this.$forceUpdate();
        },
        // 增加剧集识别模式
        addEpisodePattern() {
          // 此方法保留但不再使用
        },
        
        // 移除剧集识别模式
        removeEpisodePattern(index) {
          // 此方法保留但不再使用
        },
        loadHistoryRecords() {
          // 构建请求参数
          const params = {
            sort_by: this.historyParams.sortBy || "transfer_time",
            order: this.historyParams.order || "desc",
            page_size: parseInt(this.historyParams.page_size || 15),
            page: this.historyParams.page || 1
          };
          
          // 添加筛选条件
          if (this.historyTaskSelected) {
            params.task_name = this.historyTaskSelected;
          }
          
          if (this.historyNameFilter) {
            params.keyword = this.historyNameFilter;
          }
          
          // 判断筛选条件是否变化，只有变化时才重置页码
          const isFilterChanged = 
            (this._lastTaskFilter !== this.historyTaskSelected) || 
            (this._lastNameFilter !== this.historyNameFilter);
          
          if (isFilterChanged) {
            // 筛选条件变化时重置为第一页
            params.page = 1;
            this.gotoPage = 1;
          }
          
          // 记录当前筛选条件
          this._lastTaskFilter = this.historyTaskSelected;
          this._lastNameFilter = this.historyNameFilter;
          
          // 更新当前参数
          this.historyParams = { 
            sortBy: params.sort_by,
            order: params.order,
            page_size: params.page_size,
            page: params.page
          };
          
          axios.get('/history_records', {
            params: params
          }).then(response => {
            if (response.data.success) {
              this.history = response.data.data;

              // 确保页码信息正确反映在UI上
              if (this.history.pagination) {
                this.totalPages = parseInt(this.history.pagination.total_pages);
              }
            } else {
              console.error('Error:', response.data.message);
            }
            this.history.hasLoaded = true;
          }).catch(error => {
            console.error('Error loading history records:', error);
            this.history.hasLoaded = true;
          });
        },
        
        sortHistory(field) {
          // 更新排序参数
          if (this.historyParams.sortBy === field) {
            this.historyParams.order = this.historyParams.order === 'asc' ? 'desc' : 'asc';
          } else {
            this.historyParams.sortBy = field;
            // 保持默认排序顺序为'desc'，与后端一致
            this.historyParams.order = 'desc';
          }
          
          // 构建请求参数
          const params = {
            sort_by: this.historyParams.sortBy,
            order: this.historyParams.order,
            page_size: parseInt(this.historyParams.page_size || 15),
            page: this.historyParams.page || 1
          };
          
          // 添加筛选条件
          if (this.historyTaskSelected) {
            params.task_name = this.historyTaskSelected;
          }
          
          if (this.historyNameFilter) {
            params.keyword = this.historyNameFilter;
          }
          
          // 重新加载数据，使用当前页和当前设置的排序方式
          axios.get('/history_records', { params })
            .then(response => {
              if (response.data.success) {
                this.history = response.data.data;
                // 确保总页数更新
                if (this.history.pagination) {
                  this.totalPages = parseInt(this.history.pagination.total_pages);
                }
              } else {
                console.error('排序失败:', response.data.message);
              }
            })
            .catch(error => {
              console.error('排序请求错误:', error);
            });
        },
        
        changePage(page) {
          if (page < 1) page = 1;
          if (page > this.totalPages) page = this.totalPages;
          
          // 更新当前页码
          this.historyParams.page = page;
          this.gotoPage = page;
          
          // 直接请求新页的数据
          const params = {
            sort_by: this.historyParams.sortBy,
            order: this.historyParams.order,
            page_size: parseInt(this.historyParams.page_size),
            page: page
          };
          
          if (this.historyTaskSelected) {
            params.task_name = this.historyTaskSelected;
          }
          
          if (this.historyNameFilter) {
            params.keyword = this.historyNameFilter;
          }
          
          axios.get('/history_records', { params })
            .then(response => {
              if (response.data.success) {
                this.history = response.data.data;
                
                // 确保页码信息正确反映在UI上
                if (this.history.pagination) {
                  this.totalPages = parseInt(this.history.pagination.total_pages);
                }
              } else {
                console.error('Error:', response.data.message);
              }
            })
            .catch(error => {
              console.error('Error changing page:', error);
            });
        },
        
        changePageSize() {
          // 重置为第一页
          this.historyParams.page = 1;
          this.gotoPage = 1;
          
          // 构建参数
          const params = {
            sort_by: this.historyParams.sortBy,
            order: this.historyParams.order,
            page_size: parseInt(this.historyParams.page_size),
            page: 1
          };
          
          if (this.historyTaskSelected) {
            params.task_name = this.historyTaskSelected;
          }
          
          if (this.historyNameFilter) {
            params.keyword = this.historyNameFilter;
          }
          
          // 重新加载数据
          axios.get('/history_records', { params })
            .then(response => {
              if (response.data.success) {
                this.history = response.data.data;
                
                // 确保页码信息正确反映在UI上
                if (this.history.pagination) {
                  this.totalPages = parseInt(this.history.pagination.total_pages);
                }
              } else {
                console.error('Error:', response.data.message);
              }
            })
            .catch(error => {
              console.error('Error changing page size:', error);
            });
        },
        
        changePageSizeTo(size) {
          this.historyParams.page_size = size === 'all' ? 99999 : size;
          // 保存用户设置的每页记录数到本地存储
          localStorage.setItem('quarkAutoSave_pageSize', size.toString());
          this.changePageSize();
        },
        
        getVisiblePageNumbers() {
          const current = parseInt(this.historyParams.page) || 1;
          const total = parseInt(this.totalPages) || 1;
          // 根据屏幕宽度动态调整显示的页码数：移动端显示较少页码，桌面端显示较多页码
          const isMobile = window.innerWidth <= 768;
          const delta = isMobile ? 1 : 2; // 移动端左右各显示1个页码，桌面端左右各显示2个页码
          
          // 处理特殊情况
          if (total <= 1) return [];
          
          let range = [];
          
          // 移动端简化逻辑：只显示当前页附近的少数页码
          if (isMobile) {
            // 移动端：最多显示4个中间页码按钮（不包括第1页和最后1页），总共5个页码
            if (current <= 4) {
              // 当前页在前面时，显示 1, 2, 3, 4
              for (let i = 2; i <= Math.min(4, total - 1); i++) {
                range.push(i);
              }
            } else if (current >= total - 3) {
              // 当前页在后面时，显示倒数几页
              for (let i = Math.max(2, total - 3); i <= total - 1; i++) {
                range.push(i);
              }
            } else {
              // 当前页在中间时，显示当前页前后各1页，总共3个中间页码
              range.push(current - 1);
              range.push(current);
              range.push(current + 1);
            }
          } else {
            // 桌面端保持原有逻辑
            let rangeStart = Math.max(2, current - delta);
            let rangeEnd = Math.min(total - 1, current + delta);
            
            // 调整范围，确保显示足够的页码
            if (rangeEnd - rangeStart < delta * 2) {
              if (current - rangeStart < delta) {
                // 当前页靠近开始，扩展结束范围
                rangeEnd = Math.min(total - 1, rangeStart + delta * 2);
              } else {
                // 当前页靠近结束，扩展开始范围
                rangeStart = Math.max(2, rangeEnd - delta * 2);
              }
            }
            
            // 生成页码数组
            for (let i = rangeStart; i <= rangeEnd; i++) {
              range.push(i);
            }
          }
          
          return range;
        },
        isTextTruncated(text, recordIndex, field) {
          if (!this.filteredHistoryRecords[recordIndex]) return false;
          
          // 确保文本是字符串类型
          const str = String(text || '');
          
          // 先检查文本是否有足够长度，太短的文本肯定不需要展开
          if (str.length <= 20) return false;
          
          // 从记录的_isOverflowing属性中获取溢出状态
          const record = this.filteredHistoryRecords[recordIndex];
          
          // 如果已经展开了，那么我们认为它可能是需要截断的
          if (record._expandedFields && record._expandedFields.includes(field)) {
            return true;
          }
          
          return record._isOverflowing && record._isOverflowing[field];
        },
        toggleExpand(index, field, event) {
          // 阻止事件冒泡，避免触发行选择
          if (event) {
            event.stopPropagation();
          }
          
          // 获取当前记录
          const record = this.filteredHistoryRecords[index];
          // 初始化_expandedFields属性（如果不存在）
          if (!record._expandedFields) {
            this.$set(record, '_expandedFields', []);
          }
          
          // 检查字段是否已经展开
          const fieldIndex = record._expandedFields.indexOf(field);
          const isExpanding = fieldIndex === -1;
          
          // 所有可能需要展开的字段
          const allFields = ['task_name', 'original_name', 'renamed_to'];
          
          if (isExpanding) {
            // 如果是展开操作，展开该行所有超长字段
            allFields.forEach(fieldName => {
              // 只有超长的字段才需要展开
              if (this.isTextTruncated(record[fieldName], index, fieldName) && 
                  !record._expandedFields.includes(fieldName)) {
                record._expandedFields.push(fieldName);
              }
            });
          } else {
            // 如果是收起操作，收起该行所有字段
            record._expandedFields = [];
          }
          
          // 在下一个DOM更新周期中强制重新检测溢出状态
          this.$nextTick(() => {
            // 确保_isOverflowing属性存在
            if (!record._isOverflowing) {
              this.$set(record, '_isOverflowing', {});
            }
            
            // 在收起状态下，通过设置为true确保展开按钮依然可见
            allFields.forEach(fieldName => {
              if (!isExpanding) {
                this.$set(record._isOverflowing, fieldName, true);
              }
            });
          });
        },
        loadAllTaskNames() {
          // 向后端请求获取所有可用的任务名称
          axios.get('/history_records', {
            params: {
              get_all_task_names: true,
              page_size: 1,  // 只需要任务名称，不需要大量记录
              page: 1
            }
          }).then(response => {
            if (response.data.success && response.data.data.all_task_names) {
              this.allTaskNames = this.sortTaskNamesByPinyin(response.data.data.all_task_names);
            } else {
              // 如果API失败，回退到从当前页记录中提取任务名称
              if (this.history.records && this.history.records.length > 0) {
                const taskNames = new Set();
                this.history.records.forEach(record => {
                  if (record.task_name) {
                    taskNames.add(record.task_name);
                  }
                });
                this.allTaskNames = this.sortTaskNamesByPinyin([...taskNames]);
              }
            }
          }).catch(error => {
            // 如果API请求出错，回退到从当前页记录中提取任务名称
            if (this.history.records && this.history.records.length > 0) {
              const taskNames = new Set();
              this.history.records.forEach(record => {
                if (record.task_name) {
                  taskNames.add(record.task_name);
                }
              });
              this.allTaskNames = this.sortTaskNamesByPinyin([...taskNames]);
            }
          });
        },
        async loadTaskLatestInfo() {
          // 获取所有任务的最新转存信息（包括日期和文件）
          try {
            const response = await axios.get('/task_latest_info');
            if (response.data.success) {
              this.taskLatestRecords = response.data.data.latest_records;
              this.taskLatestFiles = response.data.data.latest_files;
              
              // 构建进度映射，确保基于日期的集数统计能正常工作
              this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(this.taskLatestFiles || {});
              this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
              
              // 同时加载 episodes 数据，确保基于日期的集数统计能正常工作
              try {
                await this.loadCalendarEpisodesLocal();
              } catch (e) {
                console.warn('加载 episodes 数据失败:', e);
              }
            } else {
              console.error('获取任务最新信息失败:', response.data.message);
            }
          } catch (error) {
            console.error('获取任务最新信息失败:', error);
          }
        },
        // 加载任务列表的元数据信息（用于热更新海报和元数据）
        async loadTasklistMetadata() {
          try {
            // 加载任务信息，获取元数据和海报信息
            const tasksResponse = await axios.get('/api/calendar/tasks');
            if (tasksResponse.data.success) {
              // 更新任务映射，确保海报视图能正确显示元数据，使用Vue.set确保响应式更新
              this.calendar.tasks = tasksResponse.data.data.tasks || [];
              this.$delete(this.calendar, 'taskMapByName');
              this.$set(this.calendar, 'taskMapByName', {});
              this.calendar.tasks.forEach(task => {
                const key = task.task_name;
                if (key) this.$set(this.calendar.taskMapByName, key, task);
              });
              
              // 更新内容类型，确保类型筛选按钮能热更新
              const rawTypes = tasksResponse.data.data.content_types || [];
              this.updateContentTypes(rawTypes);
              
              // 更新任务列表的内容类型
              this.tasklist.contentTypes = this.calendar.contentTypes;
            }
          } catch (error) {
            console.warn('加载任务列表元数据失败:', error);
          }
        },
        // 触发热更新（保存配置后调用）
        async triggerTasklistHotUpdate() {
          try {
            // 如果当前是任务列表页面，只刷新元数据，不重新加载任务列表
            if (this.activeTab === 'tasklist') {
              await this.loadTasklistMetadata();
            }
            
            // 如果当前是追剧日历页面，也刷新日历数据
            if (this.activeTab === 'calendar') {
              await this.refreshCalendarData();
            }
          } catch (error) {
            console.warn('触发热更新失败:', error);
          }
        },
        // 启动任务列表后台监听：优先使用SSE，失败时回退轮询
        startTasklistAutoWatch() {
          try {
            // 先停止现有的轮询
            this.stopTasklistAutoWatch();
            
            // 建立 SSE 连接，实时感知任务列表变化（成功建立后停用轮询，失败时回退轮询）
            try {
              // 使用全局 SSE 单例
              this.ensureGlobalSSE();
              if (this.appSSE && !this.tasklistSSEListenerAdded) {
                const onTasklistChanged = async (ev) => {
                  try {
                    // 解析变更原因（后端通过 SSE data 传递）
                    let changeReason = '';
                    try { changeReason = JSON.parse(ev && ev.data || '{}').reason || ''; } catch (e) {}

                    // 先拉取最新转存信息并重建映射
                    try {
                      const latestRes = await axios.get('/task_latest_info');
                      if (latestRes.data && latestRes.data.success) {
                        const latestFiles = latestRes.data.data.latest_files || {};
                        this.taskLatestFiles = latestFiles;
                        this.taskLatestRecords = latestRes.data.data.latest_records || {};
                      }
                    } catch (e) {}
                    
                    // 重新加载任务元数据，确保海报和元数据能热更新
                    try {
                      await this.loadTasklistMetadata();
                    } catch (e) {}
                    
                    // 如果是任务相关的变更，也更新任务列表数据
                    if (changeReason === 'edit_metadata' || changeReason === 'task_updated') {
                      try {
                        const dataRes = await axios.get('/data');
                        if (dataRes.data && dataRes.data.success) {
                          const cfg = dataRes.data.data || {};
                          const oldTaskCount = (this.formData.tasklist || []).length;
                          // 后端推送导致的任务列表更新，不应触发"未保存修改"提示
                          this.suppressConfigModifiedOnce = true;
                          this.formData.tasklist = cfg.tasklist || [];
                          // 同步任务名集合用于筛选
                          this.calendar.taskNames = (this.formData.tasklist || []).map(t => t.taskname).filter(Boolean);
                          // 如任务数量变化，重建与任务相关的最新文件映射的键集合
                          if ((this.formData.tasklist || []).length !== oldTaskCount) {
                            try {
                              this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(this.taskLatestFiles || {});
                            } catch (e) {}
                          }
                        }
                      } catch (e) {}
                    }
                  } catch (e) {}
                };
                this.onTasklistChangedHandler = onTasklistChanged;
                this.appSSE.addEventListener('calendar_changed', onTasklistChanged);
                this.tasklistSSEListenerAdded = true;
                // onopen/onerror 已集中在 ensureGlobalSSE，无需重复设置
              }
            } catch (e) {
              // 忽略 SSE 失败，继续使用轮询
            }
          } catch (e) {
            console.warn('启动任务列表后台监听失败:', e);
          }
        },
        // 停止任务列表后台监听
        stopTasklistAutoWatch() {
          try {
            // 关闭SSE连接
            if (this.tasklistSSE) {
              this.tasklistSSE.close();
              this.tasklistSSE = null;
            }
            // 停止轮询
            if (this.tasklistAutoWatchTimer) {
              clearInterval(this.tasklistAutoWatchTimer);
              this.tasklistAutoWatchTimer = null;
            }
          } catch (e) {
            console.warn('停止任务列表后台监听失败:', e);
          }
        },
        getTaskLatestRecordDisplay(taskName) {
          // 获取任务最新记录的显示文本
          const latestRecord = this.taskLatestRecords[taskName];
          return latestRecord ? latestRecord.display : '';
        },
        isTaskUpdatedToday(taskName) {
          // 检查任务是否在今天更新
          const latestRecord = this.taskLatestRecords[taskName];
          if (!latestRecord || !latestRecord.full) {
            return false;
          }

          // 获取今天的完整日期，格式为 YYYY-MM-DD
          const today = new Date();
          const todayFormatted = today.getFullYear() + '-' +
                                String(today.getMonth() + 1).padStart(2, '0') + '-' +
                                String(today.getDate()).padStart(2, '0');

          return latestRecord.full === todayFormatted;
        },
        isRecordUpdatedToday(record) {
          // 检查转存记录是否在今天更新
          if (!record || !record.transfer_time_readable) {
            return false;
          }

          // 获取今天的日期，格式为 YYYY-MM-DD
          const today = new Date();
          const todayFormatted = today.getFullYear() + '-' +
                                String(today.getMonth() + 1).padStart(2, '0') + '-' +
                                String(today.getDate()).padStart(2, '0');

          // 从 transfer_time_readable 中提取日期部分（格式通常为 "YYYY-MM-DD HH:MM:SS"）
          const recordDate = record.transfer_time_readable.split(' ')[0];

          return recordDate === todayFormatted;
        },
        isFileUpdatedToday(file) {
          // 检查文件是否在今天更新（基于修改日期）
          if (!file || !file.updated_at) {
            return false;
          }

          // 获取今天的日期，格式为 YYYY-MM-DD
          const today = new Date();
          const todayFormatted = today.getFullYear() + '-' +
                                String(today.getMonth() + 1).padStart(2, '0') + '-' +
                                String(today.getDate()).padStart(2, '0');

          // 使用与 formatDate 方法相同的逻辑处理时间戳
          try {
            const fileDate = new Date(file.updated_at);
            const fileDateFormatted = fileDate.getFullYear() + '-' +
                                     String(fileDate.getMonth() + 1).padStart(2, '0') + '-' +
                                     String(fileDate.getDate()).padStart(2, '0');

            return fileDateFormatted === todayFormatted;
          } catch (error) {
            console.error('处理文件时间戳时出错:', error, file.updated_at);
            return false;
          }
        },
        shouldShowTodayIndicator() {
          // 检查是否应该显示当日更新图标
          return this.formData.button_display.today_update_indicator !== 'disabled';
        },
        getTodayIndicatorClass() {
          // 获取当日更新图标的CSS类
          if (this.formData.button_display.today_update_indicator === 'hover') {
            return 'hover-only';
          }
          return '';
        },
        openDatePicker(index) {
          // 使用$refs访问对应的日期选择器并打开它
          const dateRef = this.$refs[`enddate_${index}`];
          if (dateRef && dateRef[0]) {
            // 由于Vue的ref在v-for中会返回一个数组，所以我们需要访问第一个元素
            dateRef[0].showPicker();
            
            // 点击按钮后立即使按钮失去焦点
            document.activeElement.blur();
          }
        },
        toggleCloudSaverPassword() {
          this.showCloudSaverPassword = !this.showCloudSaverPassword;
        },
        toggleWebuiPassword() {
          this.showWebuiPassword = !this.showWebuiPassword;
        },
        togglePageWidth() {
          // 循环切换页面宽度模式：窄 -> 中 -> 宽 -> 窄
          if (this.pageWidthMode === 'narrow') {
            this.pageWidthMode = 'medium';
          } else if (this.pageWidthMode === 'medium') {
            this.pageWidthMode = 'wide';
          } else {
            this.pageWidthMode = 'narrow';
          }
          // 保存用户选择到本地存储
          localStorage.setItem('quarkAutoSave_pageWidthMode', this.pageWidthMode);
          // 应用页面宽度类
          document.body.classList.remove('page-width-narrow', 'page-width-medium', 'page-width-wide');
          document.body.classList.add('page-width-' + this.pageWidthMode);
        },
        showToast(message) {
          this.toastMessage = message;
          $(this.$refs.toast).toast('show');
        },
        checkPreviewScrollbar() {
          // 检查命名预览界面的滚动条状态
          const fileSelectModal = document.getElementById('fileSelectModal');
          if (fileSelectModal && fileSelectModal.getAttribute('data-modal-type') === 'preview') {
            // 查找命名预览模式的内容容器
            const contentContainers = fileSelectModal.querySelectorAll('.modal-body > div:not(.alert-warning)');
            
            contentContainers.forEach(container => {
              // 检查是否有水平滚动条（滚动宽度大于客户端宽度）
              if (container.scrollWidth > container.clientWidth) {
                container.classList.add('has-scrollbar');
              } else {
                container.classList.remove('has-scrollbar');
              }
            });
          }
        },
        isModalTextTruncated(text, fileIndex, field) {
          if (!this.fileSelect.fileList[fileIndex]) return false;
          
          // 确保文本是字符串类型
          const str = String(text || '');
          
          // 先检查文本是否有足够长度，太短的文本肯定不需要展开
          if (str.length <= 20) return false;
          
          // 从文件的_isOverflowing属性中获取溢出状态
          const file = this.fileSelect.fileList[fileIndex];
          
          // 如果已经展开了，那么我们认为它可能是需要截断的
          if (file._expandedFields && file._expandedFields.includes(field)) {
            return true;
          }
          
          return file._isOverflowing && file._isOverflowing[field];
        },
        toggleModalExpand(index, field) {
          // 获取当前文件
          const file = this.fileSelect.fileList[index];
          // 初始化_expandedFields属性（如果不存在）
          if (!file._expandedFields) {
            this.$set(file, '_expandedFields', []);
          }
          
          // 检查字段是否已经展开
          const fieldIndex = file._expandedFields.indexOf(field);
          const isExpanding = fieldIndex === -1;
          
          if (isExpanding) {
            // 如果是展开操作，添加字段到展开列表
            file._expandedFields.push(field);
          } else {
            // 如果是收起操作，从展开列表中移除字段
            file._expandedFields.splice(fieldIndex, 1);
          }
          
          // 在下一个DOM更新周期中强制重新检测溢出状态
          this.$nextTick(() => {
            // 确保_isOverflowing属性存在
            if (!file._isOverflowing) {
              this.$set(file, '_isOverflowing', {});
            }
            
            // 在收起状态下，通过设置为true确保展开按钮依然可见
            if (!isExpanding) {
              this.$set(file._isOverflowing, field, true);
            }
            
            // 强制更新视图
            this.$forceUpdate();
          });
          
          // 阻止事件冒泡，避免触发行点击事件
          event.stopPropagation();
        },
        // 文件选择模态框的排序方法
        sortFileList(field) {
          // 切换排序方向
          if (this.fileSelect.sortBy === field) {
            this.fileSelect.sortOrder = this.fileSelect.sortOrder === 'asc' ? 'desc' : 'asc';
          } else {
            this.fileSelect.sortBy = field;
            // 默认降序（除了文件名外）
            this.fileSelect.sortOrder = field === 'file_name' ? 'asc' : 'desc';
          }

          // 按选定字段和顺序对文件列表进行排序
          this.fileSelect.fileList.sort((a, b) => {
            if (field === 'file_name') {
              // 检查当前模态框类型，选择起始文件模态框使用全局文件排序函数
              const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
              if (modalType === 'start-file') {
                // 选择起始文件模态框：按过滤状态和类型排序
                // 1. 检查过滤状态 - 包括没有file_name_re字段的情况
                const aFiltered = !a.file_name_re || a.file_name_re === '×' || a.file_name_re.startsWith('×');
                const bFiltered = !b.file_name_re || b.file_name_re === '×' || b.file_name_re.startsWith('×');

                // 无论升序还是降序，未被过滤的项目都排在前面，被过滤的项目排在后面
                if (!aFiltered && bFiltered) return -1;
                if (aFiltered && !bFiltered) return 1;

                // 2. 在同一过滤状态内，文件夹排在文件前面
                if (a.dir && !b.dir) return -1;
                if (!a.dir && b.dir) return 1;

                // 使用全局文件排序函数
                const ka = sortFileByName(a), kb = sortFileByName(b);
                for (let i = 0; i < ka.length; ++i) {
                  if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
                }
                return 0;
              } else {
                // 其他模态框：文件夹排在文件前面
                if (a.dir && !b.dir) return -1;
                if (!a.dir && b.dir) return 1;
                // 其他模态框：使用与任务列表一致的自然排序算法
                const ka = sortFileByName(a), kb = sortFileByName(b);
                for (let i = 0; i < ka.length; ++i) {
                  if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
                }
                return 0;
              }
            }
            if (field === 'file_name_re') {
              const aHasValidRename = a.file_name_re && a.file_name_re !== '×' && !a.file_name_re.startsWith('×');
              const bHasValidRename = b.file_name_re && b.file_name_re !== '×' && !b.file_name_re.startsWith('×');

              // 有效重命名的项目排在前面
              if (aHasValidRename && !bHasValidRename) return -1;
              if (!aHasValidRename && bHasValidRename) return 1;

              // 对于有效重命名的项目，按重命名结果排序
              if (aHasValidRename && bHasValidRename) {
                // 同类型内，文件夹排在文件前面
                if (a.dir && !b.dir) return -1;
                if (!a.dir && b.dir) return 1;

                let aValue, bValue;
                // 对于重命名列，优先使用episode_number进行数值排序（如果存在）
                if (a.episode_number !== undefined && b.episode_number !== undefined) {
                  // 确保进行数值比较
                  aValue = parseInt(a.episode_number, 10);
                  bValue = parseInt(b.episode_number, 10);

                  // 如果解析失败，回退到字符串比较
                  if (isNaN(aValue) || isNaN(bValue)) {
                    aValue = String(a.episode_number);
                    bValue = String(b.episode_number);
                  }
                } else {
                  // 否则使用智能排序：优先数值排序，其次日期排序，最后字符串排序
                  const aRename = a.file_name_re || '';
                  const bRename = b.file_name_re || '';
                  
                  // 尝试提取所有数字进行数值排序
                  const aNumbers = aRename.match(/\d+/g);
                  const bNumbers = bRename.match(/\d+/g);
                  
                  if (aNumbers && bNumbers && aNumbers.length > 0 && bNumbers.length > 0) {
                    // 如果都能提取到数字，进行数值比较
                    // 优先比较第一个数字，如果相同则比较后续数字
                    for (let i = 0; i < Math.max(aNumbers.length, bNumbers.length); i++) {
                      const aNum = parseInt(aNumbers[i] || '0', 10);
                      const bNum = parseInt(bNumbers[i] || '0', 10);
                      if (aNum !== bNum) {
                        aValue = aNum;
                        bValue = bNum;
                        break;
                      }
                    }
                    // 如果所有数字都相同，使用自然排序
                    if (aValue === undefined) {
                      aValue = aRename;
                      bValue = bRename;
                    }
                  } else {
                    // 否则使用自然排序（支持数值和日期的智能排序）
                    aValue = aRename;
                    bValue = bRename;
                  }
                }

                if (this.fileSelect.sortOrder === 'asc') {
                  // 字符串使用自然排序，数值直接比较
                  if (typeof aValue === 'string' && typeof bValue === 'string') {
                    return aValue.localeCompare(bValue, undefined, { numeric: true, sensitivity: 'base' });
                  }
                  return aValue > bValue ? 1 : -1;
                } else {
                  if (typeof aValue === 'string' && typeof bValue === 'string') {
                    return bValue.localeCompare(aValue, undefined, { numeric: true, sensitivity: 'base' });
                  }
                  return aValue < bValue ? 1 : -1;
                }
              }

              // 对于无效重命名的项目，按照特定优先级排序
              if (!aHasValidRename && !bHasValidRename) {
                // 定义无效重命名的优先级（数字越小优先级越高）
                const getInvalidRenamePriority = (item) => {
                  const renameResult = item.file_name_re || '×';
                  if (renameResult.includes('无法识别剧集编号')) return 1;
                  if (renameResult === '×') return 2;
                  return 3; // 其他无效重命名
                };

                const aPriority = getInvalidRenamePriority(a);
                const bPriority = getInvalidRenamePriority(b);

                // 如果优先级不同，按优先级排序
                if (aPriority !== bPriority) {
                  return aPriority - bPriority;
                }

                // 同一优先级内，文件夹排在文件前面
                if (a.dir && !b.dir) return -1;
                if (!a.dir && b.dir) return 1;

                // 同一优先级且同一类型内，使用智能排序
                const ka = sortFileByName(a), kb = sortFileByName(b);
                for (let i = 0; i < ka.length; ++i) {
                  if (ka[i] !== kb[i]) return this.fileSelect.sortOrder === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
                }
                return 0;
              }
              return 0;
            } else if (field === 'size') {
              // 文件夹始终在前
              if (a.dir && !b.dir) return -1;
              if (!a.dir && b.dir) return 1;

              // 文件夹按项目数量排序，文件按大小排序
              let aValue = a.dir ? (a.include_items || 0) : (a.size || 0);
              let bValue = b.dir ? (b.include_items || 0) : (b.size || 0);
              if (this.fileSelect.sortOrder === 'asc') {
                return aValue > bValue ? 1 : -1;
              } else {
                return aValue < bValue ? 1 : -1;
              }
            } else if (field === 'updated_at') {
              // 修改日期排序严格按照修改日期，不区分文件夹和文件
              let aValue = a.updated_at || 0;
              let bValue = b.updated_at || 0;
              if (this.fileSelect.sortOrder === 'asc') {
                return aValue > bValue ? 1 : -1;
              } else {
                return aValue < bValue ? 1 : -1;
              }
            }
          });
        },
        manualSortFileList(field, order) {
          this.fileSelect.fileList.sort((a, b) => {
            if (field === 'file_name') {
              // 检查当前模态框类型，选择起始文件模态框使用特殊排序逻辑
              const modalType = document.getElementById('fileSelectModal').getAttribute('data-modal-type');
              if (modalType === 'start-file') {
                // 选择起始文件模态框：按过滤状态和类型排序
                // 1. 检查过滤状态 - 包括没有file_name_re字段的情况
                const aFiltered = !a.file_name_re || a.file_name_re === '×' || a.file_name_re.startsWith('×');
                const bFiltered = !b.file_name_re || b.file_name_re === '×' || b.file_name_re.startsWith('×');

                // 无论升序还是降序，未被过滤的项目都排在前面，被过滤的项目排在后面
                if (!aFiltered && bFiltered) return -1;
                if (aFiltered && !bFiltered) return 1;

                // 2. 在同一过滤状态内，文件夹排在文件前面
                if (a.dir && !b.dir) return -1;
                if (!a.dir && b.dir) return 1;

                // 使用全局文件排序函数
                const ka = sortFileByName(a), kb = sortFileByName(b);
                for (let i = 0; i < ka.length; ++i) {
                  if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
                }
                return 0;
              } else {
                // 其他模态框：文件夹排在文件前面
                if (a.dir && !b.dir) return -1;
                if (!a.dir && b.dir) return 1;
                // 使用智能排序函数
                const ka = sortFileByName(a), kb = sortFileByName(b);
                for (let i = 0; i < ka.length; ++i) {
                  if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
                }
                return 0;
              }
            }
            if (field === 'file_name_re') {
              const aHasValidRename = a.file_name_re && a.file_name_re !== '×' && !a.file_name_re.startsWith('×');
              const bHasValidRename = b.file_name_re && b.file_name_re !== '×' && !b.file_name_re.startsWith('×');

              // 有效重命名的项目排在前面
              if (aHasValidRename && !bHasValidRename) return -1;
              if (!aHasValidRename && bHasValidRename) return 1;

              // 对于有效重命名的项目，按重命名结果排序
              if (aHasValidRename && bHasValidRename) {
                // 同类型内，文件夹排在文件前面
                if (a.dir && !b.dir) return -1;
                if (!a.dir && b.dir) return 1;

                let aValue, bValue;
                // 对于重命名列，优先使用episode_number进行数值排序（如果存在）
                if (a.episode_number !== undefined && b.episode_number !== undefined) {
                  // 确保进行数值比较
                  aValue = parseInt(a.episode_number, 10);
                  bValue = parseInt(b.episode_number, 10);

                  // 如果解析失败，回退到字符串比较
                  if (isNaN(aValue) || isNaN(bValue)) {
                    aValue = String(a.episode_number);
                    bValue = String(b.episode_number);
                  }
                } else {
                  // 否则使用智能排序：优先数值排序，其次日期排序，最后字符串排序
                  const aRename = a.file_name_re || '';
                  const bRename = b.file_name_re || '';
                  
                  // 尝试提取所有数字进行数值排序
                  const aNumbers = aRename.match(/\d+/g);
                  const bNumbers = bRename.match(/\d+/g);
                  
                  if (aNumbers && bNumbers && aNumbers.length > 0 && bNumbers.length > 0) {
                    // 如果都能提取到数字，进行数值比较
                    // 优先比较第一个数字，如果相同则比较后续数字
                    for (let i = 0; i < Math.max(aNumbers.length, bNumbers.length); i++) {
                      const aNum = parseInt(aNumbers[i] || '0', 10);
                      const bNum = parseInt(bNumbers[i] || '0', 10);
                      if (aNum !== bNum) {
                        aValue = aNum;
                        bValue = bNum;
                        break;
                      }
                    }
                    // 如果所有数字都相同，使用自然排序
                    if (aValue === undefined) {
                      aValue = aRename;
                      bValue = bRename;
                    }
                  } else {
                    // 否则使用自然排序（支持数值和日期的智能排序）
                    aValue = aRename;
                    bValue = bRename;
                  }
                }

                if (order === 'asc') {
                  // 如果都是字符串，使用自然排序
                  if (typeof aValue === 'string' && typeof bValue === 'string') {
                    return aValue.localeCompare(bValue, undefined, { numeric: true, sensitivity: 'base' });
                  }
                  return aValue > bValue ? 1 : -1;
                } else {
                  // 如果都是字符串，使用自然排序
                  if (typeof aValue === 'string' && typeof bValue === 'string') {
                    return bValue.localeCompare(aValue, undefined, { numeric: true, sensitivity: 'base' });
                  }
                  return aValue < bValue ? 1 : -1;
                }
              }

              // 对于无效重命名的项目，按照特定优先级排序
              if (!aHasValidRename && !bHasValidRename) {
                // 定义无效重命名的优先级（数字越小优先级越高）
                const getInvalidRenamePriority = (item) => {
                  const renameResult = item.file_name_re || '×';
                  if (renameResult.includes('无法识别剧集编号')) return 1;
                  if (renameResult === '×') return 2;
                  return 3; // 其他无效重命名
                };

                const aPriority = getInvalidRenamePriority(a);
                const bPriority = getInvalidRenamePriority(b);

                // 如果优先级不同，按优先级排序
                if (aPriority !== bPriority) {
                  return aPriority - bPriority;
                }

                // 同一优先级内，文件夹排在文件前面
                if (a.dir && !b.dir) return -1;
                if (!a.dir && b.dir) return 1;

                // 同一优先级且同一类型内，使用智能排序
                const ka = sortFileByName(a), kb = sortFileByName(b);
                for (let i = 0; i < ka.length; ++i) {
                  if (ka[i] !== kb[i]) return order === 'asc' ? (ka[i] > kb[i] ? 1 : -1) : (ka[i] < kb[i] ? 1 : -1);
                }
                return 0;
              }
              return 0;
            } else if (field === 'size') {
              // 文件夹始终在前
              if (a.dir && !b.dir) return -1;
              if (!a.dir && b.dir) return 1;

              // 文件夹按项目数量排序，文件按大小排序
              let aValue = a.dir ? (a.include_items || 0) : (a.size || 0);
              let bValue = b.dir ? (b.include_items || 0) : (b.size || 0);
              if (order === 'asc') {
                return aValue > bValue ? 1 : -1;
              } else {
                return aValue < bValue ? 1 : -1;
              }
            } else if (field === 'updated_at') {
              // 修改日期排序严格按照修改日期，不区分文件夹和文件
              let aValue = a.updated_at || 0;
              let bValue = b.updated_at || 0;
              if (order === 'asc') {
                return aValue > bValue ? 1 : -1;
              } else {
                return aValue < bValue ? 1 : -1;
              }
            }
          });
        },
        validateNumberInput(event, field, max) {
          // 获取当前输入值
          let value = event.target.value;
          // 获取输入框的当前光标位置
          const cursorPosition = event.target.selectionStart;
          
          // 记录原始长度
          const originalLength = value.length;
          
          // 移除非数字字符
          const cleanValue = value.replace(/[^\d]/g, '');
          
          // 如果有非数字字符被移除
          if (cleanValue !== value) {
            // 计算移除了多少个字符
            const diff = originalLength - cleanValue.length;
            
            // 更新输入框的值
            event.target.value = cleanValue;
            
            // 调整光标位置（考虑到字符被移除）
            setTimeout(() => {
              event.target.setSelectionRange(Math.max(0, cursorPosition - diff), Math.max(0, cursorPosition - diff));
            }, 0);
          }
          
          // 确保不超过最大值
          if (cleanValue !== '' && parseInt(cleanValue) > max) {
            event.target.value = max.toString();
          }
          
          // 更新数据模型，如果为空则默认为0
          this.formData[field] = event.target.value === '' ? 0 : parseInt(event.target.value);
        },
        selectRecord(event, recordId) {
          // 获取当前记录的索引
          const currentIndex = this.filteredHistoryRecords.findIndex(record => record.id === recordId);
          if (currentIndex === -1) return;
          
          // 如果是Shift+点击，选择范围
          if (event.shiftKey && this.selectedRecords.length > 0) {
            // 找出所有已选中记录的索引
            const selectedIndices = this.selectedRecords.map(id => 
              this.filteredHistoryRecords.findIndex(record => record.id === id)
            ).filter(index => index !== -1); // 过滤掉未找到的记录
            
            if (selectedIndices.length > 0) {
              // 找出已选中记录中最靠前的索引
              const earliestSelectedIndex = Math.min(...selectedIndices);
              // 确定最终的选择范围
              const startIndex = Math.min(earliestSelectedIndex, currentIndex);
              const endIndex = Math.max(earliestSelectedIndex, currentIndex);
              
              // 获取新的选择范围内所有记录的ID
              this.selectedRecords = this.filteredHistoryRecords
                .slice(startIndex, endIndex + 1)
                .map(record => record.id);
            } else {
              // 如果没有有效的选中记录（可能是由于列表刷新），则只选择当前记录
              this.selectedRecords = [recordId];
            }
          } 
          // 如果是Ctrl/Cmd+点击，切换单个记录选择状态
          else if (event.ctrlKey || event.metaKey) {
            if (this.selectedRecords.includes(recordId)) {
              this.selectedRecords = this.selectedRecords.filter(id => id !== recordId);
            } else {
              this.selectedRecords.push(recordId);
            }
          } 
          // 普通点击，清除当前选择并选择当前记录
          else {
            if (this.selectedRecords.length === 1 && this.selectedRecords.includes(recordId)) {
              this.selectedRecords = [];
            } else {
              this.selectedRecords = [recordId];
            }
          }
          
          // 更新最后选择的记录索引，只有在有选择记录时才更新
          if (this.selectedRecords.length > 0) {
            this.lastSelectedRecordIndex = currentIndex;
          } else {
            this.lastSelectedRecordIndex = -1;
          }
        },
        deleteSelectedRecords() {
          if (this.selectedRecords.length === 0) return;
          
          if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条记录吗？`)) {
            axios.post('/delete_history_records', {
              record_ids: this.selectedRecords
            })
            .then(response => {
              if (response.data.success) {
                this.showToast(response.data.message);
                // 重新加载记录
                this.loadHistoryRecords();
                // 清空选择
                this.selectedRecords = [];
                this.lastSelectedRecordIndex = -1;
                // 删除历史记录成功后，主动触发任务/日历刷新，确保任务列表的进度与统计热更新
                (async () => {
                  try {
                    const latestRes = await axios.get('/task_latest_info');
                    if (latestRes.data && latestRes.data.success) {
                      const latestFiles = latestRes.data.data.latest_files || {};
                      this.taskLatestFiles = latestFiles;
                      this.taskLatestRecords = latestRes.data.data.latest_records || {};
                      this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(latestFiles);
                      this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                    }
                  } catch (e) {}
                  try {
                    const tasksResponse = await axios.get('/api/calendar/tasks');
                    if (tasksResponse.data && tasksResponse.data.success) {
                      this.calendar.tasks = tasksResponse.data.data.tasks || [];
                      this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                      const rawTypes = tasksResponse.data.data.content_types || [];
                      this.updateContentTypes(rawTypes);
                      this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                    }
                  } catch (e) {}
                  try {
                    await this.loadCalendarEpisodesLocal();
                    this.initializeCalendarDates();
                    await this.loadTodayUpdatesLocal();
                  } catch (e) {}
                })();
              } else {
                alert(response.data.message || '删除失败');
              }
            })
            .catch(error => {
              console.error('删除记录时出错:', error);
              alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
            });
          }
        },
        deleteRecord(id, original_name) {
          // 检查是否有多条记录被选中
          if (this.selectedRecords.length > 1) {
            // 如果有多条记录被选中，调用批量删除方法
            if (confirm(`确定要删除选中的 ${this.selectedRecords.length} 条记录吗？`)) {
              axios.post('/delete_history_records', {
                record_ids: this.selectedRecords
              })
              .then(response => {
                if (response.data.success) {
                  this.showToast(`成功删除 ${this.selectedRecords.length} 条记录`);
                  // 重新加载记录
                  this.loadHistoryRecords();
                  // 清空选择
                  this.selectedRecords = [];
                  this.lastSelectedRecordIndex = -1;
                  // 批量删除历史记录成功后，触发任务/日历刷新
                  (async () => {
                    try {
                      const latestRes = await axios.get('/task_latest_info');
                      if (latestRes.data && latestRes.data.success) {
                        const latestFiles = latestRes.data.data.latest_files || {};
                        this.taskLatestFiles = latestFiles;
                        this.taskLatestRecords = latestRes.data.data.latest_records || {};
                        this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(latestFiles);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    try {
                      const tasksResponse = await axios.get('/api/calendar/tasks');
                      if (tasksResponse.data && tasksResponse.data.success) {
                        this.calendar.tasks = tasksResponse.data.data.tasks || [];
                        this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                        const rawTypes = tasksResponse.data.data.content_types || [];
                        this.updateContentTypes(rawTypes);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    try {
                      await this.loadCalendarEpisodesLocal();
                      this.initializeCalendarDates();
                      await this.loadTodayUpdatesLocal();
                    } catch (e) {}
                  })();
                } else {
                  alert(response.data.message || '删除失败');
                }
              })
              .catch(error => {
                console.error('删除记录时出错:', error);
                alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
              });
            }
          } else {
            // 单条记录删除
            if (confirm(`确定要删除此条记录吗？`)) {
              axios.post('/delete_history_record', {
                id: id
              })
              .then(response => {
                if (response.data.success) {
                  this.showToast('成功删除 1 条记录');
                  // 重新加载记录
                  this.loadHistoryRecords();
                  // 清空选择
                  this.selectedRecords = [];
                  this.lastSelectedRecordIndex = -1;
                  // 单条删除历史记录成功后，触发任务/日历刷新
                  (async () => {
                    try {
                      const latestRes = await axios.get('/task_latest_info');
                      if (latestRes.data && latestRes.data.success) {
                        const latestFiles = latestRes.data.data.latest_files || {};
                        this.taskLatestFiles = latestFiles;
                        this.taskLatestRecords = latestRes.data.data.latest_records || {};
                        this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(latestFiles);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    try {
                      const tasksResponse = await axios.get('/api/calendar/tasks');
                      if (tasksResponse.data && tasksResponse.data.success) {
                        this.calendar.tasks = tasksResponse.data.data.tasks || [];
                        this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                        const rawTypes = tasksResponse.data.data.content_types || [];
                        this.updateContentTypes(rawTypes);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    try {
                      await this.loadCalendarEpisodesLocal();
                      this.initializeCalendarDates();
                      await this.loadTodayUpdatesLocal();
                    } catch (e) {}
                  })();
                } else {
                  alert(response.data.message || '删除失败');
                }
              })
              .catch(error => {
                console.error('删除记录时出错:', error);
                alert('删除记录时出错: ' + (error.response?.data?.message || error.message));
              });
            }
          }
        },
        // 处理点击表格外区域的事件
        handleOutsideClick(event) {
          // 如果当前不是历史记录页面或者没有选中的记录，则不处理
          if (this.activeTab !== 'history' || this.selectedRecords.length === 0) {
            return;
          }
          
          // 检查点击是否在表格内
          const tableElement = document.querySelector('.table.selectable-records');
          // 检查点击是否在分页控制区域
          const paginationElement = document.querySelector('.pagination-container');
          
          // 如果点击不在表格内且不在分页控制区域内，则清除选择
          if (tableElement && !tableElement.contains(event.target) && 
              paginationElement && !paginationElement.contains(event.target)) {
            this.selectedRecords = [];
            this.lastSelectedRecordIndex = -1;
          }
        },
        // 处理重命名编辑状态的外部点击事件
        handleRenameOutsideClick(event) {
          // 如果当前不是文件整理页面，则不处理
          if (this.activeTab !== 'filemanager') {
            return;
          }

          // 检查是否有文件正在编辑状态
          const editingFile = this.fileManager.fileList.find(file => file._editing);
          if (!editingFile) {
            return;
          }

          // 检查点击是否在重命名按钮上
          const renameButton = event.target.closest('.rename-record-btn');
          if (renameButton) {
            return; // 点击在重命名按钮上，不处理
          }

          // 点击在任何地方（包括输入框内），都保存重命名并关闭输入框
          this.saveRenameFile(editingFile);

          // 阻止事件继续传播，避免与其他点击事件处理器冲突
          event.stopPropagation();
        },
        selectFileItem(event, fileId) {
          // 如果是在预览模式或选择分享模式，不允许选择
          if (this.fileSelect.previewRegex || this.fileSelect.selectShare) return;
          
          // 获取当前文件的索引
          const currentIndex = this.fileSelect.fileList.findIndex(file => file.fid === fileId);
          if (currentIndex === -1) return;
          
          // 如果是Shift+点击，选择范围
          if (event.shiftKey && this.fileSelect.selectedFiles.length > 0) {
            // 找出所有已选中文件的索引
            const selectedIndices = this.fileSelect.selectedFiles.map(id => 
              this.fileSelect.fileList.findIndex(file => file.fid === id)
            ).filter(index => index !== -1); // 过滤掉未找到的文件
            
            if (selectedIndices.length > 0) {
              // 找出已选中文件中最靠前的索引
              const earliestSelectedIndex = Math.min(...selectedIndices);
              // 确定最终的选择范围
              const startIndex = Math.min(earliestSelectedIndex, currentIndex);
              const endIndex = Math.max(earliestSelectedIndex, currentIndex);
              
              // 获取范围内所有文件的ID（排除文件夹）
              this.fileSelect.selectedFiles = this.fileSelect.fileList
                .slice(startIndex, endIndex + 1)
                .filter(file => !file.dir) // 只选择文件，不选择文件夹
                .map(file => file.fid);
            } else {
              // 如果没有有效的选中文件（可能是由于列表刷新），则只选择当前文件
              const file = this.fileSelect.fileList[currentIndex];
              if (!file.dir) { // 不选择文件夹
                this.fileSelect.selectedFiles = [fileId];
              }
            }
          } 
          // 如果是Ctrl/Cmd+点击，切换单个文件选择状态
          else if (event.ctrlKey || event.metaKey) {
            const file = this.fileSelect.fileList[currentIndex];
            if (file.dir) return; // 不允许选择文件夹
            
            if (this.fileSelect.selectedFiles.includes(fileId)) {
              this.fileSelect.selectedFiles = this.fileSelect.selectedFiles.filter(id => id !== fileId);
            } else {
              this.fileSelect.selectedFiles.push(fileId);
            }
          } 
          // 普通点击，清除当前选择并选择当前文件
          else {
            const file = this.fileSelect.fileList[currentIndex];
            if (file.dir) return; // 不允许选择文件夹
            
            if (this.fileSelect.selectedFiles.length === 1 && this.fileSelect.selectedFiles.includes(fileId)) {
              this.fileSelect.selectedFiles = [];
            } else {
              this.fileSelect.selectedFiles = [fileId];
            }
          }
          
          // 更新最后选择的文件索引，只有在有选择文件时才更新
          if (this.fileSelect.selectedFiles.length > 0) {
            this.fileSelect.lastSelectedFileIndex = currentIndex;
          } else {
            this.fileSelect.lastSelectedFileIndex = -1;
          }
          
          // 阻止事件冒泡
          event.stopPropagation();
        },
        deleteSelectedFiles(clickedFid, clickedFname, isDir, deleteRecords = false) {
          // 如果是文件夹或者没有选中的文件，则按原来的方式删除单个文件
          if (isDir || this.fileSelect.selectedFiles.length === 0) {
            this.deleteFileForSelect(clickedFid, clickedFname, isDir, deleteRecords);
            return;
          }
          
          // 如果点击的文件不在选中列表中，也按原来的方式删除单个文件
          if (!this.fileSelect.selectedFiles.includes(clickedFid)) {
            this.deleteFileForSelect(clickedFid, clickedFname, isDir, deleteRecords);
            return;
          }
          
          // 多选删除
          const selectedCount = this.fileSelect.selectedFiles.length;
          
          // 根据选中数量和是否删除记录使用不同的确认提示
          let confirmMessage = '';
          if (deleteRecords) {
            confirmMessage = selectedCount === 1 
              ? `确定要删除此项目及其关联记录吗？` 
              : `确定要删除选中的 ${selectedCount} 个项目及其关联记录吗？`;
          } else {
            confirmMessage = selectedCount === 1 
              ? `确定要删除此项目吗？` 
              : `确定要删除选中的 ${selectedCount} 个项目吗？`;
          }
            
          if (confirm(confirmMessage)) {
            // 获取当前路径作为save_path参数
            let save_path = "";
            if (this.fileSelect && this.fileSelect.paths) {
              save_path = this.fileSelect.paths.map(item => item.name).join("/");
            }
            
            // 创建一个Promise数组来处理所有删除请求
            const deletePromises = this.fileSelect.selectedFiles.map(fid => {
              // 查找对应的文件对象，获取文件名
              const fileObj = this.fileSelect.fileList.find(file => file.fid === fid);
              const fileName = fileObj ? fileObj.file_name : '';

              return axios.post('/delete_file', { fid: fid, file_name: fileName, delete_records: deleteRecords, save_path: save_path })
                .then(response => {
                  return { fid: fid, success: response.data.code === 0, deleted_records: response.data.deleted_records || 0 };
                })
                .catch(error => {
                  return { fid: fid, success: false, deleted_records: 0 };
                });
            });
            
            // 等待所有删除请求完成
            Promise.all(deletePromises)
              .then(results => {
                // 统计成功和失败的数量
                const successCount = results.filter(r => r.success).length;
                const failCount = results.length - successCount;
                // 统计删除的记录数
                const totalDeletedRecords = results.reduce((sum, r) => sum + (r.deleted_records || 0), 0);
                
                // 从文件列表中移除成功删除的文件
                const successfullyDeletedFids = results.filter(r => r.success).map(r => r.fid);
                this.fileSelect.fileList = this.fileSelect.fileList.filter(item => !successfullyDeletedFids.includes(item.fid));
                
                // 清空选中文件列表
                this.fileSelect.selectedFiles = [];
                this.fileSelect.lastSelectedFileIndex = -1;
                
                // 显示结果
                if (failCount > 0) {
                  alert(`成功删除 ${successCount} 个项目，${failCount} 个项目删除失败`);
                } else {
                  // 根据是否删除记录显示不同的消息
                  if (deleteRecords) {
                    this.showToast(`成功删除 ${successCount} 个项目${totalDeletedRecords > 0 ? `及其关联的 ${totalDeletedRecords} 条记录` : ''}`);
                  } else {
                    this.showToast(`成功删除 ${successCount} 个项目`);
                  }
                }
                
                // 如果同时删除了记录，无论当前在哪个页面，都刷新历史记录
                if (deleteRecords) {
                  this.loadHistoryRecords();
                }
                // 批量删除：仅当删除了记录时再刷新任务/日历
                if (deleteRecords && totalDeletedRecords > 0) {
                  (async () => {
                    try {
                      const latestRes = await axios.get('/task_latest_info');
                      if (latestRes.data && latestRes.data.success) {
                        const latestFiles = latestRes.data.data.latest_files || {};
                        this.taskLatestFiles = latestFiles;
                        this.taskLatestRecords = latestRes.data.data.latest_records || {};
                        this.calendar.progressByTaskName = this.buildProgressByTaskNameFromLatestFiles(latestFiles);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    try {
                      const tasksResponse = await axios.get('/api/calendar/tasks');
                      if (tasksResponse.data && tasksResponse.data.success) {
                        this.calendar.tasks = tasksResponse.data.data.tasks || [];
                        this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                        const rawTypes = tasksResponse.data.data.content_types || [];
                        this.updateContentTypes(rawTypes);
                        this.calendar.progressByShowName = this.buildProgressByShowNameFromTasks(this.calendar.tasks || [], this.calendar.progressByTaskName);
                      }
                    } catch (e) {}
                    try {
                      await this.loadCalendarEpisodesLocal();
                      this.initializeCalendarDates();
                      await this.loadTodayUpdatesLocal();
                    } catch (e) {}
                  })();
                }
              });
          }
        },
        handleModalOutsideClick(event) {
          // 如果当前不是文件选择模式或者没有选中的文件，则不处理
          if (this.fileSelect.previewRegex || this.fileSelect.selectShare || this.fileSelect.selectedFiles.length === 0) {
            return;
          }
          
          // 检查点击是否在表格内
          const tableElement = document.querySelector('#fileSelectModal .table');
          
          // 如果点击不在表格内，则清除选择
          if (tableElement && !tableElement.contains(event.target)) {
            this.fileSelect.selectedFiles = [];
            this.fileSelect.lastSelectedFileIndex = -1;
          }
        },
        preventTextSelection(event, isDir) {
          // 如果是文件夹，不阻止默认行为
          if (isDir) return;
          
          // 如果是Shift点击或Ctrl/Cmd点击，阻止文本选择
          if (event.shiftKey || event.ctrlKey || event.metaKey) {
            event.preventDefault();
          }
        },

        // 重置文件夹内容（删除所有文件和记录）
        resetFolderContent(index) {
          // 获取当前任务的保存路径和任务名称
          let savePath, taskName;

          if (index === -1) {
            // 创建任务模态框的情况
            savePath = this.createTask.taskData.savepath;
            taskName = this.createTask.taskData.taskname;
          } else {
            // 任务列表的情况
            savePath = this.formData.tasklist[index].savepath;
            taskName = this.formData.tasklist[index].taskname;
          }

          if (!savePath) {
            this.showToast('保存路径为空，无法重置');
            return;
          }
          
          // 显示确认对话框
          if (confirm(`确定要重置文件夹「${savePath}」吗？`)) {
            // 显示加载状态
            this.modalLoading = true;
            
            // 调用后端API
            axios.post('/reset_folder', {
              save_path: savePath,
              task_name: taskName,
              account_index: this.fileManager.selectedAccountIndex
            })
            .then(response => {
              if (response.data.success) {
                this.showToast(`重置成功：删除了 ${response.data.deleted_files || 0} 个文件，${response.data.deleted_records || 0} 条记录`);
                // 重置成功后，主动热更新最近转存文件与任务元数据，避免等待 SSE 或轮询
                (async () => {
                  try {
                    const latestRes = await axios.get('/task_latest_info');
                    if (latestRes.data && latestRes.data.success) {
                      const latestFiles = latestRes.data.data.latest_files || {};
                      this.taskLatestFiles = latestFiles;
                      this.taskLatestRecords = latestRes.data.data.latest_records || {};
                    }
                  } catch (e) {}
                  // 重新加载任务元数据（海报与详细信息）
                  try { await this.loadTasklistMetadata(); } catch (e) {}
                })();
                // 如果当前是历史记录页面，刷新记录
                if (this.activeTab === 'history') {
                  this.loadHistoryRecords();
                }
              } else {
                alert(response.data.message || '重置文件夹失败');
              }
              this.modalLoading = false;
            })
            .catch(error => {
              console.error('重置文件夹出错:', error);
              alert('重置文件夹出错: ' + (error.response?.data?.message || error.message));
              this.modalLoading = false;
            });
          }
        },
        refreshPlexLibrary(index) {
          axios.post('/refresh_plex_library', { task_index: index })
            .then(response => {
              if (response.data.success) {
                this.showToast(response.data.message);
              } else {
                alert(response.data.message);
              }
            })
            .catch(error => {
              alert("刷新 Plex 媒体库失败: " + (error.response?.data?.message || error.message || "未知错误"));
            });
        },
        refreshAlistDirectory(index) {
          axios.post('/refresh_alist_directory', { task_index: index })
            .then(response => {
              if (response.data.success) {
                this.showToast(response.data.message);
              } else {
                alert(response.data.message);
              }
            })
            .catch(error => {
              alert("刷新 AList 目录失败: " + (error.response?.data?.message || error.message || "未知错误"));
            });
        },
        // 文件整理页面刷新Plex媒体库
        refreshFileManagerPlexLibrary() {
          // 获取当前目录路径
          const currentPath = this.getCurrentFolderPath();

          axios.post('/refresh_filemanager_plex_library', {
            folder_path: currentPath,
            account_index: this.fileManager.selectedAccountIndex
          })
            .then(response => {
              if (response.data.success) {
                this.showToast(response.data.message);
              } else {
                alert(response.data.message);
              }
            })
            .catch(error => {
              alert("刷新 Plex 媒体库失败: " + (error.response?.data?.message || error.message || "未知错误"));
            });
        },
        // 文件整理页面刷新AList目录
        refreshFileManagerAlistDirectory() {
          // 获取当前目录路径
          const currentPath = this.getCurrentFolderPath();

          axios.post('/refresh_filemanager_alist_directory', {
            folder_path: currentPath,
            account_index: this.fileManager.selectedAccountIndex
          })
            .then(response => {
              if (response.data.success) {
                this.showToast(response.data.message);
              } else {
                alert(response.data.message);
              }
            })
            .catch(error => {
              alert("刷新 AList 目录失败: " + (error.response?.data?.message || error.message || "未知错误"));
            });
        },
        // 获取当前文件夹路径（相对于夸克网盘根目录）
        getCurrentFolderPath() {
          if (this.fileManager.currentFolder === 'root') {
            return '/';  // 根目录返回 /，表示夸克网盘根目录
          }

          // 构建完整路径（相对于夸克网盘根目录）
          let path = '';
          for (const pathItem of this.fileManager.paths) {
            path += '/' + pathItem.name;
          }
          return path || '/';
        },
        filterByTaskName(taskName, event) {
          // 防止事件冒泡，避免触发行选择
          if (event) {
            event.stopPropagation();
          }
          
          // 如果当前已经筛选了该任务，则取消筛选
          if (this.historyTaskSelected === taskName) {
            this.historyTaskSelected = "";
          } else {
            // 设置任务筛选值
            this.historyTaskSelected = taskName;
          }
          
          // 重新加载记录
          this.loadHistoryRecords();
        },
        navigateToFolder(folderId, folderName) {
          this.fileManager.selectedFiles = []; // 清空选中的文件
          
          if (folderId === 'root') {
            // 回到根目录
            this.fileManager.paths = [];
          } else {
            // 查找是否已经在路径中
            const index = this.fileManager.paths.findIndex(path => path.fid === folderId);
            
            if (index !== -1) {
              // 如果已存在，截取到该位置
              this.fileManager.paths = this.fileManager.paths.slice(0, index + 1);
            } else {
              // 否则添加到路径
              this.fileManager.paths.push({
                fid: folderId,
                name: folderName
              });
            }
          }
          
          // 保存当前目录到localStorage（为每个账号单独保存）
          localStorage.setItem(`quarkAutoSave_fileManagerLastFolder_${this.fileManager.selectedAccountIndex}`, folderId);
          
          // 重置页码并加载新文件夹内容，不显示加载动画
          this.fileManager.currentPage = 1;
          this.loadFileListWithoutLoading(folderId);
        },
        selectFileForManager(event, file) {
          // 如果是文件夹，不允许选择
          if (file.dir) return;
          
          // 如果是Shift+点击，选择范围
          if (event.shiftKey && this.fileManager.selectedFiles.length > 0) {
            // 找出所有已选中文件的索引
            const selectedIndices = this.fileManager.selectedFiles.map(id => 
              this.fileManager.fileList.findIndex(file => file.fid === id)
            ).filter(index => index !== -1); // 过滤掉未找到的文件
            
            if (selectedIndices.length > 0) {
              // 找出已选中文件中最靠前的索引
              const earliestSelectedIndex = Math.min(...selectedIndices);
              // 确定最终的选择范围
              const startIndex = Math.min(earliestSelectedIndex, this.fileManager.fileList.indexOf(file));
              const endIndex = Math.max(earliestSelectedIndex, this.fileManager.fileList.indexOf(file));
              
              // 获取范围内所有文件的ID（排除文件夹）
              this.fileManager.selectedFiles = this.fileManager.fileList
                .slice(startIndex, endIndex + 1)
                .filter(file => !file.dir) // 只选择文件，不选择文件夹
                .map(file => file.fid);
            } else {
              // 如果没有有效的选中文件（可能是由于列表刷新），则只选择当前文件
              this.fileManager.selectedFiles = [file.fid];
            }
          } 
          // 如果是Ctrl/Cmd+点击，切换单个文件选择状态
          else if (event.ctrlKey || event.metaKey) {
            if (this.fileManager.selectedFiles.includes(file.fid)) {
              this.fileManager.selectedFiles = this.fileManager.selectedFiles.filter(id => id !== file.fid);
            } else {
              this.fileManager.selectedFiles.push(file.fid);
            }
          } 
          // 普通点击，清除当前选择并选择当前文件
          else {
            if (this.fileManager.selectedFiles.length === 1 && this.fileManager.selectedFiles.includes(file.fid)) {
              this.fileManager.selectedFiles = [];
            } else {
              this.fileManager.selectedFiles = [file.fid];
            }
          }
          
          // 更新最后选择的文件索引，只有在有选择文件时才更新
          if (this.fileManager.selectedFiles.length > 0) {
            this.fileManager.lastSelectedFileIndex = this.fileManager.fileList.indexOf(file);
          } else {
            this.fileManager.lastSelectedFileIndex = -1;
          }
          
          // 阻止事件冒泡
          event.stopPropagation();
        },
        formatFileSize(fileSize) {
          const units = ['B', 'KB', 'MB', 'GB', 'TB'];
          let size = fileSize;
          let unitIndex = 0;
          while (size >= 1024 && unitIndex < units.length - 1) {
            size /= 1024;
            unitIndex++;
          }
          return `${size.toFixed(2)} ${units[unitIndex]}`;
        },
        formatDate(date) {
          // 将时间戳转换为年-月-日 时:分:秒格式
          const d = new Date(date);
          const year = d.getFullYear();
          const month = String(d.getMonth() + 1).padStart(2, '0');
          const day = String(d.getDate()).padStart(2, '0');
          const hours = String(d.getHours()).padStart(2, '0');
          const minutes = String(d.getMinutes()).padStart(2, '0');
          const seconds = String(d.getSeconds()).padStart(2, '0');
          return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
        },
        // 统一解析资源发布日期为时间戳
        parsePublishTs(raw, source = null, pansouSource = null) {
          if (!raw) return 0;
          const s = String(raw).trim();
          
          // 判断是否需要+8小时：
          // 1. 如果是PanSou的tg来源，需要+8小时
          // 2. 如果是CloudSaver来源，需要+8小时
          // 3. 其他来源都不+8小时
          const needAdd8Hours = (pansouSource && pansouSource.startsWith('tg:')) || 
                               (source === 'CloudSaver');
          
          // YYYY-MM-DD HH:mm:ss
          let m = /^\s*(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s*$/.exec(s);
          if (m) {
            const [, y, mo, d, h, mi, se] = m;
            const date = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(se));
            if (needAdd8Hours) {
              date.setHours(date.getHours() + 8);
            }
            return date.getTime();
          }
          // YYYY-MM-DD
          m = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(s);
          if (m) {
            const [, y, mo, d] = m;
            const date = new Date(Number(y), Number(mo) - 1, Number(d), 0, 0, 0);
            if (needAdd8Hours) {
              date.setHours(date.getHours() + 8);
            }
            return date.getTime();
          }
          // ISO 回退
          const ts = Date.parse(s);
          if (isNaN(ts)) return 0;
          if (needAdd8Hours) {
            const date = new Date(ts);
            date.setHours(date.getHours() + 8);
            return date.getTime();
          }
          return ts;
        },
        // 规范化资源发布日期展示：将 ISO 格式（含 T/Z/偏移）转为 "YYYY-MM-DD HH:mm:ss"
        formatPublishDate(value, pansouSource = null, source = null) {
          if (!value) return '';
          const s = String(value).trim();
          
          // 已是标准格式则直接返回
          if (/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/.test(s)) return s;
          
          // 优先匹配 ISO 主体部分
          const m = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})/.exec(s);
          if (m) {
            const [, y, mo, d, h, mi, se] = m;
            // 判断是否需要+8小时：
            // 1. 如果是PanSou的tg来源，需要+8小时
            // 2. 如果是CloudSaver来源，需要+8小时
            // 3. 其他来源都不+8小时
            const needAdd8Hours = (pansouSource && pansouSource.startsWith('tg:')) || 
                                 (source === 'CloudSaver');
            
            if (needAdd8Hours) {
              // 创建Date对象并加上8小时
              const date = new Date(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(se));
              date.setHours(date.getHours() + 8);
              return date.getFullYear() + '-' + 
                     String(date.getMonth() + 1).padStart(2, '0') + '-' + 
                     String(date.getDate()).padStart(2, '0') + ' ' + 
                     String(date.getHours()).padStart(2, '0') + ':' + 
                     String(date.getMinutes()).padStart(2, '0') + ':' + 
                     String(date.getSeconds()).padStart(2, '0');
            }
            return `${y}-${mo}-${d} ${h}:${mi}:${se}`;
          }
          
          // 回退：简单替换T为空格并去除尾部Z/时区偏移
          let out = s.replace('T', ' ');
          out = out.replace(/Z$/i, '');
          out = out.replace(/([+-]\d{2}:?\d{2})$/i, '');
          
          // 判断是否需要+8小时
          const needAdd8Hours = (pansouSource && pansouSource.startsWith('tg:')) || 
                               (source === 'CloudSaver');
          
          if (needAdd8Hours) {
            try {
              const date = new Date(out);
              if (!isNaN(date.getTime())) {
                date.setHours(date.getHours() + 8);
                return date.getFullYear() + '-' + 
                       String(date.getMonth() + 1).padStart(2, '0') + '-' + 
                       String(date.getDate()).padStart(2, '0') + ' ' + 
                       String(date.getHours()).padStart(2, '0') + ':' + 
                       String(date.getMinutes()).padStart(2, '0') + ':' + 
                       String(date.getSeconds()).padStart(2, '0');
              }
            } catch (e) {
              // 如果转换失败，返回原始处理结果
            }
          }
          
          return out;
        },
        changeFolderPage(page) {
          if (page < 1) page = 1;
          if (page > this.fileManager.totalPages) page = this.fileManager.totalPages;
          
          // 更新当前页码
          this.fileManager.currentPage = page;
          this.fileManager.gotoPage = page;
          
          // 使用不显示加载动画的方式加载文件列表
          this.loadFileListWithoutLoading(this.fileManager.currentFolder);
        },
        getVisibleFolderPageNumbers() {
          const current = parseInt(this.fileManager.currentPage) || 1;
          const total = parseInt(this.fileManager.totalPages) || 1;
          // 根据屏幕宽度动态调整显示的页码数：移动端显示较少页码，桌面端显示较多页码
          const isMobile = window.innerWidth <= 768;
          const delta = isMobile ? 1 : 2; // 移动端左右各显示1个页码，桌面端左右各显示2个页码
          
          // 处理特殊情况
          if (total <= 1) return [];
          
          let range = [];
          
          // 移动端简化逻辑：只显示当前页附近的少数页码
          if (isMobile) {
            // 移动端：最多显示4个中间页码按钮（不包括第1页和最后1页），总共5个页码
            if (current <= 4) {
              // 当前页在前面时，显示 1, 2, 3, 4
              for (let i = 2; i <= Math.min(4, total - 1); i++) {
                range.push(i);
              }
            } else if (current >= total - 3) {
              // 当前页在后面时，显示倒数几页
              for (let i = Math.max(2, total - 3); i <= total - 1; i++) {
                range.push(i);
              }
            } else {
              // 当前页在中间时，显示当前页前后各1页，总共3个中间页码
              range.push(current - 1);
              range.push(current);
              range.push(current + 1);
            }
          } else {
            // 桌面端保持原有逻辑
            let rangeStart = Math.max(2, current - delta);
            let rangeEnd = Math.min(total - 1, current + delta);
            
            // 调整范围，确保显示足够的页码
            if (rangeEnd - rangeStart < delta * 2) {
              if (current - rangeStart < delta) {
                // 当前页靠近开始，扩展结束范围
                rangeEnd = Math.min(total - 1, rangeStart + delta * 2);
              } else {
                // 当前页靠近结束，扩展开始范围
                rangeStart = Math.max(2, rangeEnd - delta * 2);
              }
            }
            
            // 生成页码数组
            for (let i = rangeStart; i <= rangeEnd; i++) {
              range.push(i);
            }
          }
          
          return range;
        },
        isFilenameTruncated(file_name, index, field) {
          if (!this.fileManager.fileList[index]) return false;
          
          // 确保文本是字符串类型
          const str = String(file_name || '');
          
          // 先检查文本是否有足够长度，太短的文本肯定不需要展开
          if (str.length <= 20) return false;
          
          // 从文件的_isOverflowing属性中获取溢出状态
          const file = this.fileManager.fileList[index];
          
          // 如果已经展开了，那么我们认为它可能是需要截断的
          if (file._expanded) {
            return true;
          }
          
          return file._isOverflowing && file._isOverflowing[field];
        },
        toggleExpandFilename(file) {
          // 切换展开状态
          this.$set(file, '_expanded', !file._expanded);
          
          // 在下一个DOM更新周期中强制重新检测溢出状态
          this.$nextTick(() => {
            // 确保_isOverflowing属性存在
            if (!file._isOverflowing) {
              this.$set(file, '_isOverflowing', {});
            }
            
            // 在收起状态下，通过设置为true确保展开按钮依然可见
            if (!file._expanded) {
              this.$set(file._isOverflowing, 'file_name', true);
            }
          });
          
          // 确保视图更新
          this.$forceUpdate();
        },
        renameFile(file) {
          // 单个文件重命名
          const newName = prompt('请输入新的文件名：', file.file_name);
          if (newName && newName !== file.file_name) {
            axios.post('/batch_rename', {
              files: [{
                file_id: file.fid,
                new_name: newName
              }],
              // 添加账号索引参数，使用文件整理页面选中的账号
              account_index: this.fileManager.selectedAccountIndex
            })
              .then(response => {
                if (response.data.success) {
                  this.showToast('重命名成功');
                  // 刷新文件列表以确保缓存同步
                  this.refreshCurrentFolderCache();
                  // 如果命名预览模态框是打开的，也要刷新它
                  if ($('#fileSelectModal').hasClass('show')) {
                    this.showFileManagerNamingPreview(this.fileManager.currentFolder);
                  }
                } else {
                  alert(response.data.message || '重命名失败');
                }
              })
              .catch(error => {
                console.error('重命名文件失败:', error);
                alert('重命名文件失败');
              });
          }
        },
        previewAndRename() {
          // 显示文件整理页面的命名预览模态框
          this.showFileManagerNamingPreview();
        },
        
        showFileManagerNamingPreview(folderId) {
          // 显示文件整理页面的命名预览模态框（区别于任务配置的命名预览）
          this.fileSelect.selectShare = false;
          this.fileSelect.selectDir = true;
          this.fileSelect.previewRegex = true;
          this.fileSelect.fileList = [];
          this.fileSelect.paths = [];
          this.fileSelect.error = undefined;
          this.fileSelect.selectedFiles = [];
          // 预览默认按重命名列降序（与任务列表一致）
          this.fileSelect.sortBy = "file_name_re";
          this.fileSelect.sortOrder = "desc";
          // 展示当前文件夹的文件
          const currentFolderId = folderId || this.fileManager.currentFolder;
          // 模拟一个任务，用于预览
          const previewTask = {
            pattern: this.fileManager.pattern,
            replace: this.fileManager.replace,
            use_sequence_naming: this.fileManager.use_sequence_naming,
            use_episode_naming: this.fileManager.use_episode_naming,
            filterwords: this.fileManager.filterwords
          };
          // 存储临时任务索引
          this.fileSelect.index = -1; // 使用-1表示这是文件管理器的预览
          // 设置模态框标题类型
          document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'preview-filemanager');
          // 显示模态框
          if (!folderId) $('#fileSelectModal').modal('toggle'); // 仅首次打开时toggle
          // 获取文件列表并处理重命名预览
          this.modalLoading = true;
          // 获取重命名预览结果
          axios.get('/preview_rename', {
            params: {
              folder_id: currentFolderId,
              pattern: this.fileManager.pattern,
              replace: this.fileManager.replace,
              naming_mode: this.fileManager.use_sequence_naming ? 'sequence' : (this.fileManager.use_episode_naming ? 'episode' : 'regex'),
              include_folders: this.fileManager.include_folders,
              filterwords: this.fileManager.filterwords,
              // 添加页面大小参数，获取所有文件
              page_size: 99999,
              // 添加账号索引参数，使用文件整理页面选中的账号
              account_index: this.fileManager.selectedAccountIndex
            }
          })
          .then(response => {
            if (response.data.success) {
              // 处理预览结果
              const previewData = response.data.data || [];
              
              // 获取当前文件夹信息，包括路径
              axios.get('/file_list', {
                params: {
                  folder_id: currentFolderId,
                  sort_by: this.fileSelect.sortBy,
                  order: this.fileSelect.sortOrder,
                  // 使用一个较大的值以获取全部文件，不使用分页
                  page_size: 99999,
                  page: 1,
                  // 同时添加文件夹包含参数
                  include_folders: this.fileManager.include_folders,
                  // 添加账号索引参数，使用文件整理页面选中的账号
                  account_index: this.fileManager.selectedAccountIndex,
                  // 强制刷新缓存
                  force_refresh: true
                }
              })
              .then(listResponse => {
                if (listResponse.data.success) {
                  // 获取文件列表和路径
                  const fileList = listResponse.data.data.list;
                  this.fileSelect.paths = listResponse.data.data.paths || [];
                  
                  // 将重命名结果添加到文件列表
                  fileList.forEach(file => {
                    // 尝试通过ID匹配预览结果
                    let previewItem = previewData.find(item => item.file_id === file.fid);
                    
                    // 如果通过ID没有匹配到，尝试通过文件名匹配
                    if (!previewItem) {
                      previewItem = previewData.find(item => 
                        item.original_name === file.file_name && 
                        (!item.file_id || item.file_id === file.fid)
                      );
                    }
                    
                    if (previewItem) {
                      // 设置重命名字段
                      file.file_name_re = previewItem.new_name;
                      // 设置集数字段用于排序
                      file.episode_number = previewItem.episode_number;
                    } else {
                      file.file_name_re = null;
                      file.episode_number = undefined;
                    }
                  });
                  
                  // 更新文件列表
                  this.fileSelect.fileList = fileList;
                  
                  // 按照重命名结果排序，使用统一的排序逻辑
                  if (this.fileSelect.sortBy === 'file_name_re') {
                    this.manualSortFileList('file_name_re', this.fileSelect.sortOrder);
                  }
                } else {
                  this.fileSelect.error = listResponse.data.message || '获取文件列表失败';
                }
                this.modalLoading = false;
                
                // 检查滚动条
                this.$nextTick(() => {
                  this.checkPreviewScrollbar();
                });
              })
              .catch(error => {
                console.error('获取文件列表失败:', error);
                this.fileSelect.error = '获取文件列表失败，请稍后再试';
                this.modalLoading = false;
              });
            } else {
              this.fileSelect.error = response.data.message || '获取预览失败';
              this.modalLoading = false;
            }
            // 判断当前目录下是否还有可撤销的rename记录
            axios.get('/api/has_rename_record', {
              params: { save_path: currentFolderId }
            }).then(resp => {
              this.fileSelect.canUndoRename = !!(resp.data && resp.data.has_rename);
            }).catch(() => {
              this.fileSelect.canUndoRename = false;
            });
          })
          .catch(error => {
            console.error('获取预览失败:', error);
            this.fileSelect.error = '获取预览失败，请稍后再试';
            this.modalLoading = false;
          });
        },
        // showBatchRenameModal方法已删除，功能已整合到showFileManagerNamingPreview中
        // confirmBatchRename方法已删除，功能已整合到applyPreviewRename中
        deleteFile(file) {
          if (confirm(`确定要删除${file.dir ? '文件夹' : '文件'} "${file.file_name}" 吗？`)) {
            // 获取当前路径作为save_path参数
            let save_path = "";
            if (this.fileManager && this.fileManager.paths) {
              save_path = this.fileManager.paths.map(item => item.name).join("/");
            }
            
            axios.post('/delete_file', {
              fid: file.fid,
              file_name: file.file_name,
              delete_records: false,
              save_path: save_path,
              account_index: this.fileManager.selectedAccountIndex
            })
              .then(response => {
                if (response.data.code === 0) {
                  this.showToast('成功删除 1 个项目');
                  // 刷新文件列表以确保缓存同步
                  this.refreshCurrentFolderCache();
                  // 如果命名预览模态框是打开的，也要刷新它
                  if ($('#fileSelectModal').hasClass('show')) {
                    this.showFileManagerNamingPreview(this.fileManager.currentFolder);
                  }
                } else {
                  alert(response.data.message || '删除失败');
                }
              })
              .catch(error => {
                console.error('删除文件失败:', error);
                alert('删除文件失败');
              });
          }
        },
        // 开始重命名文件
        startRenameFile(file) {
          // 设置编辑状态
          this.$set(file, '_editing', true);
          this.$set(file, '_editingName', file.file_name);

          // 下一个tick后聚焦输入框并全选文本
          this.$nextTick(() => {
            const input = this.$refs['renameInput_' + file.fid];
            if (input) {
              input.focus();
              input.select();
            }
          });
        },
        // 开始移动文件
        startMoveFile(file) {
          // 如果是文件夹，不允许移动
          if (file.dir) {
            this.showToast('暂不支持移动文件夹');
            return;
          }

          // 设置移动模式相关参数
          this.fileSelect.moveMode = true;
          this.fileSelect.moveFileIds = this.fileManager.selectedFiles.length > 0 && this.fileManager.selectedFiles.includes(file.fid)
            ? this.fileManager.selectedFiles
            : [file.fid];
          this.fileSelect.selectShare = false;
          this.fileSelect.selectDir = true;
          this.fileSelect.previewRegex = false;
          this.fileSelect.error = undefined;
          this.fileSelect.fileList = [];
          this.fileSelect.paths = [];
          this.fileSelect.index = -1;

          // 重置排序状态为默认值 - 选择移动目标文件夹模态框默认修改时间倒序
          this.fileSelect.sortBy = "updated_at";
          this.fileSelect.sortOrder = "desc";

          // 设置模态框类型为move（移动目标文件夹）
          document.getElementById('fileSelectModal').setAttribute('data-modal-type', 'move');

          $('#fileSelectModal').modal('show');

          // 加载根目录
          this.getSavepathDetail(0);
        },
        // 保存重命名
        saveRenameFile(file) {
          if (!file._editing) return;

          const newName = file._editingName.trim();

          // 如果名称没有变化，直接取消编辑
          if (newName === file.file_name) {
            this.cancelRenameFile(file);
            return;
          }

          // 如果新名称为空，提示错误并恢复原始文件名，然后退出编辑状态
          if (!newName) {
            this.showToast('文件名不能为空');
            // 恢复原始文件名并退出编辑状态
            this.cancelRenameFile(file);
            return;
          }

          // 调用重命名API
          axios.post('/batch_rename', {
            files: [{
              file_id: file.fid,
              new_name: newName,
              old_name: file.file_name
            }],
            account_index: this.fileManager.selectedAccountIndex
          })
            .then(response => {
              if (response.data.success) {
                // 更新文件名
                file.file_name = newName;
                this.showToast('重命名成功');
                // 刷新文件列表以确保缓存同步
                this.refreshCurrentFolderCache();
                // 如果命名预览模态框是打开的，也要刷新它
                if ($('#fileSelectModal').hasClass('show')) {
                  this.showFileManagerNamingPreview(this.fileManager.currentFolder);
                }
              } else {
                this.showToast(response.data.message || '重命名失败');
              }
            })
            .catch(error => {
              console.error('重命名文件失败:', error);
              this.showToast('重命名文件失败');
            })
            .finally(() => {
              // 无论成功失败都退出编辑状态
              this.cancelRenameFile(file);
            });
        },
        // 取消重命名
        cancelRenameFile(file) {
          this.$set(file, '_editing', false);
          this.$set(file, '_editingName', '');
        },
        // 处理文件行点击事件
        handleFileRowClick(file, event) {
          // 如果文件正在编辑状态，不处理点击事件
          if (file._editing) {
            return;
          }

          // 检查是否有任何文件正在编辑状态
          const editingFile = this.fileManager.fileList.find(f => f._editing);
          if (editingFile) {
            // 如果有其他文件正在编辑，先保存编辑状态
            this.saveRenameFile(editingFile);
            return;
          }

          // 正常处理点击事件
          if (file.dir) {
            this.navigateToFolder(file.fid, file.file_name);
          } else {
            this.selectFileForManager(event, file);
          }
        },
        loadFileList(folderId) {
          this.fileManager.currentFolder = folderId || 'root';

          const params = {
            folder_id: folderId || 'root',
            sort_by: this.fileManager.sortBy,
            order: this.fileManager.sortOrder,
            page_size: this.fileManager.pageSize,
            page: this.fileManager.currentPage,
            account_index: this.fileManager.selectedAccountIndex
          };

          axios.get('/file_list', { params })
            .then(response => {
              if (response.data.success) {
                this.fileManager.fileList = response.data.data.list;
                this.fileManager.total = response.data.data.total;
                this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
                this.fileManager.paths = response.data.data.paths || [];
                this.fileManager.gotoPage = this.fileManager.currentPage;
              } else {
                alert(response.data.message || '获取文件列表失败');
              }
              this.fileManager.hasLoaded = true;
            })
            .catch(error => {
              console.error('获取文件列表失败:', error);
              this.fileManager.hasLoaded = true;
            });
        },
        sortFiles(field) {
          if (this.fileManager.sortBy === field) {
            // 如果已经按此字段排序，则切换排序方向
            this.fileManager.sortOrder = this.fileManager.sortOrder === 'asc' ? 'desc' : 'asc';
          } else {
            // 否则切换排序字段，并设置默认排序方向
            this.fileManager.sortBy = field;
            // 文件名默认按字母升序，日期和大小默认按降序
            this.fileManager.sortOrder = field === 'file_name' ? 'asc' : 'desc';
          }

          // 保存排序设置到localStorage
          const sortSettings = {
            sortBy: this.fileManager.sortBy,
            sortOrder: this.fileManager.sortOrder
          };
          localStorage.setItem('quarkAutoSave_fileManagerSort', JSON.stringify(sortSettings));

          // 使用不显示加载动画的方式加载文件列表
          this.loadFileListWithoutLoading(this.fileManager.currentFolder);
        },
        changeFileManagerPageSize(size) {
          // 先更新内存中的值
          this.fileManager.pageSize = size === 'all' ? 99999 : size;
          this.fileManager.currentPage = 1;
          
          // 然后保存到localStorage
          localStorage.setItem('quarkAutoSave_fileManagerPageSize', size.toString());
          
          // 最后重新加载文件列表
          this.loadFileListWithoutLoading(this.fileManager.currentFolder);
        },
        navigateToFolder(folderId, folderName) {
          this.fileManager.selectedFiles = []; // 清空选中的文件
          
          if (folderId === 'root') {
            // 回到根目录
            this.fileManager.paths = [];
          } else {
            // 查找是否已经在路径中
            const index = this.fileManager.paths.findIndex(path => path.fid === folderId);
            
            if (index !== -1) {
              // 如果已存在，截取到该位置
              this.fileManager.paths = this.fileManager.paths.slice(0, index + 1);
            } else {
              // 否则添加到路径
              this.fileManager.paths.push({
                fid: folderId,
                name: folderName
              });
            }
          }
          
          // 保存当前目录到localStorage（为每个账号单独保存）
          localStorage.setItem(`quarkAutoSave_fileManagerLastFolder_${this.fileManager.selectedAccountIndex}`, folderId);
          
          // 重置页码并加载新文件夹内容，不显示加载动画
          this.fileManager.currentPage = 1;
          this.loadFileListWithoutLoading(folderId);
        },
        
        // 添加一个不显示加载动画的文件列表加载方法
        loadFileListWithoutLoading(folderId) {
          this.fileManager.currentFolder = folderId || 'root';

          // 从localStorage读取分页大小设置（仅在初始化时使用）
          // 在changeFileManagerPageSize方法中，我们已经确保了正确的执行顺序
          const savedFileManagerPageSize = localStorage.getItem('quarkAutoSave_fileManagerPageSize');
          if (savedFileManagerPageSize && this.fileManager.pageSize === 15) { // 只在默认值时读取
            this.fileManager.pageSize = savedFileManagerPageSize === 'all' ? 99999 : parseInt(savedFileManagerPageSize);
          }

          const params = {
            folder_id: folderId || 'root',
            sort_by: this.fileManager.sortBy,
            order: this.fileManager.sortOrder,
            page_size: this.fileManager.pageSize,
            page: this.fileManager.currentPage,
            account_index: this.fileManager.selectedAccountIndex
          };

          axios.get('/file_list', { params })
            .then(response => {
              if (response.data.success) {
                this.fileManager.fileList = response.data.data.list;
                this.fileManager.total = response.data.data.total;
                this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
                this.fileManager.paths = response.data.data.paths || [];
                this.fileManager.gotoPage = this.fileManager.currentPage;
              } else {
                console.error('获取文件列表失败:', response.data.message);
              }

              // 检测当前的命名模式
              this.detectFileManagerNamingMode();
              this.fileManager.hasLoaded = true;
            })
            .catch(error => {
              console.error('获取文件列表失败:', error);
              this.fileManager.hasLoaded = true;
            });
        },

        // 添加带fallback机制的文件列表加载方法，用于账号切换
        loadFileListWithFallback(folderId) {
          this.fileManager.currentFolder = folderId || 'root';

          const params = {
            folder_id: folderId || 'root',
            sort_by: this.fileManager.sortBy,
            order: this.fileManager.sortOrder,
            page_size: this.fileManager.pageSize,
            page: this.fileManager.currentPage,
            account_index: this.fileManager.selectedAccountIndex
          };

          axios.get('/file_list', { params })
            .then(response => {
              if (response.data.success) {
                this.fileManager.fileList = response.data.data.list;
                this.fileManager.total = response.data.data.total;
                this.fileManager.totalPages = Math.ceil(response.data.data.total / this.fileManager.pageSize);
                this.fileManager.paths = response.data.data.paths || [];
                this.fileManager.gotoPage = this.fileManager.currentPage;

                // 检测当前的命名模式
                this.detectFileManagerNamingMode();
              } else {
                // 如果获取文件列表失败，可能是目录在新账号中不存在
                console.warn('当前目录在新账号中不存在，自动切换到根目录:', response.data.message);
                this.fallbackToRoot();
              }
            })
            .catch(error => {
              console.error('获取文件列表失败，自动切换到根目录:', error);
              this.fallbackToRoot();
            });
        },

        // fallback到根目录的方法
        fallbackToRoot() {
          this.fileManager.currentFolder = 'root';
          this.fileManager.paths = [];
          this.fileManager.currentPage = 1;

          // 更新localStorage中的当前目录（为每个账号单独保存）
          localStorage.setItem(`quarkAutoSave_fileManagerLastFolder_${this.fileManager.selectedAccountIndex}`, 'root');

          // 重新加载根目录
          this.loadFileListWithoutLoading('root');
        },
        getVisibleFolderPageNumbers() {
          const current = parseInt(this.fileManager.currentPage) || 1;
          const total = parseInt(this.fileManager.totalPages) || 1;
          // 根据屏幕宽度动态调整显示的页码数：移动端显示较少页码，桌面端显示较多页码
          const isMobile = window.innerWidth <= 768;
          const delta = isMobile ? 1 : 2; // 移动端左右各显示1个页码，桌面端左右各显示2个页码
          
          // 处理特殊情况
          if (total <= 1) return [];
          
          let range = [];
          
          // 移动端简化逻辑：只显示当前页附近的少数页码
          if (isMobile) {
            // 移动端：最多显示4个中间页码按钮（不包括第1页和最后1页），总共5个页码
            if (current <= 4) {
              // 当前页在前面时，显示 1, 2, 3, 4
              for (let i = 2; i <= Math.min(4, total - 1); i++) {
                range.push(i);
              }
            } else if (current >= total - 3) {
              // 当前页在后面时，显示倒数几页
              for (let i = Math.max(2, total - 3); i <= total - 1; i++) {
                range.push(i);
              }
            } else {
              // 当前页在中间时，显示当前页前后各1页，总共3个中间页码
              range.push(current - 1);
              range.push(current);
              range.push(current + 1);
            }
          } else {
            // 桌面端保持原有逻辑
            let rangeStart = Math.max(2, current - delta);
            let rangeEnd = Math.min(total - 1, current + delta);
            
            // 调整范围，确保显示足够的页码
            if (rangeEnd - rangeStart < delta * 2) {
              if (current - rangeStart < delta) {
                // 当前页靠近开始，扩展结束范围
                rangeEnd = Math.min(total - 1, rangeStart + delta * 2);
              } else {
                // 当前页靠近结束，扩展开始范围
                rangeStart = Math.max(2, rangeEnd - delta * 2);
              }
            }
            
            // 生成页码数组
            for (let i = rangeStart; i <= rangeEnd; i++) {
              range.push(i);
            }
          }
          
          return range;
        },
        // cancelBatchRename和applyBatchRename方法已删除，功能已整合到fileSelectModal中
        handleFileManagerOutsideClick(event) {
          // 如果当前不是文件整理页面或者没有选中的文件，则不处理
          if (this.activeTab !== 'filemanager' || this.fileManager.selectedFiles.length === 0) {
            return;
          }

          // 如果有文件正在编辑状态，不处理文件选择逻辑，让重命名处理器优先处理
          const editingFile = this.fileManager.fileList.find(file => file._editing);
          if (editingFile) {
            return;
          }

          // 检查点击是否在表格内
          const tableElement = document.querySelector('.table.selectable-files');
          // 检查点击是否在分页控制区域
          const paginationElement = document.querySelector('.pagination-container');

          // 如果点击不在表格内且不在分页控制区域内，则清除选择
          if (tableElement && !tableElement.contains(event.target) &&
              paginationElement && !paginationElement.contains(event.target)) {
            this.fileManager.selectedFiles = [];
            this.fileManager.lastSelectedFileIndex = -1;
          }
        },
        deleteSelectedFilesForManager(clickedFid, clickedFname, isDir, deleteRecords = false) {
          // 如果是文件夹或者没有选中的文件，则按原来的方式删除单个文件
          if (isDir || this.fileManager.selectedFiles.length === 0) {
            this.deleteFile({ fid: clickedFid, file_name: clickedFname, dir: isDir });
            return;
          }
          
          // 如果点击的文件不在选中列表中，也按原来的方式删除单个文件
          if (!this.fileManager.selectedFiles.includes(clickedFid)) {
            this.deleteFile({ fid: clickedFid, file_name: clickedFname, dir: isDir });
            return;
          }
          
          // 多选删除
          const selectedCount = this.fileManager.selectedFiles.length;
          
          // 根据选中数量使用不同的确认提示
          let confirmMessage = selectedCount === 1 
            ? `确定要删除此项目吗？` 
            : `确定要删除选中的 ${selectedCount} 个项目吗？`;
            
          if (confirm(confirmMessage)) {
            // 获取当前路径作为save_path参数
            let save_path = "";
            if (this.fileManager && this.fileManager.paths) {
              save_path = this.fileManager.paths.map(item => item.name).join("/");
            }
            
            // 创建一个Promise数组来处理所有删除请求
            const deletePromises = this.fileManager.selectedFiles.map(fid => {
              // 查找对应的文件对象，获取文件名
              const fileObj = this.fileManager.fileList.find(file => file.fid === fid);
              const fileName = fileObj ? fileObj.file_name : '';
              
              return axios.post('/delete_file', {
                fid: fid,
                file_name: fileName,
                delete_records: deleteRecords,
                save_path: save_path,
                account_index: this.fileManager.selectedAccountIndex
              })
                .then(response => {
                  return { fid: fid, success: response.data.code === 0, deleted_records: response.data.deleted_records || 0 };
                })
                .catch(error => {
                  return { fid: fid, success: false, deleted_records: 0 };
                });
            });
            
            // 等待所有删除请求完成
            Promise.all(deletePromises)
              .then(results => {
                // 统计成功和失败的数量
                const successCount = results.filter(r => r.success).length;
                const failCount = results.length - successCount;
                
                // 显示结果
                if (failCount > 0) {
                  alert(`成功删除 ${successCount} 个项目，${failCount} 个项目删除失败`);
                } else {
                  this.showToast(`成功删除 ${successCount} 个项目`);
                }

                // 如果有成功删除的文件，刷新文件列表以确保缓存同步
                if (successCount > 0) {
                  this.refreshCurrentFolderCache();
                  // 如果命名预览模态框是打开的，也要刷新它
                  if ($('#fileSelectModal').hasClass('show')) {
                    this.showFileManagerNamingPreview(this.fileManager.currentFolder);
                  }
                }
              });
          }
        },
        detectFileManagerNamingMode() {
          // 检测是否为顺序命名模式或剧集命名模式
          const currentValue = this.fileManager.pattern;
          
          if (currentValue !== undefined) {
            // 检查是否包含完整的{}占位符（确保不是分开的{和}）
            const isSequenceNaming = currentValue.includes('{}');
            
            // 如果不是顺序命名模式，检查是否包含完整的[]占位符
            const isEpisodeNaming = !isSequenceNaming && currentValue.includes('[]');
            
            // 处理模式切换
            if (isSequenceNaming) {
              // 切换到顺序命名模式
              this.fileManager.use_sequence_naming = true;
              this.fileManager.use_episode_naming = false;
            } else if (isEpisodeNaming) {
              // 切换到剧集命名模式
              this.fileManager.use_sequence_naming = false;
              this.fileManager.use_episode_naming = true;
            } else {
              // 切换到正则命名模式
              this.fileManager.use_sequence_naming = false;
              this.fileManager.use_episode_naming = false;
            }
          }
        },
        cancelPreviewRename() {
          $('#fileSelectModal').modal('hide');
        },
        async applyPreviewRename() {
          if (this.modalLoading) return;
          this.modalLoading = true;
          try {
            const renameList = this.fileSelect.fileList.filter(f => f.file_name_re && f.file_name_re !== '×' && !f.file_name_re.startsWith('×'));
            if (renameList.length === 0) {
              this.showToast('没有可重命名的项目');
              this.modalLoading = false;
              return;
            }
            // 只传 file_id 和 new_name，并加上 save_path
            const files = renameList.map(f => ({
              file_id: f.fid,
              new_name: f.file_name_re,
              old_name: f.file_name // 传递原文件名，便于撤销
            }));
            const save_path = this.fileManager.currentFolder;
            const response = await axios.post('/batch_rename', {
              files,
              save_path,
              // 添加账号索引参数，使用文件整理页面选中的账号
              account_index: this.fileManager.selectedAccountIndex
            });
            if (response.data.success) {
              this.showToast(`成功重命名 ${response.data.success_count || files.length} 个项目`);
              // 强制刷新文件列表缓存
              this.refreshCurrentFolderCache();
              $('#fileSelectModal').modal('hide');
              this.fileSelect.canUndoRename = true; // 重命名后立即可撤销
            } else {
              this.showToast(response.data.message || '重命名失败');
            }
          } catch (e) {
            this.showToast('重命名请求失败');
          }
          this.modalLoading = false;
        },
        async undoPreviewRename() {
          if (this.modalLoading) return;
          this.modalLoading = true;
          try {
            const save_path = this.fileManager.currentFolder;
            const response = await axios.post('/undo_rename', {
              save_path,
              // 添加账号索引参数，使用文件整理页面选中的账号
              account_index: this.fileManager.selectedAccountIndex
            });
            if (response.data.success) {
              this.showToast(`成功撤销 ${response.data.success_count} 个项目重命名`);
              this.showFileManagerNamingPreview(save_path);
              // 强制刷新文件列表缓存
              this.refreshCurrentFolderCache();
              // 撤销后如无可撤销项再设为false（由showFileManagerNamingPreview刷新）
            } else {
              this.showToast(response.data.message || '撤销失败');
              this.fileSelect.canUndoRename = false;
            }
          } catch (e) {
            this.showToast('撤销失败: ' + (e.message || e));
            this.fileSelect.canUndoRename = false;
          }
          this.modalLoading = false;
        },
        // 影视发现相关方法
        selectMainCategory(categoryKey) {
          this.discovery.selectedMainCategory = categoryKey;
          // 保存主榜单选择到localStorage
          localStorage.setItem('discovery_main_category', categoryKey);

          // 重置子分类为第一个选项
          const subCategories = this.discovery.subCategories[categoryKey];
          if (subCategories && subCategories.length > 0) {
            this.discovery.selectedSubCategory = subCategories[0].key;
            // 保存子榜单选择到localStorage
            localStorage.setItem('discovery_sub_category', subCategories[0].key);
          }
          // 加载新的榜单数据
          this.loadDiscoveryData();
        },
        selectSubCategory(subCategoryKey) {
          this.discovery.selectedSubCategory = subCategoryKey;
          // 保存子榜单选择到localStorage
          localStorage.setItem('discovery_sub_category', subCategoryKey);
          // 加载新的榜单数据
          this.loadDiscoveryData();
        },
        initializeDiscoverySelection() {
          // 验证从localStorage读取的主榜单是否有效
          const savedMainCategory = localStorage.getItem('discovery_main_category');
          const validMainCategories = this.discovery.mainCategories.map(cat => cat.key);

          if (savedMainCategory && validMainCategories.includes(savedMainCategory)) {
            this.discovery.selectedMainCategory = savedMainCategory;
          } else {
            // 如果无效，使用默认值并更新localStorage
            this.discovery.selectedMainCategory = 'movie_hot';
            localStorage.setItem('discovery_main_category', 'movie_hot');
          }

          // 验证从localStorage读取的子榜单是否有效
          const savedSubCategory = localStorage.getItem('discovery_sub_category');
          const validSubCategories = this.discovery.subCategories[this.discovery.selectedMainCategory];

          if (savedSubCategory && validSubCategories && validSubCategories.some(sub => sub.key === savedSubCategory)) {
            this.discovery.selectedSubCategory = savedSubCategory;
          } else {
            // 如果无效，使用该主榜单的第一个子榜单
            if (validSubCategories && validSubCategories.length > 0) {
              this.discovery.selectedSubCategory = validSubCategories[0].key;
              localStorage.setItem('discovery_sub_category', validSubCategories[0].key);
            }
          }
        },
        async loadDiscoveryData() {
          // 如果配置还没有加载完成，直接返回，不清空现有数据
          if (!this.formData.file_performance || this.formData.file_performance.discovery_items_count === undefined) {
            return;
          }

          // 标记为已初始化
          this.discovery.isInitialized = true;

          // 只有在首次加载时才显示加载状态，避免页面刷新时的闪烁
          if (!this.discovery.hasLoaded) {
            this.discovery.hasLoaded = false;
            this.discovery.items = [];
          }
          this.discovery.error = null;

          try {
            // 构建API路径 - 使用通用接口
            let apiPath = '';
            // 使用配置的影视榜单项目数量，如果没有配置则使用默认值30
            const itemsCount = this.formData.file_performance.discovery_items_count || 30;
            let params = { limit: itemsCount };
            const mainCategory = this.discovery.selectedMainCategory;
            const subCategory = this.discovery.selectedSubCategory;

            if (mainCategory.startsWith('movie_')) {
              apiPath = '/api/douban/movie/recent_hot';
              // 映射分类参数
              const categoryMapping = {
                'movie_hot': '热门',
                'movie_latest': '最新',
                'movie_top': '豆瓣高分',
                'movie_underrated': '冷门佳片'
              };
              params.category = categoryMapping[mainCategory] || '热门';
              params.type = subCategory;
            } else if (mainCategory.startsWith('tv_')) {
              apiPath = '/api/douban/tv/recent_hot';
              // 映射分类参数
              if (mainCategory === 'tv_drama') {
                params.category = 'tv';
                params.type = subCategory;
              } else if (mainCategory === 'tv_animation') {
                params.category = 'tv';
                params.type = subCategory; // 使用子榜的key值'动画'
              } else if (mainCategory === 'tv_variety') {
                params.category = 'show';
                params.type = subCategory;
              } else if (mainCategory === 'tv_documentary') {
                params.category = 'tv';
                params.type = subCategory; // 使用子榜的key值'纪录片'
              } else {
                params.category = 'tv';
                params.type = subCategory;
              }
            }

            const response = await axios.get(apiPath, {
              params: params
            });

            if (response.data.success) {
              this.discovery.items = response.data.data.items || [];
            } else {
              this.discovery.error = response.data.message || '获取榜单数据失败';
            }
          } catch (error) {
            console.error('获取豆瓣榜单失败:', error);
            this.discovery.error = '网络错误，请稍后重试';
          } finally {
            this.discovery.hasLoaded = true;
          }
        },
        openDoubanPage(item) {
          if (item.url) {
            window.open(item.url, '_blank');
          } else if (item.id) {
            // 构建豆瓣页面URL
            const doubanUrl = `https://movie.douban.com/subject/${item.id}/`;
            window.open(doubanUrl, '_blank');
          }
        },

        getMovieGenre(item) {
          // 从card_subtitle中提取类型信息
          // 格式: 年份 / 地区 / 类型 / 导演 / 主演
          if (item.card_subtitle) {
            const parts = item.card_subtitle.split(' / ');
            if (parts.length >= 3) {
              const genreText = parts[2]; // 第三部分是类型
              if (genreText && genreText.trim()) {
                // 将空格分隔的类型转换为斜杠分隔，并为斜杠添加特殊样式
                return genreText.replace(/\s+/g, ' <span class="genre-slash">/</span> ');
              }
            }
          }
          return '未知';
        },
        getMovieGenreText(item) {
          // 获取纯文本版本的类型信息，用于title属性
          if (item.card_subtitle) {
            const parts = item.card_subtitle.split(' / ');
            if (parts.length >= 3) {
              const genreText = parts[2]; // 第三部分是类型
              if (genreText && genreText.trim()) {
                // 将空格分隔的类型转换为斜杠分隔
                return genreText.replace(/\s+/g, ' / ');
              }
            }
          }
          return '未知';
        },
        getMovieDetails(item) {
          // 处理card_subtitle信息用于海报悬停显示
          if (item.card_subtitle) {
            const parts = item.card_subtitle.split(' / ');
            const details = [];

            if (parts.length >= 1 && parts[0]) {
              details.push(parts[0]); // 年份
            }
            if (parts.length >= 2 && parts[1]) {
              details.push(parts[1]); // 地区
            }
            if (parts.length >= 3 && parts[2]) {
              details.push(parts[2]); // 类型
            }
            if (parts.length >= 4 && parts[3]) {
              // 处理导演信息，最多显示两个导演
              const directors = parts[3].trim();
              if (directors) {
                const directorList = directors.split(/\s+/); // 按空格分割导演名字
                if (directorList.length > 2) {
                  // 如果导演超过2个，只取前两个
                  const limitedDirectors = directorList.slice(0, 2).join(' ');
                  details.push(limitedDirectors);
                } else {
                  // 导演不超过2个，直接显示
                  details.push(directors);
                }
              }
            }
            if (parts.length >= 5 && parts[4]) {
              details.push(parts[4]); // 主演
            }

            return details;
          }
          return [];
        },
        createGradientFromImage(imgElement) {
          // 从图片提取颜色生成渐变背景
          try {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');

            // 设置canvas尺寸
            canvas.width = imgElement.naturalWidth || imgElement.width || 200;
            canvas.height = imgElement.naturalHeight || imgElement.height || 300;

            // 绘制图片到canvas
            ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height);

            // 固定四个非对称点位
            const fixedPoints = [
              { x: Math.floor(canvas.width * 0.2), y: Math.floor(canvas.height * 0.15), pos: '20% 15%' },
              { x: Math.floor(canvas.width * 0.8), y: Math.floor(canvas.height * 0.35), pos: '80% 35%' },
              { x: Math.floor(canvas.width * 0.15), y: Math.floor(canvas.height * 0.7), pos: '15% 70%' },
              { x: Math.floor(canvas.width * 0.65), y: Math.floor(canvas.height * 0.85), pos: '65% 85%' }
            ];

            const colors = [];

            // 从四个固定点位提取颜色
            fixedPoints.forEach(point => {
              const pixel = ctx.getImageData(point.x, point.y, 1, 1).data;
              // 降低亮度以便更好显示文本
              const darkenFactor = 0.6; // 降低到60%亮度
              const r = Math.floor(pixel[0] * darkenFactor);
              const g = Math.floor(pixel[1] * darkenFactor);
              const b = Math.floor(pixel[2] * darkenFactor);
              const alpha = 0.9;
              colors.push(`rgba(${r}, ${g}, ${b}, ${alpha})`);
            });

            // 使用多个径向渐变叠加，模拟全方位渐变效果
            const gradients = [
              `radial-gradient(circle at 20% 15%, ${colors[0]} 0%, transparent 80%)`,
              `radial-gradient(circle at 80% 35%, ${colors[1]} 0%, transparent 80%)`,
              `radial-gradient(circle at 15% 70%, ${colors[2]} 0%, transparent 80%)`,
              `radial-gradient(circle at 65% 85%, ${colors[3]} 0%, transparent 80%)`,
              `radial-gradient(ellipse at center, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%)`
            ];

            return gradients.join(', ');

          } catch (error) {
            console.warn('无法从图片提取颜色:', error);
            // 降级为统一的深色背景（使用主题变量 --dark-text-color）
            return 'var(--dark-text-color)';
          }
        },
        handlePosterHover(event, item) {
          // 处理海报悬停事件
          const posterElement = event.currentTarget;
          const imgElement = posterElement.querySelector('img');
          const overlayElement = posterElement.querySelector('.discovery-poster-overlay');

          if (imgElement && overlayElement) {
            if (imgElement.complete) {
              const gradient = this.createGradientFromImage(imgElement);
              overlayElement.style.background = gradient;
            } else {
              // 图片未完成加载时，直接使用降级颜色
              overlayElement.style.background = 'var(--dark-text-color)';
            }
          }
        },

        // 内容管理页面的海报悬停处理
        handleManagementPosterHover(event, task) {
          const posterElement = event.currentTarget;
          const overlayElement = posterElement.querySelector('.discovery-poster-overlay');

          if (overlayElement) {
            // 检查任务是否匹配
            const isMatched = task.matched_show_name && task.matched_show_name.trim() !== '';
            
            if (isMatched) {
              // 匹配的任务：从图片提取颜色
              const imgElement = posterElement.querySelector('img');
              if (imgElement && imgElement.complete) {
                const gradient = this.createGradientFromImage(imgElement);
                overlayElement.style.background = gradient;
              } else {
                overlayElement.style.background = 'var(--dark-text-color)';
              }
            } else {
              // 未匹配的任务：使用固定颜色
              overlayElement.style.background = 'var(--dark-text-color)';
            }
          }
        },
        handleImageError(event) {
          // 图片加载失败时显示默认图片
          console.warn('海报加载失败:', event.target.src);
          event.target.src = '/static/images/no-poster.svg';
        },
        getProxiedImageUrl(originalUrl) {
          // 保持直连，发现页加载更快，且不需要取色跨域处理
          if (!originalUrl) return '/static/images/no-poster.svg';
          return originalUrl;
        },
        createTaskFromDiscovery(item) {
          // 从影视发现页面创建任务
          try {
            // 强制切换为创建模式并重置表单
            this.createTask.isEditMode = false;
            this.createTask.editTaskIndex = null;
            this.createTask.error = null;
            // 使用 newTask 的完整结构初始化，再由智能填充覆盖
            this.createTask.taskData = { ...this.newTask };
            // 应用系统配置的执行周期默认选项
            this.createTask.taskData.execution_mode = this.formData.execution_mode || 'manual';

            // 存储影视作品数据，并提取年份信息
            const movieData = { ...item };
            // 如果没有year字段，尝试从card_subtitle中提取
            if (!movieData.year && movieData.card_subtitle) {
              const yearFromSubtitle = this.extractYearFromCardSubtitle(movieData.card_subtitle);
              if (yearFromSubtitle) {
                movieData.year = yearFromSubtitle;
              }
            }
            this.createTask.movieData = movieData;

            // 智能填充任务数据
            this.smartFillTaskData(item, movieData);

            // 打开创建任务模态框
            $('#createTaskModal').modal('show');

            // 如果启用了自动搜索资源且配置了有效的搜索来源，自动触发资源搜索
            this.$nextTick(() => {
              if (this.formData.task_settings.auto_search_resources === 'enabled' &&
                  this.hasAnyValidSearchSource() &&
                  this.createTask.taskData.taskname) {
                this.searchSuggestions(-1, this.createTask.taskData.taskname);
              }
            });
          } catch (error) {
            console.error('创建任务时出错:', error);
          }
        },
        openCreateTaskModal() {
          // 从任务列表页面创建任务（不传递影视数据）
          try {
            // 清空影视数据
            this.createTask.movieData = null;
            // 重置编辑模式状态
            this.createTask.isEditMode = false;
            this.createTask.editTaskIndex = null;
            
            // 重置任务数据为默认值，使用 newTask 的完整结构
            this.createTask.taskData = { ...this.newTask };
            // 应用系统配置的执行周期默认选项
            this.createTask.taskData.execution_mode = this.formData.execution_mode || 'manual';
            
            // 如果有上一个任务，继承保存路径和命名规则
            if (this.formData.tasklist && this.formData.tasklist.length > 0) {
              // 使用新的筛选逻辑：根据当前视图筛选条件获取对应类型中编号最大的任务
              const lastTask = this.getLastTaskByCurrentFilter();
              if (lastTask) {
                this.createTask.taskData.savepath = lastTask.savepath || "";
                this.createTask.taskData.pattern = lastTask.pattern || "";
                this.createTask.taskData.replace = lastTask.replace || "";
                
                // 继承命名规则选择模式
                this.createTask.taskData.use_sequence_naming = lastTask.use_sequence_naming || false;
                this.createTask.taskData.use_episode_naming = lastTask.use_episode_naming || false;
                this.createTask.taskData.sequence_naming = lastTask.sequence_naming || "";
                this.createTask.taskData.episode_naming = lastTask.episode_naming || "";
              }
            }
            
            // 应用全局插件配置
            this.applyGlobalPluginConfig(this.createTask.taskData);
            
            // 打开创建任务模态框
            $('#createTaskModal').modal('show');
          } catch (error) {
            console.error('打开创建任务模态框时出错:', error);
          }
        },
        // 打开编辑任务模态框
        openEditTaskModal(taskIndex) {
          try {
            if (taskIndex === null || taskIndex === undefined || !this.formData.tasklist || !this.formData.tasklist[taskIndex]) {
              this.showToast('任务不存在');
              return;
            }
            
            const task = this.formData.tasklist[taskIndex];
            
            // 设置编辑模式
            this.createTask.isEditMode = true;
            this.createTask.editTaskIndex = taskIndex;
            this.createTask.movieData = null;
            this.createTask.error = null;
            
            // 复制任务数据到编辑表单
            this.createTask.taskData = { ...task };
            
            // 确保所有必要字段都有默认值
            this.createTask.taskData.runweek = task.runweek || [1, 2, 3, 4, 5, 6, 7];
            this.createTask.taskData.addition = task.addition || {};
            this.createTask.taskData.ignore_extension = task.ignore_extension || false;
            this.createTask.taskData.use_sequence_naming = task.use_sequence_naming || false;
            this.createTask.taskData.use_episode_naming = task.use_episode_naming || false;
            // 确保execution_mode有默认值
            if (!this.createTask.taskData.execution_mode) {
              this.createTask.taskData.execution_mode = this.formData.execution_mode || 'manual';
            }
            
            // 打开模态框
            $('#createTaskModal').modal('show');
          } catch (error) {
            console.error('打开编辑任务模态框时出错:', error);
            this.showToast('打开编辑任务失败');
          }
        },
        // 更新任务列表的元数据（用于热更新TMDB匹配信息）
        async updateTasklistMetadata(delay = 0) {
          const updateMetadata = async () => {
            try {
              // 获取最新的任务元数据，包括TMDB匹配信息
              const tasksResponse = await axios.get('/api/calendar/tasks');
              if (tasksResponse.data && tasksResponse.data.success) {
                this.calendar.tasks = tasksResponse.data.data.tasks || [];
                this.calendar.taskNames = this.calendar.tasks.map(task => task.task_name);
                
                // 重新构建任务映射，使用Vue.set确保响应式更新
                try {
                  // 先清空现有映射
                  this.$delete(this.calendar, 'taskMapByName');
                  this.$set(this.calendar, 'taskMapByName', {});
                  
                  (this.calendar.tasks || []).forEach(t => {
                    const key = (t.task_name || t.taskname || '').trim();
                    if (key) {
                      this.$set(this.calendar.taskMapByName, key, t);
                    }
                  });
                } catch (e) {
                  console.warn('构建任务映射失败:', e);
                }
                
                // 更新任务列表的内容类型，确保类型筛选按钮能热更新
                try {
                  const rawTypes = tasksResponse.data.data.content_types || [];
                  this.updateContentTypes(rawTypes);
                } catch (e) {
                  console.warn('更新内容类型失败:', e);
                }
                
                console.log('任务列表元数据已更新，包含TMDB匹配信息');
              }
            } catch (error) {
              console.error('更新任务列表元数据失败:', error);
            }
          };
          
          if (delay > 0) {
            // 延迟更新，给TMDB匹配一些时间
            setTimeout(updateMetadata, delay);
          } else {
            updateMetadata();
          }
        },
        hasAnyValidSearchSource() {
          const src = this.formData.source || {};
          const cs = src.cloudsaver || {};
          const ps = src.pansou || {};
          const csValid = cs.server && cs.username && cs.password;
          const psValid = ps.server;
          return !!(csValid || psValid);
        },
        smartFillTaskData(item, movieData) {
          // 智能填充任务数据
          const taskSettings = this.formData.task_settings || {};

          // 获取当前榜单类型
          const selectedMainCategory = this.discovery.selectedMainCategory;

          // 根据主分类确定榜单类型名称
          const categoryNameMap = {
            'movie_hot': '热门电影',
            'movie_latest': '最新电影',
            'movie_top': '豆瓣高分',
            'movie_underrated': '冷门佳片',
            'tv_drama': '热门剧集',
            'tv_animation': '热门动画',
            'tv_variety': '热门综艺',
            'tv_documentary': '热门纪录片'
          };

          const currentRanking = categoryNameMap[selectedMainCategory] || '热门电影';

          // 判断是否为电视类内容
          const isTvContent = currentRanking.includes('剧集') || currentRanking.includes('动画') ||
                             currentRanking.includes('综艺') || currentRanking.includes('纪录片');

          // 判断当前内容类型
          let contentType = 'movie'; // 默认为电影
          if (currentRanking.includes('剧集')) {
            contentType = 'tv';
          } else if (currentRanking.includes('动画')) {
            contentType = 'anime';
          } else if (currentRanking.includes('综艺')) {
            contentType = 'variety';
          } else if (currentRanking.includes('纪录片')) {
            contentType = 'documentary';
          }

          // 检查当前内容类型是否使用了自定义设置
          const isCustomSettings = this.isUsingCustomTaskSettingsForType(taskSettings, contentType);

          if (!isCustomSettings) {
            // 如果当前类型没有自定义设置，使用原有的继承逻辑
            this.createTask.taskData.taskname = item.title || "";

            // 继承最后一个任务的配置
            if (this.formData.tasklist && this.formData.tasklist.length > 0) {
              const lastTask = this.formData.tasklist[this.formData.tasklist.length - 1];
              this.createTask.taskData.savepath = lastTask.savepath || "";
              this.createTask.taskData.pattern = lastTask.pattern || "";
              this.createTask.taskData.replace = lastTask.replace || "";
              this.createTask.taskData.use_sequence_naming = lastTask.use_sequence_naming || false;
              this.createTask.taskData.use_episode_naming = lastTask.use_episode_naming || false;
              this.createTask.taskData.sequence_naming = lastTask.sequence_naming || "";
              this.createTask.taskData.episode_naming = lastTask.episode_naming || "";
            }

            // 但是对于电视类内容，总是应用电视命名规则
            if (isTvContent) {
              const title = item.title || "";
              const tvInfo = this.extractTvInfo(title);
              // 使用用户设置的电视命名规则，如果没有设置则使用默认值
              const namingRule = taskSettings.tv_naming_rule && taskSettings.tv_naming_rule.trim() !== ""
                ? taskSettings.tv_naming_rule
                : "剧名 - S季数E[]";

              const generatedNamingRule = this.generateTvNamingRule(namingRule, tvInfo.seriesName, tvInfo.season);

              this.createTask.taskData.taskname = tvInfo.seriesName;
              this.createTask.taskData.use_episode_naming = true;
              this.createTask.taskData.episode_naming = generatedNamingRule;
              this.createTask.taskData.pattern = generatedNamingRule; // 同时设置到pattern字段以便模态框显示
              this.createTask.taskData.ignore_extension = taskSettings.tv_ignore_extension !== undefined ? taskSettings.tv_ignore_extension : true; // 应用电视忽略后缀设置
            }

            // 设置其他默认值
            this.createTask.taskData.shareurl = "";
            this.createTask.taskData.enddate = "";
            this.createTask.taskData.runweek = [1, 2, 3, 4, 5, 6, 7];
            this.createTask.taskData.filterwords = "";
            this.createTask.taskData.startfid = "";
            this.createTask.taskData.update_subdir = "";

            // 设置默认的插件配置，并应用全局配置
            if (this.formData.task_plugins_config_default) {
              this.createTask.taskData.addition = { ...this.formData.task_plugins_config_default };
              this.applyGlobalPluginConfig(this.createTask.taskData);
            }
            return;
          }

          // 使用自定义设置进行智能填充

          // 提取影片信息
          const title = item.title || "";
          const year = movieData.year || "";

          // 获取对应的配置
          let savePathTemplate = taskSettings.movie_save_path;
          let namingRule = "";
          let useEpisodeNaming = false;

          // 根据内容类型获取对应的配置
          if (contentType === 'tv') {
            savePathTemplate = taskSettings.tv_save_path;
            namingRule = taskSettings.tv_naming_rule;
            useEpisodeNaming = true;
          } else if (contentType === 'anime') {
            savePathTemplate = taskSettings.anime_save_path;
            namingRule = taskSettings.tv_naming_rule;
            useEpisodeNaming = true;
          } else if (contentType === 'variety') {
            savePathTemplate = taskSettings.variety_save_path;
            namingRule = taskSettings.tv_naming_rule;
            useEpisodeNaming = true;
          } else if (contentType === 'documentary') {
            savePathTemplate = taskSettings.documentary_save_path;
            namingRule = taskSettings.tv_naming_rule;
            useEpisodeNaming = true;
          }

          // 处理电影类型
          if (contentType === 'movie') {
            this.createTask.taskData.taskname = title;
            this.createTask.taskData.savepath = this.generateMovieSavePath(savePathTemplate, title, year);

            // 应用电影命名规则
            const movieNamingPattern = taskSettings.movie_naming_pattern && taskSettings.movie_naming_pattern.trim() !== ""
              ? taskSettings.movie_naming_pattern
              : "";
            const movieNamingReplace = taskSettings.movie_naming_replace && taskSettings.movie_naming_replace.trim() !== ""
              ? taskSettings.movie_naming_replace
              : "";

            if (movieNamingPattern && movieNamingReplace) {
              // 生成智能填充的替换表达式
              const generatedReplace = this.generateMovieNamingRule(movieNamingReplace, title, year);
              this.createTask.taskData.pattern = movieNamingPattern;
              this.createTask.taskData.replace = generatedReplace;
            } else {
              this.createTask.taskData.pattern = "";
              this.createTask.taskData.replace = "";
            }

            this.createTask.taskData.use_sequence_naming = false;
            this.createTask.taskData.use_episode_naming = false;
            this.createTask.taskData.sequence_naming = "";
            this.createTask.taskData.episode_naming = "";
          } else {
            // 处理电视类型（剧集、动画、综艺、纪录片）
            const tvInfo = this.extractTvInfo(title);
            this.createTask.taskData.taskname = tvInfo.seriesName;
            this.createTask.taskData.savepath = this.generateTvSavePath(savePathTemplate, tvInfo.seriesName, year, tvInfo.season, tvInfo.isFirstSeason);
            this.createTask.taskData.pattern = "";
            this.createTask.taskData.replace = "";
            this.createTask.taskData.use_sequence_naming = false;
            const generatedNamingRule = this.generateTvNamingRule(namingRule, tvInfo.seriesName, tvInfo.season);

            this.createTask.taskData.use_episode_naming = useEpisodeNaming;
            this.createTask.taskData.sequence_naming = "";
            this.createTask.taskData.episode_naming = generatedNamingRule;
            this.createTask.taskData.pattern = generatedNamingRule; // 同时设置到pattern字段以便模态框显示

            // 只有电视类内容才应用电视忽略后缀设置
            if (useEpisodeNaming) {
              this.createTask.taskData.ignore_extension = taskSettings.tv_ignore_extension !== undefined ? taskSettings.tv_ignore_extension : true;
            } else {
              // 电影类内容保持默认的false
              this.createTask.taskData.ignore_extension = false;
            }
          }

          // 设置其他默认值
          this.createTask.taskData.shareurl = "";
          this.createTask.taskData.enddate = "";
          this.createTask.taskData.runweek = [1, 2, 3, 4, 5, 6, 7];
          this.createTask.taskData.filterwords = "";
          this.createTask.taskData.startfid = "";
          this.createTask.taskData.update_subdir = "";

          // 设置默认的插件配置，并应用全局配置
          if (this.formData.task_plugins_config_default) {
            this.createTask.taskData.addition = { ...this.formData.task_plugins_config_default };
            this.applyGlobalPluginConfig(this.createTask.taskData);
          }
        },
        isUsingCustomTaskSettingsForType(taskSettings, contentType) {
          // 检查特定内容类型是否使用了自定义任务设置
          const defaultSettings = {
            movie: "电影目录前缀/片名 (年份)",
            tv: "剧集目录前缀/剧名 (年份)/剧名 - S季数",
            anime: "动画目录前缀/剧名 (年份)/剧名 - S季数",
            variety: "综艺目录前缀/剧名 (年份)/剧名 - S季数",
            documentary: "纪录片目录前缀/剧名 (年份)/剧名 - S季数"
          };

          const settingKeys = {
            movie: 'movie_save_path',
            tv: 'tv_save_path',
            anime: 'anime_save_path',
            variety: 'variety_save_path',
            documentary: 'documentary_save_path'
          };

          const settingKey = settingKeys[contentType];
          if (!settingKey) return false;

          const userValue = taskSettings[settingKey] || "";
          const defaultValue = defaultSettings[contentType];

          // 如果用户设置了非空且与默认值不同的值，则认为是自定义设置
          return userValue && userValue !== defaultValue;
        },
        isUsingCustomTaskSettings(taskSettings) {
          // 检查是否使用了自定义任务设置（非默认模板）
          // 注意：电视命名规则总是生效，不参与自定义判断
          const defaultSettings = {
            movie_save_path: "电影目录前缀/片名 (年份)",
            tv_save_path: "剧集目录前缀/剧名 (年份)/剧名 - S季数",
            anime_save_path: "动画目录前缀/剧名 (年份)/剧名 - S季数",
            variety_save_path: "综艺目录前缀/剧名 (年份)/剧名 - S季数",
            documentary_save_path: "纪录片目录前缀/剧名 (年份)/剧名 - S季数"
            // tv_naming_rule 不参与判断，总是生效
          };

          // 检查每个设置是否与默认值不同且不为空
          for (const key in defaultSettings) {
            const userValue = taskSettings[key] || "";
            const defaultValue = defaultSettings[key];

            // 如果用户设置了非空且与默认值不同的值，则认为是自定义设置
            if (userValue && userValue !== defaultValue) {
              return true;
            }
          }

          return false;
        },
        extractTvInfo(title) {
          // 从标题中提取电视剧信息（剧名、季数等）
          // 复用cleanTaskNameForSearch的逻辑
          if (!title) return { seriesName: '', season: '01', isFirstSeason: true };

          // 清理任务名称中的连续空格和特殊符号
          let cleanedName = title.replace(/\u3000/g, ' ').replace(/\t/g, ' ');
          cleanedName = cleanedName.replace(/\s+/g, ' ').trim();

          // 匹配常见的季数格式（与cleanTaskNameForSearch保持一致）
          const seasonPatterns = [
            /^(.*?)[\s\.\-_]+S(\d+)$/i,  // 黑镜 - S07、折腰.S01、音你而来-S02
            /^(.*?)[\s\.\-_]+Season\s*(\d+)$/i,  // 黑镜 - Season 1
            /^(.*?)\s+S(\d+)$/i,  // 快乐的大人 S02
            /^(.*?)[\s\.\-_]+S(\d+)E\d+$/i,  // 处理 S01E01 格式
            /^(.*?)\s+第\s*(\d+)\s*季$/i,  // 处理 第N季 格式
            /^(.*?)[\s\.\-_]+第\s*(\d+)\s*季$/i,  // 处理 - 第N季 格式
            /^(.*?)\s+第([一二三四五六七八九十零]+)季$/i,  // 处理 第一季、第二季 格式
            /^(.*?)[\s\.\-_]+第([一二三四五六七八九十零]+)季$/i,  // 处理 - 第一季、- 第二季 格式
          ];

          for (const pattern of seasonPatterns) {
            const match = cleanedName.match(pattern);
            if (match) {
              let seriesName = match[1].trim();
              // 去除末尾可能残留的分隔符
              seriesName = seriesName.replace(/[\s\.\-_]+$/, '');

              let seasonNum = match[2];
              // 处理中文数字
              if (isNaN(seasonNum)) {
                const chineseNumbers = {
                  '一': 1, '二': 2, '三': 3, '四': 4, '五': 5,
                  '六': 6, '七': 7, '八': 8, '九': 9, '十': 10, '零': 0
                };
                seasonNum = chineseNumbers[seasonNum] || 1;
              }
              seasonNum = parseInt(seasonNum);

              return {
                seriesName: seriesName,
                season: seasonNum.toString().padStart(2, '0'),
                isFirstSeason: seasonNum === 1
              };
            }
          }

          // 如果没有匹配到季数格式，返回原标题作为剧名，默认第一季
          return {
            seriesName: cleanedName,
            season: '01',
            isFirstSeason: true
          };
        },

        // 加载“今日更新”的本地数据，用于指示当日转存的剧集/任务
        async loadTodayUpdatesLocal() {
          try {
            const res = await axios.get('/api/calendar/today_updates_local');
            if (res.data && res.data.success) {
              const items = res.data.data && res.data.data.items ? res.data.data.items : [];
              const byTask = {};
              const byShow = {};
              const makeKey = (it) => {
                if (it && it.episode_number != null && it.season_number != null) {
                  const s = String(it.season_number).padStart(2, '0');
                  const e = String(it.episode_number).padStart(2, '0');
                  return `S${s}E${e}`;
                }
                if (it && it.air_date) {
                  return `D:${it.air_date}`;
                }
                return null;
              };
              items.forEach(it => {
                if (it && it.task_name) byTask[it.task_name] = true;
                const key = makeKey(it);
                const sname = (it && it.show_name) ? String(it.show_name).trim() : '';
                if (sname && key) {
                  if (!byShow[sname]) byShow[sname] = new Set();
                  byShow[sname].add(key);
                }
              });
              // 将 Set 序列化为对象数组以便 Vue 响应式
              const byShowObj = {};
              Object.keys(byShow).forEach(k => { byShowObj[k] = Array.from(byShow[k]); });
              this.calendar.todayUpdatesByTaskName = byTask;
              this.calendar.todayUpdatesByShow = byShowObj;
            }
          } catch (e) {
            // 忽略错误，维持上次数据
          }
        },

        // 判断剧集是否属于“今日更新”的已转存集（按剧名 + SxxExx / 日期 匹配）
        isEpisodeUpdatedToday(episode) {
          try {
            if (!episode) return false;
            const sname = (episode.show_name || '').trim();
            if (!sname) return false;
            const list = this.calendar.todayUpdatesByShow && this.calendar.todayUpdatesByShow[sname];
            if (!list || list.length === 0) return false;
            // 拆分模式：逐集匹配
            const makeKey = (ep) => {
              if (ep && ep.episode_number != null && ep.season_number != null) {
                const s = String(ep.season_number).padStart(2, '0');
                const e = String(ep.episode_number).padStart(2, '0');
                return `S${s}E${e}`;
              }
              if (ep && ep.air_date) return `D:${ep.air_date}`;
              return null;
            };
            if (!episode.is_merged) {
              const key = makeKey(episode);
              if (!key) return false;
              return list.includes(key);
            }
            // 合并模式：任一原始集命中即可
            if (Array.isArray(episode.original_episodes) && episode.original_episodes.length > 0) {
              return episode.original_episodes.some(ep => {
                const key = makeKey({
                  season_number: ep.season_number || episode.season_number,
                  episode_number: ep.episode_number,
                  air_date: ep.air_date || episode.air_date
                });
                return key && list.includes(key);
              });
            }
            // 兜底：用合并后的显示信息尝试一次
            const key = makeKey(episode);
            return key ? list.includes(key) : false;
          } catch (e) { return false; }
        },

        // 判断管理视图中的任务是否有当日更新
        isCalendarTaskUpdatedToday(task) {
          try {
            const name = task && (task.task_name || task.taskname);
            if (!name) return false;
            return !!(this.calendar.todayUpdatesByTaskName && this.calendar.todayUpdatesByTaskName[name]);
          } catch (e) { return false; }
        },
        generateMovieSavePath(template, title, year) {
          // 生成电影保存路径
          let savePath = template;

          // 替换片名
          savePath = savePath.replace(/片名/g, title);

          // 替换年份
          if (year) {
            savePath = savePath.replace(/年份/g, year);
          } else {
            // 如果没有年份，移除年份相关的括号
            savePath = savePath.replace(/\s*\(年份\)/g, '');
          }

          return savePath;
        },
        generateTvSavePath(template, seriesName, year, season, isFirstSeason) {
          // 生成电视剧保存路径
          let savePath = template;

          // 替换剧名
          savePath = savePath.replace(/剧名/g, seriesName);

          // 替换季数
          savePath = savePath.replace(/季数/g, season);

          // 处理年份：如果不是第一季，忽略年份信息
          if (isFirstSeason && year) {
            savePath = savePath.replace(/年份/g, year);
          } else {
            // 移除年份相关的部分
            savePath = savePath.replace(/\s*\(年份\)\//g, '/');
            savePath = savePath.replace(/\s*\(年份\)/g, '');
          }

          return savePath;
        },
        generateTvNamingRule(template, seriesName, season) {
          // 生成电视剧命名规则
          let namingRule = template;

          // 替换剧名
          namingRule = namingRule.replace(/剧名/g, seriesName);

          // 替换季数
          namingRule = namingRule.replace(/季数/g, season);

          return namingRule;
        },
        generateMovieNamingRule(replaceTemplate, movieTitle, year) {
          // 生成电影命名规则
          let namingRule = replaceTemplate;

          // 替换片名
          namingRule = namingRule.replace(/片名/g, movieTitle);

          // 替换年份
          if (year) {
            namingRule = namingRule.replace(/年份/g, year);
          } else {
            // 如果没有年份，移除包含年份的部分
            namingRule = namingRule.replace(/\s*\(年份\)/g, '');
            namingRule = namingRule.replace(/\s*（年份）/g, '');
          }

          // 注意：正则表达式的反向引用（如\2）保持不变，将在实际重命名时由正则引擎处理

          return namingRule;
        },
        generateCustomFolderPath(taskData) {
          // 根据任务设置生成自定义文件夹路径
          const taskSettings = this.formData.task_settings || {};
          const movieData = this.createTask.movieData;

          if (!movieData) {
            return taskData.taskname; // 如果没有影视数据，使用任务名称
          }

          // 获取当前榜单类型
          const selectedMainCategory = this.discovery.selectedMainCategory;
          const categoryNameMap = {
            'movie_hot': '热门电影',
            'movie_latest': '最新电影',
            'movie_top': '豆瓣高分',
            'movie_underrated': '冷门佳片',
            'tv_drama': '热门剧集',
            'tv_animation': '热门动画',
            'tv_variety': '热门综艺',
            'tv_documentary': '热门纪录片'
          };

          const currentRanking = categoryNameMap[selectedMainCategory] || '热门电影';

          // 判断内容类型
          let contentType = 'movie';
          if (currentRanking.includes('剧集')) {
            contentType = 'tv';
          } else if (currentRanking.includes('动画')) {
            contentType = 'anime';
          } else if (currentRanking.includes('综艺')) {
            contentType = 'variety';
          } else if (currentRanking.includes('纪录片')) {
            contentType = 'documentary';
          }

          // 检查是否有自定义路径设置
          let savePathTemplate = '';
          if (contentType === 'tv') {
            savePathTemplate = taskSettings.tv_save_path;
          } else if (contentType === 'anime') {
            savePathTemplate = taskSettings.anime_save_path;
          } else if (contentType === 'variety') {
            savePathTemplate = taskSettings.variety_save_path;
          } else if (contentType === 'documentary') {
            savePathTemplate = taskSettings.documentary_save_path;
          } else if (contentType === 'movie') {
            savePathTemplate = taskSettings.movie_save_path;
          }

          // 如果没有自定义路径设置，使用任务名称
          if (!savePathTemplate || savePathTemplate.trim() === '') {
            return taskData.taskname;
          }

          // 提取相对路径部分（去掉前缀）
          const pathParts = savePathTemplate.split('/');
          let relativePath = '';

          // 找到第一个包含模板变量的部分开始
          for (let i = 0; i < pathParts.length; i++) {
            if (pathParts[i].includes('片名') || pathParts[i].includes('剧名')) {
              relativePath = pathParts.slice(i).join('/');
              break;
            }
          }

          if (!relativePath) {
            return taskData.taskname;
          }

          const title = movieData.title || taskData.taskname;
          const year = movieData.year || '';

          if (contentType === 'movie') {
            // 电影类型：使用generateMovieSavePath的逻辑
            return this.generateMovieSavePath(relativePath, title, year);
          } else {
            // 电视类型：使用generateTvSavePath的逻辑
            const tvInfo = this.extractTvInfo(title);
            return this.generateTvSavePath(relativePath, tvInfo.seriesName, year, tvInfo.season, tvInfo.isFirstSeason);
          }
        },
        extractYearFromCardSubtitle(cardSubtitle) {
          // 从card_subtitle中提取年份
          // 格式: 年份 / 地区 / 类型 / 导演 / 主演
          if (cardSubtitle) {
            const parts = cardSubtitle.split(' / ');
            if (parts.length >= 1 && parts[0]) {
              const yearPart = parts[0].trim();
              // 匹配4位数字年份
              const yearMatch = yearPart.match(/(\d{4})/);
              if (yearMatch) {
                return yearMatch[1];
              }
            }
          }
          return null;
        },
        cancelCreateTask() {
          // 取消创建任务
          $('#createTaskModal').modal('hide');
          this.createTask.movieData = null;
          this.createTask.error = null;
          // 重置编辑模式状态
          this.createTask.isEditMode = false;
          this.createTask.editTaskIndex = null;
          // 重置任务数据，使用 newTask 的完整结构
          this.createTask.taskData = { ...this.newTask };
        },
        confirmCreateTask() {
          // 确认创建任务
          if (this.createTask.loading) return;

          // 验证必填字段
          if (!this.createTask.taskData.taskname.trim()) {
            this.createTask.error = '任务名称不能为空';
            return;
          }
          if (!this.createTask.taskData.shareurl.trim()) {
            this.createTask.error = '分享链接不能为空';
            return;
          }
          if (!this.createTask.taskData.savepath.trim()) {
            this.createTask.error = '保存路径不能为空';
            return;
          }

          this.createTask.loading = true;
          this.createTask.error = null;

          // 创建新任务
          const newTask = { ...this.createTask.taskData };

          // 应用全局插件配置
          this.applyGlobalPluginConfig(newTask);

          // 处理命名模式
          if (newTask.use_sequence_naming) {
            newTask.pattern = newTask.sequence_naming;
          } else if (newTask.use_episode_naming) {
            newTask.pattern = newTask.episode_naming;
          }

          // 添加到任务列表
          if (!this.formData.tasklist) {
            this.formData.tasklist = [];
          }
          this.formData.tasklist.push(newTask);

          // 保存配置（不显示配置更新消息）
          axios.post('/update', this.formData)
            .then(response => {
              if (response.data.success) {
                this.configModified = false;
                // 显示任务创建成功消息
                this.showToast('任务创建成功', 'success');
                // 保存成功后更新用户信息
                this.fetchUserInfo();
                // 更新任务列表元数据，确保海报视图能显示TMDB匹配信息
                this.updateTasklistMetadata();
                this.createTask.loading = false;
                this.cancelCreateTask();
              } else {
                // 错误信息使用alert，确保用户看到
                alert(response.data.message);
                this.createTask.loading = false;
              }
            })
            .catch(error => {
              // 错误处理
              alert("保存失败: " + (error.response?.data?.message || error.message || "未知错误"));
              this.createTask.loading = false;
            });
        },
        confirmEditTask() {
          // 确认编辑任务
          if (this.createTask.loading) return;

          // 验证必填字段
          if (!this.createTask.taskData.taskname.trim()) {
            this.createTask.error = '任务名称不能为空';
            return;
          }
          if (!this.createTask.taskData.shareurl.trim()) {
            this.createTask.error = '分享链接不能为空';
            return;
          }
          if (!this.createTask.taskData.savepath.trim()) {
            this.createTask.error = '保存路径不能为空';
            return;
          }

          this.createTask.loading = true;
          this.createTask.error = null;

          // 更新任务数据
          const updatedTask = { ...this.createTask.taskData };

          // 应用全局插件配置
          this.applyGlobalPluginConfig(updatedTask);

          // 处理命名模式
          if (updatedTask.use_sequence_naming) {
            updatedTask.pattern = updatedTask.sequence_naming;
          } else if (updatedTask.use_episode_naming) {
            updatedTask.pattern = updatedTask.episode_naming;
          }

          // 更新任务列表中的任务
          if (this.createTask.editTaskIndex !== null && this.formData.tasklist && this.formData.tasklist[this.createTask.editTaskIndex]) {
            this.$set(this.formData.tasklist, this.createTask.editTaskIndex, updatedTask);
          }

          // 保存配置（不显示配置更新消息）
          axios.post('/update', this.formData)
            .then(response => {
              if (response.data.success) {
                this.configModified = false;
                // 显示任务编辑成功消息
                this.showToast('任务编辑成功', 'success');
                // 保存成功后更新用户信息
                this.fetchUserInfo();
                // 更新任务列表元数据，确保海报视图能显示TMDB匹配信息
                this.updateTasklistMetadata();
                this.createTask.loading = false;
                this.cancelCreateTask();
              } else {
                // 错误信息使用alert，确保用户看到
                alert(response.data.message);
                this.createTask.loading = false;
              }
            })
            .catch(error => {
              // 错误处理
              alert("保存失败: " + (error.response?.data?.message || error.message || "未知错误"));
              this.createTask.loading = false;
            });
        },
        confirmEditAndRunTask() {
          // 确认编辑任务并立即运行
          if (this.createTask.loading) return;

          // 复用与编辑验证相同的逻辑
          if (!this.createTask.taskData.taskname.trim()) {
            this.createTask.error = '任务名称不能为空';
            return;
          }
          if (!this.createTask.taskData.shareurl.trim()) {
            this.createTask.error = '分享链接不能为空';
            return;
          }
          if (!this.createTask.taskData.savepath.trim()) {
            this.createTask.error = '保存路径不能为空';
            return;
          }

          this.createTask.loading = true;
          this.createTask.error = null;

          // 更新任务数据
          const updatedTask = { ...this.createTask.taskData };

          // 应用全局插件配置
          this.applyGlobalPluginConfig(updatedTask);

          // 处理命名模式
          if (updatedTask.use_sequence_naming) {
            updatedTask.pattern = updatedTask.sequence_naming;
          } else if (updatedTask.use_episode_naming) {
            updatedTask.pattern = updatedTask.episode_naming;
          }

          // 更新任务列表中的任务
          if (this.createTask.editTaskIndex !== null && this.formData.tasklist && this.formData.tasklist[this.createTask.editTaskIndex]) {
            this.$set(this.formData.tasklist, this.createTask.editTaskIndex, updatedTask);
          }

          // 保存配置
          axios.post('/update', this.formData)
            .then(response => {
              if (response.data.success) {
                this.configModified = false;
                this.showToast('任务编辑成功并开始运行', 'success');
                this.fetchUserInfo();
                this.updateTasklistMetadata();
                const taskIndex = this.createTask.editTaskIndex;
                this.createTask.loading = false;
                this.cancelCreateTask();
                // 稍后运行任务，保证模态框关闭完成
                setTimeout(() => {
                  if (taskIndex !== null && taskIndex >= 0) {
                    this.runScriptNow(taskIndex);
                  }
                }, 300);
              } else {
                alert(response.data.message);
                this.createTask.loading = false;
              }
            })
            .catch(error => {
              alert("保存失败: " + (error.response?.data?.message || error.message || "未知错误"));
              this.createTask.loading = false;
            });
        },
        confirmCreateAndRunTask() {
          // 确认创建并运行任务
          if (this.createTask.loading) return;

          // 验证必填字段
          if (!this.createTask.taskData.taskname.trim()) {
            this.createTask.error = '任务名称不能为空';
            return;
          }
          if (!this.createTask.taskData.shareurl.trim()) {
            this.createTask.error = '分享链接不能为空';
            return;
          }
          if (!this.createTask.taskData.savepath.trim()) {
            this.createTask.error = '保存路径不能为空';
            return;
          }

          this.createTask.loading = true;
          this.createTask.error = null;

          // 创建新任务
          const newTask = { ...this.createTask.taskData };

          // 应用全局插件配置
          this.applyGlobalPluginConfig(newTask);

          // 处理命名模式
          if (newTask.use_sequence_naming) {
            newTask.pattern = newTask.sequence_naming;
          } else if (newTask.use_episode_naming) {
            newTask.pattern = newTask.episode_naming;
          }

          // 添加到任务列表
          if (!this.formData.tasklist) {
            this.formData.tasklist = [];
          }
          this.formData.tasklist.push(newTask);

          // 保存配置（不显示配置更新消息）
          axios.post('/update', this.formData)
            .then(response => {
              if (response.data.success) {
                this.configModified = false;
                // 保存成功后更新用户信息
                this.fetchUserInfo();
                // 更新任务列表元数据，确保海报视图能显示TMDB匹配信息
                this.updateTasklistMetadata();
                // 延迟再次更新，给TMDB匹配更多时间
                this.updateTasklistMetadata(3000);

                // 显示任务创建成功消息
                this.showToast('任务创建成功并开始运行', 'success');
                this.createTask.loading = false;

                // 先关闭创建任务模态框，然后运行新创建的任务
                this.cancelCreateTask();

                // 等待模态框完全关闭后再打开运行日志模态框
                setTimeout(() => {
                  const taskIndex = this.formData.tasklist.length - 1;
                  this.runScriptNow(taskIndex);
                }, 300);
              } else {
                // 错误信息使用alert，确保用户看到
                alert(response.data.message);
                this.createTask.loading = false;
              }
            })
            .catch(error => {
              // 错误处理
              alert("保存失败: " + (error.response?.data?.message || error.message || "未知错误"));
              this.createTask.loading = false;
            });
        },
        confirmCreateRunAndDeleteTask() {
          // 确认创建、运行并删除任务
          if (this.createTask.loading) return;

          // 验证必填字段
          if (!this.createTask.taskData.taskname.trim()) {
            this.createTask.error = '任务名称不能为空';
            return;
          }
          if (!this.createTask.taskData.shareurl.trim()) {
            this.createTask.error = '分享链接不能为空';
            return;
          }
          if (!this.createTask.taskData.savepath.trim()) {
            this.createTask.error = '保存路径不能为空';
            return;
          }

          this.createTask.loading = true;
          this.createTask.error = null;

          // 创建新任务
          const newTask = { ...this.createTask.taskData };
          // 一次性任务：跳过日历匹配
          newTask.skip_calendar = true;

          // 应用全局插件配置
          this.applyGlobalPluginConfig(newTask);

          // 处理命名模式
          if (newTask.use_sequence_naming) {
            newTask.pattern = newTask.sequence_naming;
          } else if (newTask.use_episode_naming) {
            newTask.pattern = newTask.episode_naming;
          }

          // 添加到任务列表
          if (!this.formData.tasklist) {
            this.formData.tasklist = [];
          }
          this.formData.tasklist.push(newTask);

          // 保存配置（不显示配置更新消息）
          axios.post('/update', this.formData)
            .then(response => {
              if (response.data.success) {
                this.configModified = false;
                // 保存成功后更新用户信息
                this.fetchUserInfo();

                // 显示任务创建成功消息
                this.showToast('任务创建成功，开始运行并将在完成后自动删除', 'success');
                this.createTask.loading = false;

                // 先关闭创建任务模态框，然后运行新创建的任务
                this.cancelCreateTask();

                // 等待模态框完全关闭后再打开运行日志模态框并运行任务
                setTimeout(async () => {
                  const taskIndex = this.formData.tasklist.length - 1;
                  
                  // 运行任务并等待完成
                  await this.runScriptNowWithCallback(taskIndex, () => {
                    // 任务完成后删除该任务
                    setTimeout(() => {
                      this.removeTaskSilently(taskIndex);
                      this.showToast('一次性任务已完成并自动删除', 'info');
                    }, 1000); // 等待1秒确保任务状态更新
                  });
                }, 300);
              } else {
                // 错误信息使用alert，确保用户看到
                alert(response.data.message);
                this.createTask.loading = false;
              }
            })
            .catch(error => {
              // 错误处理
              alert("保存失败: " + (error.response?.data?.message || error.message || "未知错误"));
              this.createTask.loading = false;
            });
        },
        async runScriptNowWithCallback(task_index, callback) {
          // 运行任务的包装函数，支持完成回调
          body = {};
          if (task_index != null) {
            task = { ...this.formData.tasklist[task_index] };
            delete task.runweek;
            delete task.enddate;
            body = {
              "tasklist": [task],
              "original_index": task_index + 1  // 添加原始索引，从1开始计数
            };
          } else if (this.configModified) {
            if (!confirm('配置已修改但未保存，是否继续运行？')) {
              return;
            }
          }
          $('#logModal').modal('toggle');
          this.modalLoading = true;
          this.run_log = '';
          try {
            // 1. 发送 POST 请求
            const response = await fetch(`/run_script_now`, {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json'
              },
              body: JSON.stringify(body)
            });
            if (!response.ok) {
              throw new Error(`HTTP error! Status: ${response.status}`);
            }
            // 2. 处理 SSE 流
            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let partialData = '';
            while (true) {
              const { done, value } = await reader.read();
              if (done) {
                this.modalLoading = false;
                // 运行后刷新数据
                this.fetchData();
                // 调用完成回调
                if (callback) callback();
                break;
              }
              partialData += decoder.decode(value);
              const lines = partialData.split('\n').filter(line => line.trim() !== '');
              for (const line of lines) {
                if (line.startsWith('data:')) {
                  const eventData = line.substring(5).trim();
                  if (eventData === '[DONE]') {
                    this.modalLoading = false;
                    this.fetchData();
                    // 调用完成回调
                    if (callback) callback();
                    return;
                  }
                  this.run_log += eventData + '\n';
                  // 在更新 run_log 后将滚动条滚动到底部
                  this.$nextTick(() => {
                    const modalBody = document.querySelector('.modal-body');
                    modalBody.scrollTop = modalBody.scrollHeight;
                  });
                }
              }
              partialData = '';
            }
          } catch (error) {
            this.modalLoading = false;
            // 即使出错也调用回调
            if (callback) callback();
          }
        },
        removeTaskSilently(index) {
          // 静默删除任务，不显示确认对话框
          if (index >= 0 && index < this.formData.tasklist.length) {
            const task = this.formData.tasklist[index];
            const taskName = task.taskname || task.task_name;
            // 一次性任务（skip_calendar）不做日历清理，直接删除并保存
            if (task.skip_calendar === true) {
              this.formData.tasklist.splice(index, 1);
              this.saveConfig();
              return;
            }
            // 非一次性任务：删除前执行清理
            axios.post('/api/calendar/purge_by_task', { task_name: taskName })
              .then(() => {
                this.formData.tasklist.splice(index, 1);
                this.saveConfig();
              })
              .catch(() => {
                this.formData.tasklist.splice(index, 1);
                this.saveConfig();
              });
          }
        },
        openCreateTaskDatePicker() {
          // 打开创建任务的日期选择器
          if (this.$refs.createTaskEnddate) {
            this.$refs.createTaskEnddate.showPicker();
          }
        },
        
        // 添加日历页面窗口大小变化监听器
        addCalendarResizeListener() {
          // 移除之前的监听器（如果存在）
          if (this.calendarResizeHandler) {
            window.removeEventListener('resize', this.calendarResizeHandler);
          }
          
          // 创建新的监听器
          this.calendarResizeHandler = this.debounce(() => {
            if (this.activeTab === 'calendar') {
              if (this.calendar.viewMode === 'poster' && !this.calendar.manageMode) {
                this.updateWeekDates();
              }
              // 管理模式下仅触发布局tick，使用相同的列计算逻辑
              if (this.calendar.manageMode) {
                this.calendar.layoutTick = Date.now();
              }
            }
          }, 300); // 300ms防抖
          
          // 添加监听器
          window.addEventListener('resize', this.calendarResizeHandler);
        },
        
        // 防抖函数
        debounce(func, wait) {
          let timeout;
          return function executedFunction(...args) {
            const later = () => {
              clearTimeout(timeout);
              func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
          };
        },
        
        // 获取追剧日历页面的可用宽度（排除侧边栏）
        getCalendarAvailableWidth() {
          const windowWidth = window.innerWidth || document.documentElement.clientWidth;
          
          // 移动设备（小屏幕）
          if (windowWidth < 768) {
            // 移动设备下侧边栏会折叠，主内容区域占满宽度
            return windowWidth - 20; // 减去左右边距
          }
          
          // 桌面设备（大屏幕）
          let sidebarWidth = 184; // 默认展开状态宽度
          
          // 如果侧边栏已折叠
          if (this.sidebarCollapsed) {
            sidebarWidth = 54; // 折叠状态宽度
          }
          
          // 计算主内容区域可用宽度
          const availableWidth = windowWidth - sidebarWidth - 20; // 减去侧边栏宽度和左右边距
          
          // 根据页面宽度模式应用最大宽度上限，避免中/窄模式下超限
          // 对应 CSS: .page-width-narrow/.page-width-medium/.page-width-wide 的 container-fluid max-width
          let maxContentWidth;
          if (this.pageWidthMode === 'narrow') {
            maxContentWidth = 1440; // 与 CSS 保持一致
          } else if (this.pageWidthMode === 'medium') {
            maxContentWidth = 1680; // 与 CSS 保持一致
          } else {
            maxContentWidth = 2160; // 宽模式上限
          }
          // 主内容区域在 Bootstrap 栅格中通常占 10/12 宽度（col-md-10 col-lg-10），需要按比例折算
          // 这里以可视窗口宽度估算容器宽度并取上限，再减去侧边栏与边距后作为可用宽度
          const containerMaxWidth = Math.min(windowWidth, maxContentWidth);
          const maxAvailableByMode = containerMaxWidth - sidebarWidth - 20;
          const boundedWidth = Math.min(availableWidth, maxAvailableByMode);
          
          return Math.max(boundedWidth, 300); // 确保最小可用宽度为300px
        },
        
        // 格式化日期为YYYY-MM-DD格式（使用本地时间）
        formatDateToYYYYMMDD(date) {
          const year = date.getFullYear();
          const month = (date.getMonth() + 1).toString().padStart(2, '0');
          const day = date.getDate().toString().padStart(2, '0');
          return `${year}-${month}-${day}`;
        }
      },
      mounted() {
        this.fetchData();
        this.checkNewVersion();
        this.fetchUserInfo(); // 获取用户信息
        
        // 添加点击事件监听
        document.addEventListener('click', this.handleOutsideClick);
        document.addEventListener('click', this.handleModalOutsideClick);
        document.addEventListener('click', this.handleFileManagerOutsideClick);
        
        // 添加模态框关闭事件监听
        $('#fileSelectModal').on('hidden.bs.modal', () => {
          this.fileSelect.selectedFiles = [];
          this.fileSelect.lastSelectedFileIndex = -1;
          // 重置移动模式相关参数
          this.fileSelect.moveMode = false;
          this.fileSelect.moveFileIds = [];
          // 重置z-index
          document.getElementById('fileSelectModal').style.zIndex = '';
        });
        
        // 检查本地存储中的标签页状态
        const savedTab = localStorage.getItem('quarkAutoSave_activeTab');
        if (savedTab) {
          this.activeTab = savedTab;
        }
        
        // 从本地存储中恢复侧边栏折叠状态
        const savedSidebarState = localStorage.getItem('quarkAutoSave_sidebarCollapsed');
        if (savedSidebarState) {
          this.sidebarCollapsed = savedSidebarState === 'true';
        }
        
        // 从本地存储中恢复用户设置的每页记录数
        const savedPageSize = localStorage.getItem('quarkAutoSave_pageSize');
        if (savedPageSize) {
          this.historyParams.page_size = savedPageSize === 'all' ? 99999 : parseInt(savedPageSize);
        }
        
        // 从本地存储中恢复页面宽度设置
        const savedPageWidthMode = localStorage.getItem('quarkAutoSave_pageWidthMode');
        if (savedPageWidthMode) {
          this.pageWidthMode = savedPageWidthMode;
          document.body.classList.add('page-width-' + this.pageWidthMode);
        } else {
          // 默认使用中等宽度
          document.body.classList.add('page-width-medium');
        }
        
        $('[data-toggle="tooltip"]').tooltip();
        document.addEventListener('keydown', this.handleKeyDown);
        document.addEventListener('click', (e) => {
          // 如果点击的是输入框、搜索按钮或下拉菜单本身，不关闭下拉菜单
          if (e.target.closest('.input-group input') || 
              e.target.closest('.btn-primary[type="button"]') || 
              e.target.closest('.dropdown-menu.task-suggestions') ||
              e.target.closest('.bi-search') ||
              // 点击发生在文件选择模态框内（包括右上角关闭按钮）时，不关闭下拉
              e.target.closest('#fileSelectModal')) {
            return;
          }
          // 只隐藏下拉菜单，不清空搜索结果，这样点击同一任务的输入框时还能看到之前的搜索结果
          this.smart_param.showSuggestions = false;
        });
        
        // 添加点击事件监听器，用于在点击表格外区域时取消选择记录
        document.addEventListener('click', this.handleOutsideClick);

        // 添加点击事件监听器，用于在点击模态框表格外区域时取消选择文件
        document.addEventListener('click', this.handleModalOutsideClick);

        // 添加点击事件监听器，用于处理重命名编辑状态
        document.addEventListener('click', this.handleRenameOutsideClick);
        
        // 添加模态框关闭事件监听，清空选中文件列表
        $('#fileSelectModal').on('hidden.bs.modal', () => {
          this.fileSelect.selectedFiles = [];
          this.fileSelect.lastSelectedFileIndex = -1;
          // 重置移动模式相关参数
          this.fileSelect.moveMode = false;
          this.fileSelect.moveFileIds = [];
          // 重置z-index
          document.getElementById('fileSelectModal').style.zIndex = '';
        });
        
        window.addEventListener('beforeunload', this.handleBeforeUnload);
        
        // 监听模态框显示事件，检查滚动条状态
        $('#fileSelectModal').on('shown.bs.modal', this.checkPreviewScrollbar);
        // 监听窗口大小改变，重新检查滚动条状态
        window.addEventListener('resize', this.checkPreviewScrollbar);
        
        // 初始化时检查所有任务的命名模式
        setTimeout(() => {
          if (this.formData.tasklist && this.formData.tasklist.length > 0) {
            this.formData.tasklist.forEach(task => {
              // 检查现有的顺序命名设置
              if (task.use_sequence_naming && task.sequence_naming) {
                // 已经设置过顺序命名的，将顺序命名模式转换为匹配表达式
                if (!task.pattern || task._pattern_backup) {
                  task.pattern = task.sequence_naming;
                }
              } else if (task.use_episode_naming && task.episode_naming) {
                // 已经设置过剧集命名的，将剧集命名模式转换为匹配表达式
                if (!task.pattern || task._pattern_backup) {
                  task.pattern = task.episode_naming;
                }
              } else {
                // 检测是否包含顺序命名或剧集命名模式
                this.detectNamingMode(task);
              }
            });
          }
          
          // 确保剧集识别模式字段存在（但不自动添加默认规则）
          if (!this.formData.episode_patterns) {
            this.formData.episode_patterns = [];
          }
          
          // 如果当前标签是历史记录，则加载历史记录
          if (this.activeTab === 'history') {
            this.loadHistoryRecords();
            // 加载所有任务名称用于筛选
            this.loadAllTaskNames();
          }
          
          // 添加对history.pagination的监听
          this.$watch('history.pagination', function(newVal) {
            if (newVal && newVal.total_pages) {
              this.$nextTick(() => {
                // 强制Vue更新视图
                this.$forceUpdate();
              });
            }
          }, { deep: true });
          
          // 检查分享链接状态
          this.checkShareUrlStatus();

          // 初始化影视发现页面的选择状态
          this.initializeDiscoverySelection();

          // 如果当前是影视发现页面，加载榜单数据
          if (this.activeTab === 'discovery') {
            this.loadDiscoveryData();
          }
          
        // 如果当前是追剧日历页面，加载日历数据
        if (this.activeTab === 'calendar') {
          this.loadCalendarData();
        }
        
        // 如果当前是任务列表页面，启动后台监听
        if (this.activeTab === 'tasklist') {
          this.startTasklistAutoWatch();
        }
        }, 500);
      },
      beforeDestroy() {
        window.removeEventListener('beforeunload', this.handleBeforeUnload);
        // 移除点击事件监听器
        document.removeEventListener('click', this.handleOutsideClick);
        document.removeEventListener('click', this.handleRenameOutsideClick);
        
        // 移除日历页面resize监听器
        if (this.calendarResizeHandler) {
          window.removeEventListener('resize', this.calendarResizeHandler);
        }
        
        // 停止任务列表后台监听
        this.stopTasklistAutoWatch();
      }
    });
  </script>
</body>

</html>

