<template>
  <div class="system-config">
    <!-- 🌐 网站设置 -->
    <el-card id="section-sys-site" v-show="!activeSection || activeSection === 'sys-site'">
      <template #header>
        <span>🌐 网站设置</span>
      </template>
      <el-form label-width="180px" label-position="left">
        <el-form-item label="网站名称">
          <el-input v-model="configs.site_name" placeholder="CloudSearch" style="max-width: 300px" />
          <div class="form-tip">显示在网站标题和页脚</div>
        </el-form-item>
        <el-form-item label="网站 LOGO">
          <div class="fallback-upload-wrap">
            <div class="fallback-upload-row">
              <el-button type="primary" @click="triggerLogoInput">
                <template #icon><el-icon><Upload /></el-icon></template>
                选择LOGO图片并上传
              </el-button>
              <input
                ref="logoInputRef"
                type="file"
                accept=".jpg,.jpeg,.png,.webp"
                hidden
                @change="handleLogoSelect"
              />
              <div class="form-tip" style="margin-left: 12px;">
                推荐 <strong>320×60</strong> 或宽比例（如 4:1），JPEG/PNG/WebP，最大 2MB。<br>
                LOGO 同时也作为搜索结果无封面图时的兜底图使用。
              </div>
            </div>
            <div v-if="configs.site_logo" class="fallback-preview">
              <img :src="String(configs.site_logo)" alt="LOGO预览" @error="(e: any) => e.target.style.display='none'" />
              <el-button size="small" type="danger" plain @click="handleRemoveLogo">移除</el-button>
            </div>
          </div>
        </el-form-item>
        <el-form-item label="底部免责声明">
          <el-input
            v-model="configs.site_disclaimer"
            type="textarea"
            :rows="4"
            placeholder="输入免责声明内容"
          />
          <div class="form-tip">显示在网站底部，留空则不显示</div>
        </el-form-item>
        <el-form-item label="滚动通知文字">
          <el-input
            v-model="configs.site_marquee"
            placeholder="📢 欢迎使用CloudSearch"
            style="max-width: 500px"
          />
          <div class="form-tip">搜索栏下方滚动的通知条（从右往左滚动），留空则不显示</div>
        </el-form-item>
        <el-form-item label="系统时区">
          <el-input
            v-model="configs.timezone"
            placeholder="Asia/Shanghai"
            style="max-width: 300px"
          />
          <div class="form-tip">例如 Asia/Shanghai、America/New_York、UTC，修改后保存配置即可生效</div>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 🔗 外部服务 + 数据库与缓存 -->
    <el-card id="section-sys-services" v-show="!activeSection || activeSection === 'sys-services'">
      <template #header>
        <span>🔗 外部服务 &amp; 缓存</span>
      </template>
      <el-form label-width="180px" label-position="left">
        <el-form-item label="PanSou 搜索引擎地址">
          <div style="display: flex; gap: 8px; align-items: center; width: 100%;">
            <el-input v-model="configs.pansou_url" placeholder="http://pansou:8888" style="max-width: 360px" />
            <el-button type="primary" :loading="pansouTesting" @click="handleTestPansou" size="default" style="width: 100px;">
              {{ pansouTesting ? '测试中...' : '验证连接' }}
            </el-button>
            <el-button type="warning" :loading="pansouUpdating" @click="handleUpdatePansou" size="default" style="width: 130px;" :disabled="!pansouInfo?.hasUpdate">
              {{ pansouUpdating ? '更新中...' : (pansouInfo?.hasUpdate ? '🔄 有新版本' : '无更新') }}
            </el-button>
            <span v-if="pansouInfo?.latestVersion" style="font-size:11px;color:#e6a23c;white-space:nowrap;">{{ pansouInfo.latestVersion }}</span>
          </div>
          <div class="form-tip" style="margin-top: 4px;">盘搜搜索引擎的地址</div>
        </el-form-item>

        <!-- PanSou 状态面板 -->
        <div class="pansou-status-grid">
          <div class="db-stat-item">
            <div class="db-stat-value" :class="pansouInfo?.status === 'connected' ? 'text-success' : 'text-warning'">
              {{ pansouInfo?.status === 'connected' ? '已连接' : pansouInfo ? '未连接' : '-' }}
            </div>
            <div class="db-stat-label">PanSou 状态</div>
          </div>
          <div class="db-stat-item">
            <div class="db-stat-value">{{ pansouInfo?.channelCount ?? '-' }}</div>
            <div class="db-stat-label">频道数量</div>
          </div>
          <div class="db-stat-item">
            <div class="db-stat-value">{{ pansouInfo?.pluginCount ?? '-' }}</div>
            <div class="db-stat-label">插件数量</div>
          </div>
          <div class="db-stat-item">
            <div class="db-stat-value">{{ pansouInfo?.diskCount ?? '-' }}</div>
            <div class="db-stat-label">网盘数量</div>
          </div>
          <div class="db-stat-item">
            <div class="db-stat-value">{{ pansouInfo?.version || '-' }}</div>
            <div class="db-stat-label">版本</div>
          </div>
        </div>

        <el-form-item label="PanSou Web 端访问">
          <el-switch v-model="pansouWebEnabled" active-text="启用" inactive-text="关闭" />
          <div class="form-tip" style="margin-left: 12px;">
            开启后可通过 <code>/pansou/</code> 路径访问 PanSou 搜索引擎管理界面
          </div>
        </el-form-item>
        <el-form-item label="启用代理">
          <el-switch v-model="proxyEnabled" active-text="启用" inactive-text="关闭" />
          <div class="form-tip" style="margin-left: 8px;">
            仅 PanSou 需要此配置，开启后搜索请求将经过代理转发
          </div>
        </el-form-item>
        <el-form-item v-if="proxyEnabled" label="代理地址" style="margin-top: -12px;">
          <div style="display: flex; gap: 8px; align-items: center; width: 100%;">
            <el-input v-model="configs.search_proxy_url" placeholder="http://127.0.0.1:7890" style="max-width: 360px" />
            <el-button type="primary" :loading="proxyTesting" @click="handleTestProxy" size="default" style="width: 100px;">
              {{ proxyTesting ? '测试中...' : '测试代理' }}
            </el-button>
          </div>
          <div class="form-tip" style="margin-top: 4px;">HTTP 或 SOCKS5 协议地址</div>
        </el-form-item>
        <el-form-item label="视频解析服务地址">
          <div style="display: flex; gap: 8px; align-items: center; width: 100%;">
            <el-input v-model="configs.video_parser_url" placeholder="http://video-parser:3001" style="max-width: 360px" />
            <el-button type="primary" :loading="videoParserTesting" @click="handleTestVideoParser" size="default" style="width: 100px;">
              {{ videoParserTesting ? '测试中...' : '验证连接' }}
            </el-button>
          </div>
          <div class="form-tip" style="margin-top: 4px;">视频链接解析服务地址</div>
        </el-form-item>
        <el-form-item label="TMDB 读取令牌">
          <div style="display: flex; gap: 8px; align-items: center; width: 100%;">
            <el-input
              v-model="configs.tmdb_api_token"
              type="password"
              show-password
              placeholder="输入 TMDB API 读取令牌（Bearer Token）"
              style="max-width: 360px"
            />
            <el-button type="primary" :loading="tmdbTesting" @click="handleTestTmdb" size="default" style="width: 100px;">
              {{ tmdbTesting ? '测试中...' : '验证令牌' }}
            </el-button>
          </div>
          <div class="form-tip" style="margin-top: 4px;">
            用于搜索 TMDB 获取评分、导演、演员等完整内容信息。
            <a href="https://www.themoviedb.org/settings/api" target="_blank" rel="noopener" style="color: var(--primary-color)">获取令牌 →</a>
          </div>
        </el-form-item>
<el-form-item label="IP 归属地查询">
          <div style="display: flex; gap: 8px; align-items: center; width: 100%;">
            <el-input
              v-model="configs.ip_geo_api_url"
              placeholder="https://cn.apihz.cn/api/ip/chaapi.php?id=xxx&key=***&ip={ip}&td=0"
              style="max-width: 360px"
            />
            <el-button type="primary" :loading="ipGeoTesting" @click="handleTestIpGeo" size="default" style="width: 100px;">
              {{ ipGeoTesting ? "测试中..." : "验证接口" }}
            </el-button>
          </div>
          <div class="form-tip" style="margin-top: 4px;">
            IP 归属地查询 API 地址，<code>{ip}</code> 会被替换为实际 IP。
          </div>
          <div style="color: var(--el-color-warning); font-size: 13px; margin-top: 2px; width: 100%;">
            ⚠ 当前仅支持 接口盒子(apihz.cn) 格式
          </div>
        </el-form-item>
        <el-divider content-position="left">Redis 缓存</el-divider>
        <el-form-item label="Redis 连接地址">
          <div style="display: flex; gap: 8px; align-items: center; width: 100%;">
            <el-input v-model="configs.redis_url" placeholder='redis://:***@172.17.0.1:6379' style="max-width: 360px" />
            <el-button type="primary" size="default" :loading="redisTesting" @click="handleTestRedis" style="width: 100px;">
              {{ redisTesting ? '测试中...' : '验证连接' }}
            </el-button>
          </div>
          <div class="form-tip" style="margin-top: 4px;">
            Redis 用于缓存搜索验证结果，提升响应速度。<br>
            <strong>带密码格式：</strong><code>redis://:你的密码@地址:6379</code><br>
            修改后保存配置即可生效，无需重启。<strong>切换 Redis 只会清空缓存，不影响任何重要数据。</strong>
          </div>
        </el-form-item>
      </el-form>

      <div v-if="dbLoading" style="text-align: center; padding: 16px;">
        <el-icon class="is-loading" :size="20"><Loading /></el-icon>
        <span style="margin-left: 8px; color: #909399;">加载中...</span>
      </div>
      <div v-else class="db-status-grid">
        <div class="db-stat-item">
          <div class="db-stat-value" :class="dbStatus.redis_status === '已连接' ? 'text-success' : 'text-warning'">
            {{ dbStatus.redis_status }}
          </div>
          <div class="db-stat-label">Redis 状态</div>
        </div>
        <div class="db-stat-item">
          <div class="db-stat-value">{{ dbStatus.db_size }}</div>
          <div class="db-stat-label">数据库大小</div>
        </div>
        <div class="db-stat-item">
          <div class="db-stat-value">{{ dbStatus.save_records }}</div>
          <div class="db-stat-label">转存记录</div>
        </div>
        <div class="db-stat-item">
          <div class="db-stat-value">{{ dbStatus.search_stats }}</div>
          <div class="db-stat-label">搜索记录</div>
        </div>
        <div class="db-stat-item">
          <div class="db-stat-value">{{ dbStatus.cloud_configs }}</div>
          <div class="db-stat-label">网盘配置</div>
        </div>
        <div class="db-stat-item">
          <div class="db-stat-value">{{ dbStatus.content_cache }}</div>
          <div class="db-stat-label">内容缓存</div>
        </div>
      </div>
    </el-card>

    <!-- 📊 搜索结果展示策略（含链接验证配置） -->
    <el-card id="section-sys-strategy" v-show="!activeSection || activeSection === 'sys-strategy'">
      <template #header>
        <span>🔧 性能配置</span>
      </template>

      <div class="strategy-section">
        <el-divider content-position="left">搜索结果返回方式</el-divider>

        <div class="field-block">
          <div class="field-label-row">
            <span class="field-label">开启资源链接有效性监测</span>
            <el-switch
              v-model="configs.link_validation_enabled"
              active-value="true"
              inactive-value="false"
              active-text="开启"
              inactive-text="关闭"
            />
          </div>
          <div class="field-desc">开启后搜索时会自动检测链接是否有效，过滤失效链接。关闭则直接返回所有结果（更快）</div>
        </div>

        <div class="field-block">
          <div class="field-label-row">
            <span class="field-label">搜索结果方式</span>
            <el-radio-group v-model="configs.search_strategy">
              <el-radio value="wait_all">等待全部结果后展示</el-radio>
              <el-radio value="stream_channel">频道结果逐步展示</el-radio>
            </el-radio-group>
          </div>
          <div class="field-desc">逐步展示会分频道并发请求并优先展示先返回的频道；该模式下频道顺序按返回先后，不按配置顺序。</div>
        </div>

        <el-divider content-position="left">搜索策略</el-divider>

        <!-- 开关 + 数字输入 3列 -->
        <div class="strategy-grid">
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">使用所有频道参与搜索</span>
              <el-switch v-model="searchAllChannels" active-text="开启" inactive-text="关闭" />
            </div>
            <div class="field-desc">包含未启用频道，命中更广但请求压力更高。</div>
          </div>
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">每类网盘有效结果数</span>
              <el-input-number v-model="configs.search_result_limit" :min="1" :max="100" />
            </div>
            <div class="field-desc">每个网盘类型最多展示的有效链接数量</div>
          </div>
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">验证并发数</span>
              <el-input-number v-model="configs.validation_concurrency" :min="1" :max="50" />
            </div>
            <div class="field-desc">同时验证的链接数量</div>
          </div>

          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">有效链接缓存 (s)</span>
              <el-input-number v-model="configs.validation_cache_ttl_valid" :min="60" :max="86400" :step="60" />
            </div>
            <div class="field-desc">有效验证结果的缓存时间</div>
          </div>
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">无效链接缓存 (s)</span>
              <el-input-number v-model="configs.validation_cache_ttl_invalid" :min="60" :max="86400" :step="60" />
            </div>
            <div class="field-desc">无效验证结果的缓存时间</div>
          </div>
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">验证超时 (ms)</span>
              <el-input-number v-model="configs.validation_timeout" :min="1000" :max="30000" :step="500" />
            </div>
            <div class="field-desc">单个链接验证超时毫秒数</div>
          </div>
        </div>

        <el-divider content-position="left">链接检测配置</el-divider>

        <!-- 搜索标题过滤规则（放入链接检测配置内） -->
        <div class="field-block">
          <div class="field-label-row">
            <span class="field-label">搜索标题过滤规则</span>
          </div>
          <div style="display:flex; gap:8px; align-items:stretch;">
            <el-input
              v-model="filterRuleBatchInput"
              type="textarea"
              :rows="1"
              placeholder="每行一条规则，回车分隔"
              style="max-width:420px; font-family:monospace; font-size:12px"
            />
            <el-button type="primary" @click="batchAddFilterRules" :disabled="!filterRuleBatchInput.trim()">
              确认添加
            </el-button>
          </div>
          <div class="tag-list" v-if="filterRuleList.length > 0">
            <el-tag
              v-for="(rule, i) in filterRuleList"
              :key="i"
              closable
              :type="ruleTagType(rule)"
              :disable-transitions="false"
              @close="removeFilterRule(i)"
            >{{ rule }}</el-tag>
          </div>
          <div v-else class="tag-empty">暂无过滤规则</div>
          <div class="filter-rule-help">
            <div class="help-title">📖 规则说明</div>
            <div class="help-row"><code># 注释内容</code> — <code>#</code> 后必须跟<strong>空格</strong>才会被识别为注释，单纯的 <code>#动漫</code> 会被当作要移除的文本</div>
            <div class="help-row"><code>/正则表达式/标志</code> — 正则匹配，匹配到的内容会被删除。如 <code>/^\\d+[、,.\\s]/</code> 去掉开头的序号</div>
            <div class="help-row"><code>纯文本</code> — 直接写要移除的文字（支持 emoji），凡是标题中含有的都会被删除</div>
          </div>
        </div>

        <div class="field-block">
          <div class="field-label-row">
            <span class="field-label">失效关键词</span>
          </div>
          <div style="display:flex; gap:8px; align-items:stretch;">
            <el-input
              v-model="invalidKeywordBatchInput"
              type="textarea"
              :rows="1"
              placeholder="每行一个关键词，回车分隔"
              style="max-width:420px; font-family:monospace; font-size:12px"
            />
            <el-button type="danger" @click="batchAddInvalidKeywords" :disabled="!invalidKeywordBatchInput.trim()">
              确认添加
            </el-button>
          </div>
          <div class="field-desc">自定义失效关键词，PanSou 检测 summary 或 HTML 页面内容包含即判为失效。</div>
          <div class="tag-list" v-if="invalidKeywordList.length > 0">
            <el-tag
              v-for="(kw, i) in invalidKeywordList"
              :key="i"
              closable
              type="danger"
              :disable-transitions="false"
              @close="removeInvalidKeyword(i)"
            >{{ kw }}</el-tag>
          </div>
          <div v-else class="tag-empty">暂无失效关键词，所有链接将默认判为有效</div>
        </div>

        <el-divider content-position="left">夸克网盘转存清理</el-divider>

        <div class="field-block">
          <div class="field-label-row">
            <span class="field-label">广告关键词</span>
          </div>
          <div style="display:flex; gap:8px; align-items:stretch;">
            <el-input
              v-model="adKeywordBatchInput"
              type="textarea"
              :rows="1"
              placeholder="每行一个关键词，回车分隔"
              style="max-width:420px; font-family:monospace; font-size:12px"
            />
            <el-button type="danger" @click="batchAddAdKeywords" :disabled="!adKeywordBatchInput.trim()">
              确认添加
            </el-button>
          </div>
          <div class="field-desc">夸克转存完成后自动删除文件名/文件夹名含这些关键词的内容（防广告病毒）</div>
          <div class="tag-list" v-if="adKeywordList.length > 0">
            <el-tag
              v-for="(kw, i) in adKeywordList"
              :key="i"
              closable
              type="warning"
              :disable-transitions="false"
              @close="removeAdKeyword(i)"
            >{{ kw }}</el-tag>
          </div>
          <div v-else class="tag-empty">暂未配置广告关键词，不会进行广告清理</div>
        </div>

        <div class="field-block">
          <div class="field-label-row">
            <span class="field-label">警示文件夹名</span>
          </div>
          <div style="display:flex; gap:8px; align-items:stretch;">
            <el-input
              v-model="warningFolderBatchInput"
              type="textarea"
              :rows="1"
              placeholder="每行一个文件夹名，回车分隔"
              style="max-width:420px; font-family:monospace; font-size:12px"
            />
            <el-button type="primary" @click="batchAddWarningFolders" :disabled="!warningFolderBatchInput.trim()">
              确认添加
            </el-button>
          </div>
          <div class="field-desc">夸克转存完成后在网盘根目录自动创建这些警示文件夹（自动加上 ⚠️ 前缀）</div>
          <div class="tag-list" v-if="warningFolderList.length > 0">
            <el-tag
              v-for="(name, i) in warningFolderList"
              :key="i"
              closable
              type="info"
              :disable-transitions="false"
              @close="removeWarningFolder(i)"
            >{{ name }}</el-tag>
          </div>
          <div v-else class="tag-empty">暂未配置警示文件夹</div>
        </div>

        <div class="field-block">
          <div class="field-label-row">
            <span class="field-label">可疑文件后缀</span>
          </div>
          <div style="display:flex; gap:8px; align-items:stretch;">
            <el-input
              v-model="susExtensionBatchInput"
              type="textarea"
              :rows="1"
              placeholder="每行一个后缀，不要带点号，回车分隔"
              style="max-width:420px; font-family:monospace; font-size:12px"
            />
            <el-button type="danger" @click="batchAddSusExtensions" :disabled="!susExtensionBatchInput.trim()">
              确认添加
            </el-button>
          </div>
          <div class="field-desc">夸克转存完成后自动删除后缀匹配的文件（防病毒，如 bat、exe、scr 等）</div>
          <div class="tag-list" v-if="susExtensionList.length > 0">
            <el-tag
              v-for="(ext, i) in susExtensionList"
              :key="i"
              closable
              type="danger"
              :disable-transitions="false"
              @close="removeSusExtension(i)"
            >.{{ ext }}</el-tag>
          </div>
          <div v-else class="tag-empty">暂无配置，使用默认列表：bat、exe、vbs、scr、cmd、com、pif、js、jar、msi、reg、inf、ps1</div>
        </div>
      </div>
    </el-card>

    <!-- 修改密码 -->
    <el-card id="section-sys-password" v-show="!activeSection || activeSection === 'sys-password'">
      <template #header>
        <span>🔑 修改管理员密码</span>
      </template>
      <el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="120px" label-position="left">
        <el-form-item label="原密码" prop="oldPassword">
          <el-input v-model="passwordForm.oldPassword" type="password" show-password placeholder="输入原密码" style="max-width: 300px" />
        </el-form-item>
        <el-form-item label="新密码" prop="newPassword">
          <el-input v-model="passwordForm.newPassword" type="password" show-password placeholder="输入新密码（至少6位）" style="max-width: 300px" />
        </el-form-item>
        <el-form-item label="确认新密码" prop="confirmPassword">
          <el-input v-model="passwordForm.confirmPassword" type="password" show-password placeholder="再次输入新密码" style="max-width: 300px" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" :loading="changingPassword" @click="handleChangePassword">修改密码</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 📬 消息推送 -->
    <el-card id="section-sys-notify" v-show="!activeSection || activeSection === 'sys-notify'">
      <template #header>
        <span>📬 消息推送</span>
      </template>

      <div class="strategy-section">
        <el-divider content-position="left">推送通道配置</el-divider>

        <!-- 飞书 -->
        <el-form-item label="飞书 Webhook">
          <el-input v-model="configs.feishu_webhook_url" placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/xxx" style="max-width: 500px" />
          <div class="form-tip">飞书机器人 Webhook URL，配置后发送卡片消息到群聊。</div>
          <div class="form-tip" style="color: var(--el-color-primary); font-size: 12px; margin-top: 2px;">
            优先从环境变量 FEISHU_WEBHOOK 读取，其次读取此配置
          </div>
        </el-form-item>

        <!-- Server酱 -->
        <el-form-item label="Server酱 (微信)">
          <el-input v-model="configs.serverchan_key" placeholder="SendKey" style="max-width: 300px" />
          <div class="form-tip">通过 <a href="https://sct.ftqq.com" target="_blank" rel="noopener" style="color: var(--primary-color)">Server酱</a> 推送到微信，只需填写 SendKey</div>
        </el-form-item>

        <!-- Bark -->
        <el-form-item label="Bark (iOS)">
          <el-input v-model="configs.bark_key" placeholder="xxxxxxxxxxxxxxxxxxxxxx" style="max-width: 300px" />
          <div class="form-tip" style="margin-bottom: 4px;">通过 <a href="https://bark.day.app" target="_blank" rel="noopener" style="color: var(--primary-color)">Bark</a> 推送到 iOS 设备，填写 API Key</div>
          <div class="field-label-row">
            <span class="field-label" style="font-size:12px; color:#909399;">自定义服务器</span>
            <el-input v-model="configs.bark_server" placeholder="https://api.day.app" style="max-width: 280px" />
          </div>
        </el-form-item>

        <!-- Telegram -->
        <el-form-item label="Telegram">
          <div style="display: flex; gap: 8px; align-items: center; width: 100%;">
            <el-input v-model="configs.telegram_bot_token" placeholder="123456:ABC-DEF" style="max-width: 300px" />
            <span style="font-size:12px; color:#909399;">Bot Token</span>
            <el-input v-model="configs.telegram_chat_id" placeholder="@频道或 -100..." style="max-width: 200px" />
            <span style="font-size:12px; color:#909399;">Chat ID</span>
          </div>
          <div class="form-tip">通过 TG Bot 推送消息，需先创建 Bot 并获取 Token</div>
        </el-form-item>

        <!-- 自定义 Webhook -->
        <el-form-item label="自定义 Webhook">
          <el-input v-model="configs.webhook_url" placeholder="https://example.com/webhook" style="max-width: 500px" />
          <div class="form-tip">POST JSON 到指定 URL，格式：{title, content, level, source: "CloudSearch"}</div>
        </el-form-item>

        <el-divider content-position="left">推送事件开关</el-divider>

        <div class="strategy-grid" style="grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));">
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">✅ 转存成功</span>
              <el-switch v-model="configs.notify_on_save_success" active-value="true" inactive-value="false" />
            </div>
            <div class="field-desc">转存成功时推送通知</div>
          </div>
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">❌ 转存连续失败</span>
              <el-switch v-model="configs.notify_on_save_fail" active-value="true" inactive-value="false" />
            </div>
            <div class="field-desc">连续失败 3 次后推送通知</div>
          </div>
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">⚠️ Cookie 过期</span>
              <el-switch v-model="configs.notify_on_cookie_expire" active-value="true" inactive-value="false" />
            </div>
            <div class="field-desc">Cookie 过期时推送提醒</div>
          </div>
          <div class="grid-cell">
            <div class="field-label-row">
              <span class="field-label">🧹 清理完成</span>
              <el-switch v-model="configs.notify_on_cleanup" active-value="true" inactive-value="false" />
            </div>
            <div class="field-desc">每日自动清理完成时推送</div>
          </div>
        </div>
      </div>
    </el-card>

    <!-- 🔄 系统维护 -->    <el-card id="section-sys-maintenance" v-show="!activeSection || activeSection === 'sys-maintenance'">      <template #header>        <span>🔄 系统维护</span>      </template>      <el-form label-width="180px" label-position="left">        <el-form-item label="自动更新镜像">          <el-switch v-model="autoUpdateEnabled" active-text="启用" inactive-text="禁用" />          <div class="form-tip">启用后 CloudSearch 将自动检测并更新到最新镜像版本</div>          <div class="form-tip" style="color: var(--(--el-color-warning,#e6a23c));">            当前需手动在服务器执行：docker-compose -f /opt/CloudSearch/docker-compose.yml pull && docker-compose -f /opt/CloudSearch/docker-compose.yml up -d          </div>        </el-form-item>      </el-form>    </el-card>

    <!-- 保存按钮 -->
    <div class="save-bar">
      <el-button type="primary" size="large" :loading="saving" @click="handleSave">
        保存配置
      </el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, computed } from "vue"
import { useRoute, useRouter } from "vue-router"
import { ElMessage } from 'element-plus'
import type { ElForm } from 'element-plus'
import { getSystemConfigs, updateSystemConfigs, changePassword as changePasswordApi, uploadFallbackImage, uploadLogo, updateSetting, getDbStatus, testRedisConnection, testExternalService } from "../../api"
import { Upload, Loading } from "@element-plus/icons-vue"


const props = defineProps<{ section?: string }>()
const route = useRoute()
const router = useRouter()
const activeSection = computed(() => props.section || (route.query.section as string) || "")

interface ConfigMap {
  [key: string]: string | number
}

const passwordFormRef = ref<InstanceType<typeof ElForm>>()
const rawConfigs = ref<{ key: string; value: string; description: string }[]>([])
const configs = reactive<ConfigMap>({})
const filterRuleList = ref<string[]>([])
const filterRuleBatchInput = ref('')
const invalidKeywordList = ref<string[]>([])
const invalidKeywordBatchInput = ref('')
const adKeywordList = ref<string[]>([])
const adKeywordBatchInput = ref('')
const warningFolderList = ref<string[]>([])
const warningFolderBatchInput = ref('')
const susExtensionBatchInput = ref('')
const susExtensionList = ref([])
const saving = ref(false)
const changingPassword = ref(false)
const dbStatus = reactive({
  db_size: '-',
  save_records: 0,
  search_stats: 0,
  cloud_configs: 0,
  content_cache: 0,
  redis_status: '未连接',
})
const dbLoading = ref(true)
const redisTesting = ref(false)
const pansouTesting = ref(false)
const videoParserTesting = ref(false)
const tmdbTesting = ref(false)
const proxyTesting = ref(false)
const ipGeoTesting = ref(false)
const pansouInfo = ref<any>(null)
const pansouInfoLoading = ref(true)
const pansouUpdating = ref(false)
const proxyEnabled = computed({
  get: () => String(configs.search_proxy_enabled) === 'true',
  set: (val: boolean) => { configs.search_proxy_enabled = val ? 'true' : 'false' },
})

const pansouWebEnabled = computed({
  get: () => String(configs.pansou_web_enabled) === 'true',
  set: (val: boolean) => { configs.pansou_web_enabled = val ? 'true' : 'false' },
})
const searchAllChannels = computed({
  get: () => String(configs.search_all_channels) === 'true',
  set: (val: boolean) => { configs.search_all_channels = val ? 'true' : 'false' },
})

const autoUpdateEnabled = computed({
  get: () => String(configs.auto_update_enabled) === 'true',
  set: (val: boolean) => { configs.auto_update_enabled = val ? 'true' : 'false' },
})

const passwordForm = reactive({
  oldPassword: '',
  newPassword: '',
  confirmPassword: '',
})
const passwordRules = {
  oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
  newPassword: [
    { required: true, message: '请输入新密码', trigger: 'blur' },
    { min: 6, message: '新密码至少需要6个字符', trigger: 'blur' },
  ],
  confirmPassword: [
    { required: true, message: '请确认新密码', trigger: 'blur' },
    {
      validator: (_rule: any, value: string, callback: Function) => {
        if (value !== passwordForm.newPassword) {
          callback(new Error('两次输入的密码不一致'))
        } else {
          callback()
        }
      },
      trigger: 'blur',
    },
  ],
}

onMounted(async () => {
  try {
    rawConfigs.value = await getSystemConfigs()
    for (const cfg of rawConfigs.value) {
      configs[cfg.key] = cfg.value
    }
    // 解析过滤规则为列表
    const rules = String(configs.title_filter_rules || '')
    filterRuleList.value = rules.split('\n').filter(r => r.trim())
    // 解析失效关键词列表
    const invalidKws = String(configs.link_invalid_keywords || '')
    invalidKeywordList.value = invalidKws.split('\n').filter(k => k.trim())
    // 解析广告关键词列表
    const adKws = String(configs.quark_ad_keywords || '')
    adKeywordList.value = adKws.split('\n').filter(k => k.trim())
    // 解析警示文件夹名列表
    const wfNames = String(configs.quark_warning_folder_names || '')
    warningFolderList.value = wfNames.split('\n').filter(k => k.trim())
    // 解析可疑文件后缀列表
    const susExts = String(configs.quark_sus_extensions || '')
    susExtensionList.value = susExts.split('\n').filter(k => k.trim())
  } catch (e) {
    ElMessage.error('加载系统配置失败')
  }
  // 加载数据库状态
  try {
    const status = await getDbStatus()
    Object.assign(dbStatus, status)
  } catch {
    dbStatus.db_size = '无法读取'
  } finally {
    dbLoading.value = false
  }
  // Auto-load PanSou info on page load
  fetchPansouInfo()
})

async function handleTestRedis() {
  const url = String(configs.redis_url || 'redis://redis:6379')
  redisTesting.value = true
  try {
    const result = await testRedisConnection(url)
    if (result.ok) {
      ElMessage.success(`✅ Redis 连接成功 — ${result.info}`)
    } else {
      ElMessage.error(`❌ Redis 连接失败 — ${result.info}`)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '测试请求失败')
  } finally {
    redisTesting.value = false
  }
}

async function fetchPansouInfo() {
  pansouInfoLoading.value = true
  try {
    const token = localStorage.getItem('admin_token')
    const headers: Record<string, string> = {}
    if (token) {
      headers['Authorization'] = 'Bearer ' + token
    }
    const res = await fetch('/api/admin/pansou-info', { headers })
    if (!res.ok) {
      throw new Error('HTTP ' + res.status)
    }
    const data = await res.json()
    pansouInfo.value = data
  } catch {
    pansouInfo.value = { status: "disconnected", version: "-", channelCount: 0, pluginCount: 0, diskCount: 0 }
  } finally {
    pansouInfoLoading.value = false
  }
}
async function handleUpdatePansou() {
  pansouUpdating.value = true
  try {
    const token = localStorage.getItem('admin_token')
    const headers: Record<string, string> = { 'Content-Type': 'application/json' }
    if (token) headers['Authorization'] = 'Bearer ' + token
    const res = await fetch('/api/admin/update-pansou', { method: 'POST', headers })
    const data = await res.json()
    if (data.ok) {
      ElMessage.success('✅ PanSou 已更新并重启')
      setTimeout(() => fetchPansouInfo(), 3000)
    } else {
      ElMessage.error('❌ 更新失败 — ' + (data.error || '未知错误'))
    }
  } catch (e: any) {
    ElMessage.error(e.message || '更新请求失败')
  } finally {
    pansouUpdating.value = false
  }
}

async function handleTestPansou() {
  pansouTesting.value = true
  try {
    const result = await testExternalService({ type: 'pansou', url: String(configs.pansou_url || '') })
    if (result.ok) {
      fetchPansouInfo()
      ElMessage.success(`✅ PanSou 连接成功 — ${result.info}`)
    } else {
      ElMessage.error(`❌ PanSou 连接失败 — ${result.info}`)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '测试请求失败')
  } finally {
    pansouTesting.value = false
  }
}

async function handleTestVideoParser() {
  videoParserTesting.value = true
  try {
    const result = await testExternalService({ type: 'video_parser', url: String(configs.video_parser_url || '') })
    if (result.ok) {
      ElMessage.success(`✅ 视频解析服务连接成功 — ${result.info}`)
    } else {
      ElMessage.error(`❌ 视频解析服务连接失败 — ${result.info}`)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '测试请求失败')
  } finally {
    videoParserTesting.value = false
  }
}

async function handleTestTmdb() {
  tmdbTesting.value = true
  try {
    const result = await testExternalService({ type: 'tmdb', token: String(configs.tmdb_api_token || '') })
    if (result.ok) {
      ElMessage.success(`✅ TMDB 令牌有效 — ${result.info}`)
    } else {
      ElMessage.error(`❌ TMDB 连接失败 — ${result.info}`)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '测试请求失败')
  } finally {
    tmdbTesting.value = false
  }
}

async function handleTestProxy() {
  proxyTesting.value = true
  try {
    const result = await testExternalService({ type: 'proxy', url: String(configs.search_proxy_url || '') })
    if (result.ok) {
      ElMessage.success(`✅ 搜索代理可用 — ${result.info}`)
    } else {
      ElMessage.error(`❌ 搜索代理不可用 — ${result.info}`)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '测试请求失败')
  } finally {
    proxyTesting.value = false
  }
}

async function handleTestIpGeo() {
  ipGeoTesting.value = true
  try {
    const url = String(configs.ip_geo_api_url || "")
    if (!url) {
      ElMessage.warning("请先输入 IP 归属地查询 API 地址")
      return
    }
    const result = await testExternalService({ type: "ip_geo", url })
    if (result.ok) {
      ElMessage.success("✅ IP 归属地接口可用 — " + result.info)
    } else {
      ElMessage.error("❌ IP 归属地接口不可用 — " + result.info)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || "测试请求失败")
  } finally {
    ipGeoTesting.value = false
  }
}

// ── 标题过滤规则管理 ──
function batchAddFilterRules() {
  const raw = filterRuleBatchInput.value.trim()
  if (!raw) return
  const items = raw.split('\n').map(s => s.trim()).filter(s => s)
  let added = 0
  for (const item of items) {
    if (!filterRuleList.value.includes(item)) {
      filterRuleList.value.push(item)
      added++
    }
  }
  filterRuleBatchInput.value = ''
  syncFilterRules()
  if (added > 0) ElMessage.success(`已添加 ${added} 条规则`)
  else ElMessage.info('所有规则已存在')
}

function removeFilterRule(index: number) {
  filterRuleList.value.splice(index, 1)
  syncFilterRules()
}

function ruleTagType(rule: string): string {
  if (rule.startsWith('#')) return 'info'
  if (rule.startsWith('/') && (rule.endsWith('/') || rule.endsWith('/g') || rule.endsWith('/i') || rule.endsWith('/gi'))) return 'warning'
  return ''
}

function syncFilterRules() {
  configs.title_filter_rules = filterRuleList.value.join('\n')
}

// ── 失效关键词管理 ──
function batchAddInvalidKeywords() {
  const raw = invalidKeywordBatchInput.value.trim()
  if (!raw) return
  const items = raw.split('\n').map(s => s.trim()).filter(s => s)
  let added = 0
  for (const item of items) {
    if (!invalidKeywordList.value.includes(item)) {
      invalidKeywordList.value.push(item)
      added++
    }
  }
  invalidKeywordBatchInput.value = ''
  syncInvalidKeywords()
  if (added > 0) ElMessage.success(`已添加 ${added} 个关键词`)
  else ElMessage.info('所有关键词已存在')
}
function removeInvalidKeyword(index: number) {
  invalidKeywordList.value.splice(index, 1)
  syncInvalidKeywords()
}
function syncInvalidKeywords() {
  configs.link_invalid_keywords = invalidKeywordList.value.join('\n')
}

// ── 广告关键词管理 ──
function batchAddAdKeywords() {
  const raw = adKeywordBatchInput.value.trim()
  if (!raw) return
  const items = raw.split('\n').map(s => s.trim()).filter(s => s)
  let added = 0
  for (const item of items) {
    if (!adKeywordList.value.includes(item)) {
      adKeywordList.value.push(item)
      added++
    }
  }
  adKeywordBatchInput.value = ''
  syncAdKeywords()
  if (added > 0) ElMessage.success()
  else ElMessage.info('所有关键词已存在')
}
function removeAdKeyword(index: number) {
  adKeywordList.value.splice(index, 1)
  syncAdKeywords()
}
function syncAdKeywords() {
  configs.quark_ad_keywords = adKeywordList.value.join('\n')
}

// ── 警示文件夹名管理 ──
function batchAddWarningFolders() {
  const raw = warningFolderBatchInput.value.trim()
  if (!raw) return
  const items = raw.split('\n').map(s => s.trim()).filter(s => s)
  let added = 0
  for (const item of items) {
    if (!warningFolderList.value.includes(item)) {
      warningFolderList.value.push(item)
      added++
    }
  }
  warningFolderBatchInput.value = ''
  syncWarningFolders()
  if (added > 0) ElMessage.success()
  else ElMessage.info('所有文件夹名已存在')
}
function removeWarningFolder(index: number) {
  warningFolderList.value.splice(index, 1)
  syncWarningFolders()
}
function syncWarningFolders() {
  configs.quark_warning_folder_names = warningFolderList.value.join('\n')
}

// ── 可疑文件后缀管理 ──
function batchAddSusExtensions() {
  const raw = susExtensionBatchInput.value.trim()
  if (!raw) return
  const items = raw.split('\n').map(s => s.trim().toLowerCase().replace(/^\./, '')).filter(s => s)
  let added = 0
  for (const item of items) {
    if (!susExtensionList.value.includes(item)) {
      susExtensionList.value.push(item)
      added++
    }
  }
  susExtensionBatchInput.value = ''
  syncSusExtensions()
  if (added > 0) ElMessage.success('已添加 ${added} 个后缀')
  else ElMessage.info('所有后缀已存在')
}
function removeSusExtension(index: number) {
  susExtensionList.value.splice(index, 1)
  syncSusExtensions()
}
function syncSusExtensions() {
  configs.quark_sus_extensions = susExtensionList.value.join('\n')
}

function handleLogout() {
  localStorage.removeItem("admin_token")
  router.push("/admin/login")
}

async function handleSave() {
  saving.value = true
  try {
    const entries = rawConfigs.value.map(cfg => ({
      key: cfg.key,
      value: String(configs[cfg.key] ?? cfg.value),
    }))
    await updateSystemConfigs(entries)
    ElMessage.success('配置已保存')
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '保存失败')
  } finally {
    saving.value = false
  }
}

async function handleChangePassword() {
  const valid = await passwordFormRef.value?.validate().catch(() => false)
  if (!valid) return

  changingPassword.value = true
  try {
    const result = await changePasswordApi(passwordForm.oldPassword, passwordForm.newPassword)
    if (result.success) {
      ElMessage.success('✅ 密码修改成功，下次登录请使用新密码')
      passwordForm.oldPassword = ''
      passwordForm.newPassword = ''
      passwordForm.confirmPassword = ''
    } else {
      ElMessage.error(result.message)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '密码修改失败')
  } finally {
    changingPassword.value = false
  }
}

const fileInputRef = ref<HTMLInputElement>()

function triggerFileInput() {
  fileInputRef.value?.click()
}

async function handleFileSelect(event: Event) {
  const target = event.target as HTMLInputElement
  const file = target.files?.[0]
  if (!file) return

  if (!file.type.startsWith('image/')) {
    ElMessage.error('仅支持图片文件（JPEG/PNG/WebP）')
    target.value = ''
    return
  }

  if (file.size > 2 * 1024 * 1024) {
    ElMessage.error('图片大小不能超过 2MB')
    target.value = ''
    return
  }

  try {
    const result = await uploadFallbackImage(file)
    if (result.success) {
      configs.search_fallback_image = result.url
      ElMessage.success('✅ 兜底图已上传并生效')
    } else {
      ElMessage.error(result.message)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '上传失败')
  }
  target.value = ''
}

async function handleRemoveFallback() {
  try {
    configs.search_fallback_image = ''
    await updateSetting('search_fallback_image', '')
    ElMessage.success('已移除兜底图')
  } catch (e: any) {
    ElMessage.error('移除失败')
  }
}

const logoInputRef = ref<HTMLInputElement>()

function triggerLogoInput() {
  logoInputRef.value?.click()
}

async function handleLogoSelect(event: Event) {
  const target = event.target as HTMLInputElement
  const file = target.files?.[0]
  if (!file) return

  if (!file.type.startsWith('image/')) {
    ElMessage.error('仅支持图片文件（JPEG/PNG/WebP）')
    target.value = ''
    return
  }

  if (file.size > 2 * 1024 * 1024) {
    ElMessage.error('图片大小不能超过 2MB')
    target.value = ''
    return
  }

  try {
    const result = await uploadLogo(file)
    if (result.success) {
      configs.site_logo = result.url
      ElMessage.success('✅ LOGO 已上传并生效')
    } else {
      ElMessage.error(result.message)
    }
  } catch (e: any) {
    ElMessage.error(e.response?.data?.error || '上传失败')
  }
  target.value = ''
}

async function handleRemoveLogo() {
  try {
    configs.site_logo = ''
    await updateSetting('site_logo', '')
    ElMessage.success('已移除 LOGO')
  } catch (e: any) {
    ElMessage.error('移除失败')
  }
}
</script>

<style scoped>
.system-config {
}
.el-card {
  margin-bottom: 20px;
}
.el-card :deep(.el-card__header) {
  font-weight: 600;
  font-size: 15px;
}

/* el-divider 内容左对齐 */
:deep(.el-divider__text.is-left) {
  left: 0;
  padding-left: 0;
}

.form-tip {
  font-size: 12px;
  color: #909399;
  margin-top: 4px;
}
.fallback-upload-wrap {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.fallback-upload-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}
.fallback-preview {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.fallback-preview img {
  max-width: 100%;
  height: auto;
  max-height: 120px;
  border-radius: 8px;
  border: 1px solid var(--border-color);
  background: #f0f0f0;
  object-fit: contain;
}

/* ── 搜索策略 3列网格 ── */
.strategy-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 12px 16px;
}
.grid-cell {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* ── 策略卡片内的字段块 ── */
.strategy-section {
  padding: 0 4px;
}
.field-block {
  margin: 12px 0;
}
.field-label-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.field-label {
  font-size: 14px;
  font-weight: 500;
  color: #303133;
  white-space: nowrap;
}
.field-desc {
  font-size: 12px;
  color: #909399;
  margin: 3px 0 0 0;
  line-height: 1.5;
}

/* ── 关键词输入行 ── */
.keyword-input-row {
  display: flex;
  gap: 8px;
  flex: 1;
  min-width: 200px;
}
.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.tag-empty {
  font-size: 13px;
  color: #c0c4cc;
  margin-top: 8px;
}

/* ── 标题过滤规则说明（嵌入链接检测配置内） ── */
.filter-rule-help {
  margin-top: 8px;
  padding: 10px 12px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e8e8e8;
}
.filter-rule-help .help-title {
  font-weight: 600;
  font-size: 13px;
  margin: 8px 0 4px;
  color: #333;
}
.filter-rule-help .help-title:first-child {
  margin-top: 0;
}
.filter-rule-help .help-row {
  font-size: 12px;
  color: #555;
  margin: 3px 0;
  line-height: 1.6;
}
.filter-rule-help .help-row code {
  background: #eef1f5;
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 11px;
  font-family: monospace;
}

/* 标题过滤规则帮助 */
.filter-rules-help {
  margin-top: 8px;
  padding: 12px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e8e8e8;
}
.help-title {
  font-weight: 600;
  font-size: 13px;
  margin: 10px 0 6px;
  color: #333;
}
.help-title:first-child {
  margin-top: 0;
}
.help-row {
  font-size: 12px;
  color: #555;
  margin: 3px 0;
  line-height: 1.6;
}
.help-row code {
  background: #eef1f5;
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 11px;
  font-family: monospace;
}
.help-sample {
  background: #1e1e1e;
  color: #d4d4d4;
  padding: 10px 14px;
  border-radius: 6px;
  font-size: 12px;
  line-height: 1.6;
  overflow-x: auto;
  white-space: pre;
  margin: 6px 0 0;
  font-family: monospace;
}
.help-preview-row {
  font-size: 13px;
  margin: 4px 0;
  display: flex;
  align-items: center;
  gap: 6px;
}
.help-preview-label {
  color: #888;
  min-width: 70px;
  font-size: 12px;
}
.help-preview-original {
  color: #e74c3c;
}
.help-preview-filtered {
  color: #27ae60;
  font-weight: 500;
}

/* ── 过滤规则输入行 ── */
.filter-input-row {
  display: flex;
  gap: 8px;
  width: 100%;
  margin-bottom: 8px;
}
.filter-tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 8px;
}
.filter-empty {
  font-size: 13px;
  color: #c0c4cc;
  padding: 8px 0;
  margin-bottom: 8px;
}

/* 数据库状态网格 */
.db-status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
  margin-top: 8px;
}

.db-stat-item {
  background: #f8f9fa;
  border-radius: 10px;
  padding: 16px 12px;
  text-align: center;
  border: 1px solid #eee;
  transition: transform 0.15s, box-shadow 0.15s;
}
.db-stat-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.db-stat-value {
  white-space: nowrap;
  font-size: 24px;
  font-weight: 700;
  color: #303133;
  margin-bottom: 4px;
}
.db-stat-value.text-success { color: #67c23a; }
.db-stat-value.text-warning { color: #e6a23c; }
.db-stat-label {
  font-size: 12px;
  color: #909399;
}

/* 响应式：小屏幕降为2列或1列 */
@media (max-width: 900px) {
  .strategy-grid {
    grid-template-columns: 1fr 1fr;
  }
}
@media (max-width: 600px) {
  .strategy-grid {
    grid-template-columns: 1fr;
  }
}

/* PanSou info panel */
.pansou-status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
  margin-top: 8px;
}
.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}
.dot-ok { background: #67c23a; }
.dot-err { background: #f56c6c; }


</style>
