<script setup lang="ts">
import { NButton, NInput, NSpin, NSwitch, NTabPane, NTabs } from 'naive-ui'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'

import {
  getNotifyEvents,
  getSecuritySetting,
  getTaskSetting,
  getTelegramSetting,
  listNotifyChannels,
  listNotifyTemplates,
  testTelegram,
  updateNotifyEvents,
  updateSecuritySetting,
  updateTaskSetting,
  updateTelegramSetting,
} from '@/api/settings'
import AppInputNumber from '@/components/AppInputNumber.vue'
import FootNote from '@/components/FootNote.vue'
import FormField from '@/components/FormField.vue'
import AboutTab from '@/components/settings/AboutTab.vue'
import AccountSecurityCard from '@/components/settings/AccountSecurityCard.vue'
import NotifyChannelCard from '@/components/settings/NotifyChannelCard.vue'
import NotifyTemplateModal from '@/components/settings/NotifyTemplateModal.vue'
import { useAsync } from '@/composables/useAsync'
import type {
  NotifyChannelItem,
  NotifyEventsSetting,
  NotifyTemplateItem,
  SecuritySetting,
} from '@/types/api'
import { useAuthStore } from '@/stores/auth'
import { useToast } from '@/composables/useToast'

const message = useToast()
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()

// 设置页以 tab 组织:通知(管理 + 方式)/ 任务(抢机熔断阈值)/ 安全(WAF/网络/账号)
const tab = ref('notify')

// OAuth 绑定回跳:成功/失败提示后清理 query,并停在安全 tab;
// 绑定使令牌版本递增,fragment 携带的新 token 须先落地,否则后续请求 401
onMounted(() => {
  const { oauth, oauthError } = route.query
  if (oauth !== 'bound' && !oauthError) return
  consumeBindToken()
  tab.value = 'security'
  if (oauth === 'bound') message.success('外部身份绑定成功')
  if (typeof oauthError === 'string' && oauthError) message.error(oauthError)
  void router.replace({ query: {} })
})

/** 消费绑定回跳 fragment 中的新会话 token(无感换发,不进服务端日志) */
function consumeBindToken() {
  const hash = window.location.hash
  if (!hash.startsWith('#oauthToken=')) return
  const token = decodeURIComponent(hash.slice('#oauthToken='.length))
  history.replaceState(null, '', window.location.pathname + window.location.search)
  if (token) auth.setSession(token, new Date(Date.now() + 24 * 3600e3).toISOString())
}

// ---- 通知管理：按事件类型的推送开关 ----
const events = useAsync(getNotifyEvents)
const eventForm = ref<NotifyEventsSetting>({
  taskFail: true,
  taskRecover: true,
  snatchSuccess: true,
  tenantDead: true,
  taskStop: true,
  loginLock: true,
  modelDeprecated: true,
  logEventInstance: true,
  logEventIdentity: true,
  logEventPolicy: true,
  logEventRegion: true,
  logEventLogin: true,
})
const savingEvents = ref(false)

const eventRows: { key: keyof NotifyEventsSetting; label: string; hint: string }[] = [
  { key: 'taskFail', label: '任务失败', hint: '任务由成功转失败时推送（连续失败不重复发）' },
  { key: 'taskRecover', label: '任务恢复', hint: '任务由失败恢复正常时推送' },
  { key: 'snatchSuccess', label: '抢机成功', hint: '抢机任务达成目标台数时推送' },
  { key: 'tenantDead', label: '租户失联', hint: '测活失联集合变化时推送' },
  { key: 'taskStop', label: '任务停止', hint: '抢机连续鉴权失败熔断停止时推送' },
  { key: 'loginLock', label: '登录锁定', hint: '同一 IP+用户名连续失败达阈值时推送' },
  { key: 'modelDeprecated', label: '模型弃用预警', hint: 'AI 网关在池模型 30 天内被 OCI 弃用时随每日探测推送' },
  { key: 'logEventInstance', label: '实例生命周期', hint: '创建 / 终止 / 电源操作(Launch、Terminate、InstanceAction)' },
  { key: 'logEventIdentity', label: '用户与凭据', hint: '用户增删改、API Key 增删、能力变更' },
  { key: 'logEventPolicy', label: '策略变更', hint: 'IAM Policy 创建 / 修改 / 删除' },
  { key: 'logEventRegion', label: '区域订阅', hint: '订阅新区域(CreateRegionSubscription)' },
  { key: 'logEventLogin', label: '控制台登录', hint: 'OCI 控制台交互登录(InteractiveLogin)' },
]

/** 通知管理按业务域分组展示；新事件类型加入对应组即可 */
const eventGroups: { title: string; keys: (keyof NotifyEventsSetting)[] }[] = [
  { title: '任务', keys: ['taskFail', 'taskRecover', 'snatchSuccess', 'taskStop', 'modelDeprecated'] },
  { title: '租户与安全', keys: ['tenantDead', 'loginLock'] },
  {
    title: '云端事件 · 日志回传',
    keys: ['logEventInstance', 'logEventIdentity', 'logEventPolicy', 'logEventRegion', 'logEventLogin'],
  },
]

const enabledCount = computed(() => Object.values(eventForm.value).filter(Boolean).length)

// ---- 通知模板:每个事件可自定义推送 markdown ----
const templates = useAsync(listNotifyTemplates)
const tplShow = ref(false)
const tplCurrent = ref<NotifyTemplateItem | null>(null)
const tplHint = ref('')

/** eventRows 的 camelCase key → 模板 kind(snake_case) */
function tplKindOf(key: string) {
  return key.replace(/[A-Z]/g, (m) => '_' + m.toLowerCase())
}

function openTemplate(row: { key: string; label: string; hint: string }) {
  const kind = tplKindOf(row.key)
  const item = (templates.data.value ?? []).find((t) => t.kind === kind)
  if (!item) {
    message.error('模板加载中,请稍后再试')
    return
  }
  tplCurrent.value = item
  tplHint.value = row.hint
  tplShow.value = true
}

function onTemplateSaved(kind: string, template: string) {
  const item = (templates.data.value ?? []).find((t) => t.kind === kind)
  if (item) item.template = template
}

function rowsOf(keys: (keyof NotifyEventsSetting)[]) {
  return eventRows.filter((r) => keys.includes(r.key))
}

watch(
  () => events.data.value,
  (data) => {
    if (data) eventForm.value = { ...data }
  },
)

async function saveEvents() {
  savingEvents.value = true
  try {
    await updateNotifyEvents({ ...eventForm.value })
    message.success('通知管理已保存')
    void events.run({ silent: true })
  } catch (e) {
    message.error(e instanceof Error ? e.message : '保存失败')
  } finally {
    savingEvents.value = false
  }
}

// ---- 通知方式:多渠道(webhook/ntfy/bark/smtp)----
const channels = useAsync(listNotifyChannels)

/** 渠道保存接口回全渠道最新视图,就地覆盖免二次拉取 */
function onChannelsSaved(items: NotifyChannelItem[]) {
  channels.data.value = items
}

// ---- 通知方式：Telegram 渠道配置 ----
const setting = useAsync(getTelegramSetting)
const enabled = ref(false)
const chatId = ref('')
const botToken = ref('')
const saving = ref(false)
const testing = ref(false)

// 服务端数据到达后回填表单；token 不回明文，只以占位符提示已配置态
watch(
  () => setting.data.value,
  (data) => {
    if (!data) return
    enabled.value = data.enabled
    chatId.value = data.chatId
    botToken.value = ''
  },
)

const tokenPlaceholder = computed(() => {
  const data = setting.data.value
  if (!data?.tokenSet) return '123456789:AAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
  return `已配置（尾号 ${data.tokenTail}），留空沿用`
})

// ---- 任务设置：抢机熔断阈值 ----
const taskSetting = useAsync(getTaskSetting)
const authFailLimit = ref<number | null>(3)
const savingTask = ref(false)

/** 抢机熔断流程步骤条;hot 标记可配置的关键一步 */
const CIRCUIT_STEPS = [
  { t: '连续失败', d: '抢机请求连续返回 NotAuthenticated', hot: false },
  { t: '达到阈值', d: '失败次数达到下方配置值', hot: true },
  { t: '熔断停止', d: '任务置为已熔断并退出调度', hot: false },
  { t: '推送与恢复', d: '发送「任务停止」通知,修复 Key 后在任务页重新启用', hot: false },
]

watch(
  () => taskSetting.data.value,
  (data) => {
    if (data) authFailLimit.value = data.snatchAuthFailLimit
  },
)

// ---- 安全设置:WAF 参数 / 真实IP请求头 / 面板地址 ----
const security = useAsync(getSecuritySetting)
const securityForm = ref<SecuritySetting>({
  loginFailLimit: 5,
  loginLockMinutes: 15,
  ipRateRps: 10,
  ipRateBurst: 30,
  realIpHeader: '',
  appUrl: '',
})
const savingSecurity = ref(false)

/** 真实IP请求头预设;设计稿 segmented 选项,预设外经「自定义」输入 */
const REALIP_PRESETS = [
  { label: '直连', value: '' },
  { label: 'X-Forwarded-For', value: 'X-Forwarded-For' },
  { label: 'X-Real-IP', value: 'X-Real-IP' },
  { label: 'CF-Connecting-IP', value: 'CF-Connecting-IP' },
]
const customHeaderMode = ref(false)

function pickHeader(v: string) {
  customHeaderMode.value = false
  securityForm.value.realIpHeader = v
}

function pickCustomHeader() {
  if (customHeaderMode.value) return
  customHeaderMode.value = true
  // 从预设切入自定义时清空,避免误保存预设值
  if (REALIP_PRESETS.some((p) => p.value === securityForm.value.realIpHeader))
    securityForm.value.realIpHeader = ''
}

watch(
  () => security.data.value,
  (data) => {
    if (!data) return
    securityForm.value = { ...data }
    customHeaderMode.value =
      data.realIpHeader !== '' && !REALIP_PRESETS.some((p) => p.value === data.realIpHeader)
  },
)

async function saveSecurity() {
  savingSecurity.value = true
  try {
    await updateSecuritySetting({ ...securityForm.value })
    message.success('安全设置已保存,立即生效')
    void security.run({ silent: true })
  } catch (e) {
    message.error(e instanceof Error ? e.message : '保存失败')
  } finally {
    savingSecurity.value = false
  }
}

async function saveTaskSetting() {
  const limit = authFailLimit.value
  if (limit == null || limit < 1 || limit > 10) {
    message.error('熔断阈值须在 1-10 之间')
    return
  }
  savingTask.value = true
  try {
    await updateTaskSetting({ snatchAuthFailLimit: limit })
    message.success('任务设置已保存')
    void taskSetting.run({ silent: true })
  } catch (e) {
    message.error(e instanceof Error ? e.message : '保存失败')
  } finally {
    savingTask.value = false
  }
}

const initialLoading = computed(
  () =>
    (setting.loading.value && !setting.data.value) ||
    (events.loading.value && !events.data.value) ||
    (taskSetting.loading.value && !taskSetting.data.value) ||
    (security.loading.value && !security.data.value),
)

async function save() {
  saving.value = true
  try {
    const token = botToken.value.trim()
    await updateTelegramSetting({
      enabled: enabled.value,
      chatId: chatId.value.trim(),
      // 留空表示沿用已保存的 token
      ...(token ? { botToken: token } : {}),
    })
    message.success('通知设置已保存')
    botToken.value = ''
    void setting.run({ silent: true })
  } catch (e) {
    message.error(e instanceof Error ? e.message : '保存失败')
  } finally {
    saving.value = false
  }
}

async function sendTest() {
  testing.value = true
  try {
    await testTelegram()
    message.success('测试消息已发送，请在 Telegram 查收')
  } catch (e) {
    message.error(e instanceof Error ? e.message : '发送失败')
  } finally {
    testing.value = false
  }
}
</script>

<template>
  <div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
    <div class="flex flex-wrap items-baseline gap-2.5">
      <h1 class="page-title">设置</h1>
      <span class="text-[13px] text-ink-3">面板级系统配置</span>
    </div>

    <NTabs v-model:value="tab" type="line">
      <NTabPane name="notify" tab="通知" />
      <NTabPane name="task" tab="任务" />
      <NTabPane name="security" tab="安全" />
      <NTabPane name="about" tab="关于" />
    </NTabs>

    <div v-if="initialLoading" class="panel flex max-w-[620px] items-center justify-center py-20">
      <NSpin size="small" />
    </div>
    <div
      v-else-if="tab === 'notify'"
      class="grid max-w-[1180px] grid-cols-[7fr_5fr] items-start gap-4 max-lg:grid-cols-1"
    >
      <!-- 左:通知管理(按事件类型控制是否推送) -->
      <div class="panel">
        <div
          class="flex flex-wrap items-center justify-between gap-3 border-b border-line-soft px-4.5 py-3.5"
        >
          <div class="min-w-0">
            <div class="text-sm font-semibold">通知管理</div>
            <div class="mt-0.5 text-xs text-ink-3">
              按事件类型控制是否推送；事件仅在状态变化时触发
            </div>
          </div>
          <span
            class="inline-flex items-center gap-1.5 rounded-full bg-wash px-2.5 py-[3px] text-[11.5px] text-ink-2"
          >
            <span class="h-1.5 w-1.5 rounded-full bg-ok" />
            已开启 {{ enabledCount }} / {{ eventRows.length }} 项
          </span>
        </div>
        <div class="px-4.5 pb-2">
          <template v-for="group in eventGroups" :key="group.title">
            <div class="pt-3.5 pb-1 text-[11px] font-semibold tracking-wider text-ink-3 uppercase">
              {{ group.title }}
            </div>
            <div
              v-for="row in rowsOf(group.keys)"
              :key="row.key"
              class="flex items-center justify-between gap-3 border-b border-line-soft py-2.5 last:border-b-0"
            >
              <div class="min-w-0">
                <div class="text-[13px] font-medium">{{ row.label }}</div>
                <div class="mt-0.5 text-xs text-ink-3">{{ row.hint }}</div>
              </div>
              <div class="flex flex-none items-center gap-2.5">
                <button
                  type="button"
                  class="cursor-pointer text-xs text-accent hover:opacity-80"
                  @click="openTemplate(row)"
                >
                  模板
                </button>
                <NSwitch v-model:value="eventForm[row.key]" size="small" />
              </div>
            </div>
          </template>
        </div>
        <div class="flex items-center gap-2 border-t border-line-soft px-4.5 py-3.5">
          <NButton size="small" type="primary" :loading="savingEvents" @click="saveEvents">
            保存
          </NButton>
          <span class="text-xs text-ink-3">全部开关整体保存，立即生效</span>
        </div>
      </div>

      <!-- 右:通知方式 + 送达规则 -->
      <div class="flex flex-col gap-4">
        <div class="panel">
          <div class="border-b border-line-soft px-4.5 py-3.5">
            <div class="text-sm font-semibold">通知方式</div>
            <div class="mt-0.5 text-xs text-ink-3">配置通知送达的渠道</div>
          </div>
          <div class="flex items-center gap-3 border-b border-line-soft px-4.5 py-3.5">
            <span
              class="flex h-8.5 w-8.5 flex-none items-center justify-center rounded-full bg-info text-[15px] font-bold text-white"
            >
              T
            </span>
            <div class="min-w-0 flex-1">
              <div class="flex flex-wrap items-center gap-2">
                <span class="text-[13px] font-medium">Telegram</span>
                <span
                  class="inline-flex items-center gap-1.5 rounded-full bg-wash px-2 py-px text-[11.5px] text-ink-2"
                >
                  <span
                    class="h-1.5 w-1.5 rounded-full"
                    :class="enabled ? 'bg-ok' : 'bg-ink-3/50'"
                  />
                  {{ enabled ? '已启用' : '未启用' }}
                </span>
              </div>
              <div class="mt-0.5 text-xs text-ink-3">开启的通知事件推送到指定 chat</div>
            </div>
            <NSwitch v-model:value="enabled" size="small" />
          </div>

          <div class="px-4.5 py-3.5">
            <FormField
              label="Bot Token"
              hint="来自 @BotFather 的 bot 令牌；服务端加密存储，保存后不再回显明文"
            >
              <NInput
                v-model:value="botToken"
                type="password"
                show-password-on="click"
                :placeholder="tokenPlaceholder"
              />
            </FormField>
            <FormField label="Chat ID" hint="接收通知的会话 ID：个人为正整数，群组为负整数">
              <NInput v-model:value="chatId" placeholder="如 123456789" />
            </FormField>
            <div class="mt-1 flex items-center gap-2">
              <NButton size="small" type="primary" :loading="saving" @click="save">保存</NButton>
              <NButton size="small" :loading="testing" @click="sendTest">发送测试消息</NButton>
            </div>
            <div class="mt-2 text-xs text-ink-3">测试使用已保存的配置，修改后请先保存</div>
          </div>

          <FootNote>
            获取 chat_id：① 用 @BotFather 创建 bot 拿到 token；② 给 bot 发一条消息（群组则把 bot
            拉进群）；③ 浏览器打开 api.telegram.org/bot&lt;token&gt;/getUpdates，响应中
            message.chat.id 即为 chat_id
          </FootNote>

          <!-- 多渠道:与 Telegram 并存,可同时启用任意多个 -->
          <div class="border-t border-line-soft">
            <NotifyChannelCard
              v-for="ch in channels.data.value ?? []"
              :key="ch.type"
              :item="ch"
              @saved="onChannelsSaved"
            />
          </div>
        </div>

        <div class="panel px-4.5 py-3.5">
          <div class="text-[13px] font-semibold">送达规则</div>
          <div class="mt-1.5 text-xs leading-relaxed text-ink-3">
            事件同时投递到全部已启用渠道；发送为异步、10 秒超时，单渠道失败仅记后端日志，
            不影响其他渠道与任务执行；同一事件按状态变化去重，不会刷屏。
            云端事件另受「日志回传」链路约束——未建链路的租户不产生该类通知。
          </div>
        </div>
      </div>
    </div>
    <div v-else-if="tab === 'task'" class="flex max-w-[860px] flex-col gap-4">
      <!-- 任务行为：抢机熔断阈值 -->
      <div class="panel">
        <div class="border-b border-line-soft px-4.5 py-3.5">
          <div class="text-sm font-semibold">抢机熔断</div>
          <div class="mt-0.5 text-xs text-ink-3">
            API Key 失效后自动停止无意义的重试,避免频繁鉴权失败触发云端风控
          </div>
        </div>
        <div class="grid grid-cols-4 px-2.5 pt-4.5 pb-1 max-md:grid-cols-2 max-md:gap-y-4">
          <div
            v-for="(s, i) in CIRCUIT_STEPS"
            :key="s.t"
            class="relative px-2.5 text-center after:absolute after:top-[15px] after:left-[calc(50%+26px)] after:h-[1.5px] after:w-[calc(100%-52px)] after:bg-line last:after:hidden max-md:after:hidden"
          >
            <span
              class="inline-flex h-[30px] w-[30px] items-center justify-center rounded-full border-[1.5px] text-[13px] font-semibold"
              :class="
                s.hot
                  ? 'border-accent bg-accent/15 text-accent'
                  : 'border-line bg-wash text-ink-2'
              "
            >
              {{ i + 1 }}
            </span>
            <div class="mt-2 text-[12.5px] font-semibold">{{ s.t }}</div>
            <div class="mt-0.5 text-[11.5px] leading-normal text-ink-3">{{ s.d }}</div>
          </div>
        </div>
        <div class="px-4.5 pt-3 pb-4.5">
          <div class="flex flex-wrap items-center gap-4">
            <AppInputNumber
              v-model:value="authFailLimit"
              :min="1"
              :max="10"
              size="large"
              class="!w-28"
            />
            <div class="min-w-0">
              <div class="text-[13px] font-semibold">熔断阈值(连续 NotAuthenticated 次数)</div>
              <div class="mt-0.5 text-xs text-ink-3">
                范围 1-10,默认 3;保存后下一次抢机执行即按新阈值判定
              </div>
            </div>
          </div>
          <div class="mt-4 grid grid-cols-3 gap-3 max-md:grid-cols-1">
            <div class="rounded-lg bg-wash px-3.5 py-3">
              <div class="text-[11px] tracking-wider text-ink-3 uppercase">当前生效值</div>
              <div class="mt-1 text-[13px] font-semibold">
                {{ taskSetting.data.value ? `${taskSetting.data.value.snatchAuthFailLimit} 次` : '—' }}
              </div>
            </div>
            <div class="rounded-lg bg-wash px-3.5 py-3">
              <div class="text-[11px] tracking-wider text-ink-3 uppercase">受控任务</div>
              <div class="mt-1 text-[13px] font-semibold">抢机任务(snatch)</div>
            </div>
            <div class="rounded-lg bg-wash px-3.5 py-3">
              <div class="text-[11px] tracking-wider text-ink-3 uppercase">熔断后恢复方式</div>
              <div class="mt-1 text-[13px] font-semibold">任务页手动启用</div>
            </div>
          </div>
          <div class="mt-4 flex items-center gap-2">
            <NButton size="small" type="primary" :loading="savingTask" @click="saveTaskSetting">
              保存
            </NButton>
            <span class="text-xs text-ink-3">保存后立即生效,无需重启</span>
          </div>
        </div>
        <FootNote>
          熔断只针对鉴权类失败(API Key 失效);容量不足等业务失败不计入,抢机会持续重试
        </FootNote>
      </div>
    </div>

    <!-- 安全:双栏 —— 左访问防护 / 网络与地址,右账号安全与登录方式 -->
    <div v-else-if="tab === 'security'" class="grid max-w-[1180px] grid-cols-2 items-start gap-4 max-lg:grid-cols-1">
        <div class="flex flex-col gap-4">
          <div class="panel">
            <div class="border-b border-line-soft px-4.5 py-3.5">
              <div class="text-sm font-semibold">访问防护</div>
              <div class="mt-0.5 text-xs text-ink-3">
                登录守卫与全局 IP 限速,超限返回 429;锁定事件可推送告警(通知管理 → 登录锁定)
              </div>
            </div>
            <div class="grid grid-cols-2 gap-x-4 px-4.5 py-3.5 max-md:grid-cols-1">
              <FormField label="登录失败阈值" hint="窗口内同一 IP+用户名连续失败达到该次数即锁定(1-20)">
                <AppInputNumber v-model:value="securityForm.loginFailLimit" :min="1" :max="20" />
              </FormField>
              <FormField label="锁定时长(分钟)" hint="锁定持续时间,同时是失败计数窗口(1-1440)">
                <AppInputNumber v-model:value="securityForm.loginLockMinutes" :min="1" :max="1440" />
              </FormField>
              <FormField label="限速(req/s)" hint="每 IP 令牌桶每秒补充令牌数(1-100)">
                <AppInputNumber v-model:value="securityForm.ipRateRps" :min="1" :max="100" />
              </FormField>
              <FormField label="突发额度" hint="令牌桶容量,允许的瞬时突发请求数(1-500)">
                <AppInputNumber v-model:value="securityForm.ipRateBurst" :min="1" :max="500" />
              </FormField>
            </div>
          </div>

          <div class="panel">
            <div class="border-b border-line-soft px-4.5 py-3.5">
              <div class="text-sm font-semibold">网络与地址</div>
              <div class="mt-0.5 text-xs text-ink-3">
                反代部署时必须正确选择真实IP请求头,否则限速与锁定会作用在反代 IP 上
              </div>
            </div>
            <div class="px-4.5 py-3.5">
              <FormField
                label="真实IP请求头"
                hint="Caddy/Nginx 反代选 X-Forwarded-For;Cloudflare 前置时选 CF-Connecting-IP;直连部署保持「直连」防伪造"
              >
                <div class="flex flex-wrap gap-2">
                  <button
                    v-for="opt in REALIP_PRESETS"
                    :key="opt.label"
                    type="button"
                    class="cursor-pointer rounded-md border px-3 py-1.5 text-xs"
                    :class="
                      !customHeaderMode && securityForm.realIpHeader === opt.value
                        ? 'border-accent bg-accent/10 font-semibold text-accent'
                        : 'border-line bg-white text-ink-2 hover:border-ink-3'
                    "
                    @click="pickHeader(opt.value)"
                  >
                    {{ opt.label }}
                  </button>
                  <button
                    type="button"
                    class="cursor-pointer rounded-md border border-dashed px-3 py-1.5 text-xs"
                    :class="
                      customHeaderMode
                        ? 'border-accent bg-accent/10 font-semibold text-accent'
                        : 'border-line bg-white text-ink-2 hover:border-ink-3'
                    "
                    @click="pickCustomHeader"
                  >
                    自定义…
                  </button>
                </div>
                <NInput
                  v-if="customHeaderMode"
                  v-model:value="securityForm.realIpHeader"
                  class="mono mt-2 !w-70"
                  placeholder="自定义头名,如 X-Client-IP"
                />
                <div v-if="customHeaderMode" class="mt-1.5 text-xs text-ink-3">
                  头名仅限字母数字与连字符
                </div>
              </FormField>
              <FormField
                label="面板地址"
                hint="面板公网基址,优先于 PUBLIC_URL 环境变量;日志回传一键创建与 OAuth 回调以此拼接 URL"
              >
                <NInput v-model:value="securityForm.appUrl" placeholder="https://demo.example.com" />
              </FormField>
              <div class="mt-1 flex items-center gap-2">
                <NButton size="small" type="primary" :loading="savingSecurity" @click="saveSecurity">
                  保存
                </NButton>
                <span class="text-xs text-ink-3">整体保存,立即生效,无需重启</span>
              </div>
            </div>
            <FootNote>
              登录锁定为内存计数,重启服务即清零;真实IP解析对 X-Forwarded-For
              取链尾(反代追加的直连对端),链首可被客户端伪造不采信
            </FootNote>
          </div>
        </div>

        <!-- 账号安全:两步验证 / 外部身份绑定 / 登录方式配置 -->
        <div class="flex flex-col gap-4">
          <AccountSecurityCard />
        </div>
      </div>

    <!-- 关于:构建信息与项目主页(v-else-if 挂载时才启动粒子动画) -->
    <AboutTab v-else-if="tab === 'about'" />

    <NotifyTemplateModal
      v-model:show="tplShow"
      :item="tplCurrent"
      :hint="tplHint"
      @saved="onTemplateSaved"
    />
  </div>
</template>
