<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import {
  fetchHikDeviceLinks,
  fetchHikHealth,
  fetchHikPeopleCounting,
  fetchHikVenuesHourly,
  formatIncludedGroups,
  type HikDeviceLinkRow,
  healthStatusColor,
  netInVenue,
  postHikDeviceLinksRetry,
  type HikDeviceLinksResponse,
  type HikPeopleCountingGroup,
  type HikPeopleCountingVenue,
  type HikPeopleCountingResponse,
  type HikVenueRangeTotalRow,
  type HikHealthResponse,
  type HikVenueHourlyRow,
  type HikVenuesHourlyResponse,
} from '../../api/hikPdcClient'

const loadingPc = ref(false)
const loadingHealth = ref(false)

function todayYmd(): string {
  const d = new Date()
  const p = (n: number) => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
}

const activeTab = ref('realtime')
const peopleCountingDate = ref(todayYmd())
/** 对应请求参数 end_date；留空则仅按起始日单日查询 */
const peopleCountingEndDate = ref('')

const peopleData = ref<HikPeopleCountingResponse | null>(null)
const healthData = ref<HikHealthResponse | null>(null)
const healthError = ref<string | null>(null)
const lastFetchAt = ref<string | null>(null)

const venues = computed(() => peopleData.value?.venues ?? [])

/** 是否展示区间内各馆累计（服务端返回 range / venuesRangeTotals 等时） */
const peopleRangeSectionVisible = computed(() => {
  const p = peopleData.value
  if (!p || p.code !== 200) return false
  const totals = p.venuesRangeTotals
  const hasTotals = Array.isArray(totals) && totals.length > 0
  const hasMeta =
    !!(p.rangeFrom || p.rangeTo || (p.venuesRangeTotalsNote || '').trim() || (p.venuesRangeMissingDates?.length ?? 0))
  return hasTotals || hasMeta
})

const hourlyDate = ref(todayYmd())
const hourlyVenueId = ref<string | undefined>(undefined)
const hourlyData = ref<HikVenuesHourlyResponse | null>(null)
const loadingHourly = ref(false)

const deviceProbe = ref(false)
const deviceLinksData = ref<HikDeviceLinksResponse | null>(null)
const loadingDevices = ref(false)
const retrySubmitting = ref(false)
const selectedRetrySlots = ref<number[]>([])

const nestedModalVisible = ref(false)
const nestedModalVenue = ref<HikPeopleCountingVenue | null>(null)

const hourlyVenueFilterOptions = computed(() =>
  venues.value.map((v) => ({
    label: `${v.venueName || v.venueId} (${v.venueId})`,
    value: String(v.venueId),
  })),
)

/** 按小时页场馆下拉：支持按名称或场馆 ID 关键字筛选 */
function filterHourlyVenueOption(inputValue: string, option: { label?: string; value?: string }) {
  const q = (inputValue || '').trim().toLowerCase()
  if (!q) return true
  const label = String(option?.label ?? '').toLowerCase()
  const value = String(option?.value ?? '').toLowerCase()
  return label.includes(q) || value.includes(q)
}

async function refreshPeopleCounting(showToast = false) {
  const d = (peopleCountingDate.value || '').trim()
  const ed = (peopleCountingEndDate.value || '').trim()
  if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) {
    Message.warning('请选择合法的统计起始日期（yyyy-MM-dd）')
    return
  }
  if (ed && !/^\d{4}-\d{2}-\d{2}$/.test(ed)) {
    Message.warning('请选择合法的统计结束日期（yyyy-MM-dd）')
    return
  }
  if (ed && ed < d) {
    Message.warning('统计结束日期须大于或等于起始日期')
    return
  }
  loadingPc.value = true
  try {
    peopleData.value = await fetchHikPeopleCounting(d, ed || undefined)
    lastFetchAt.value = new Date().toLocaleString('zh-CN')
    if (peopleData.value.code !== 200) {
      Message.warning(peopleData.value.message || `请求失败（错误码 ${peopleData.value.code}）`)
    } else if (showToast) {
      Message.success('客流数据已更新')
    }
  } catch (e: unknown) {
    peopleData.value = null
    const ax = e as { response?: { status?: number; data?: { message?: string } | string } }
    const st = ax.response?.status
    const raw = ax.response?.data
    const rawObj =
      typeof raw === 'object' && raw != null ? (raw as Record<string, unknown>) : null
    const bodyMsg = rawObj
      ? String(rawObj.message ?? rawObj.error ?? rawObj.hint ?? '').trim()
      : ''
    const msg =
      e && typeof e === 'object' && 'message' in e ? String((e as { message?: string }).message) : String(e)
    if (st === 404) {
      Message.warning(bodyMsg || '暂无该日期的归档数据')
    } else if (st === 400) {
      Message.warning(bodyMsg || '请求参数无效（请检查日期区间）')
    } else {
      Message.error(msg || '客流接口请求失败')
    }
  } finally {
    loadingPc.value = false
  }
}

async function refreshHealth() {
  loadingHealth.value = true
  healthError.value = null
  try {
    healthData.value = await fetchHikHealth()
  } catch (e: unknown) {
    healthData.value = null
    healthError.value =
      e && typeof e === 'object' && 'message' in e ? String((e as { message?: string }).message) : String(e)
    Message.error(healthError.value || '健康检查失败')
  } finally {
    loadingHealth.value = false
  }
}

async function refreshAll() {
  await Promise.all([refreshPeopleCounting(true), refreshHealth()])
}

async function queryHourly() {
  const d = (hourlyDate.value || '').trim()
  if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) {
    Message.warning('请选择合法日期 yyyy-MM-dd')
    return
  }
  loadingHourly.value = true
  hourlyData.value = null
  try {
    hourlyData.value = await fetchHikVenuesHourly(d, hourlyVenueId.value || undefined)
    Message.success('已加载按小时数据')
  } catch (e: unknown) {
    const msg =
      e && typeof e === 'object' && 'message' in e ? String((e as { message?: string }).message) : String(e)
    Message.error(msg || '按小时查询失败')
  } finally {
    loadingHourly.value = false
  }
}

async function loadDeviceLinks() {
  loadingDevices.value = true
  deviceLinksData.value = null
  selectedRetrySlots.value = []
  try {
    deviceLinksData.value = await fetchHikDeviceLinks({
      probe: deviceProbe.value,
      live: deviceProbe.value,
    })
    Message.success(deviceProbe.value ? '已加载设备（含探测）' : '已加载设备链路')
    const failedSlots = [...new Set((deviceLinksData.value.devices ?? [])
      .filter((x) => deviceLinkNotOnline(x.status))
      .map((x) => x.deviceSlot)
      .filter((n): n is number => typeof n === 'number' && Number.isFinite(n)))].sort((a, b) => a - b)
    selectedRetrySlots.value = failedSlots
  } catch (e: unknown) {
    const ax = e as { response?: { status?: number; data?: { message?: string } } }
    const st = ax.response?.status
    const msg =
      ax.response?.data?.message ||
      (e && typeof e === 'object' && 'message' in e ? String((e as { message?: string }).message) : String(e))
    Message.error(st === 403 ? '探测已被服务端关闭' : msg || '设备链路加载失败')
  } finally {
    loadingDevices.value = false
  }
}

function deviceLinkNotOnline(status: string | undefined): boolean {
  return String(status || '').trim().toUpperCase() !== 'ONLINE'
}

/** 重试结果/下拉项：用配置中的设备名（多即场馆名）+ IP 展示 */
function formatDeviceVenueIpLabel(deviceSlot: number): string {
  const devs = deviceLinksData.value?.devices ?? []
  const row = devs.find((d) => d.deviceSlot === deviceSlot)
  const venueName = (row?.name || '').trim() || `设备槽位 ${deviceSlot}`
  const ip = (row?.ip || '').trim()
  return ip ? `${venueName}　${ip}` : venueName
}

function retryOptions() {
  const devs = deviceLinksData.value?.devices ?? []
  const slots = [...new Set(devs.map((d) => d.deviceSlot).filter((n): n is number => typeof n === 'number'))]
  return slots.sort((a, b) => a - b).map((s) => ({
    label: formatDeviceVenueIpLabel(s),
    value: s,
  }))
}

async function confirmRetryLogin() {
  const indices = selectedRetrySlots.value.filter((n) => Number.isFinite(n))
  if (!indices.length) {
    Message.warning('请至少选择一个设备槽位序号')
    return
  }
  retrySubmitting.value = true
  try {
    const res = await postHikDeviceLinksRetry({ deviceIndices: indices })
    if (res.code === 200) {
      Message.success(res.message || '重试指令已下发')
      const lines = (res.results ?? []).map((r) => {
        const head = formatDeviceVenueIpLabel(r.deviceIndex)
        return `${head}：${r.ok ? '成功' : '失败'} ${r.message || ''}`.trim()
      })
      if (lines.length) {
        Modal.info({
          title: '重试结果',
          content: lines.join('\n'),
        })
      }
      await loadDeviceLinks()
      await refreshHealth()
    } else {
      Message.warning(res.message || `错误码 ${res.code}`)
    }
  } catch (e: unknown) {
    const ax = e as { response?: { status?: number } }
    const msg =
      e && typeof e === 'object' && 'message' in e ? String((e as { message?: string }).message) : String(e)
    Message.error(ax.response?.status === 403 ? '服务端已禁止远程重试登录' : msg || '重试请求失败')
  } finally {
    retrySubmitting.value = false
  }
}

function openVenueNestedModal(v: HikPeopleCountingVenue) {
  nestedModalVenue.value = v
  nestedModalVisible.value = true
}

onMounted(() => {
  void refreshAll()
})

const venueRangeTotalsColumns = [
  { title: '场馆ID', dataIndex: 'venueId', width: 120, ellipsis: true, tooltip: true },
  { title: '场馆名称', dataIndex: 'venueName', width: 180, ellipsis: true, tooltip: true },
  { title: '入馆总人数', dataIndex: 'enter', width: 130 },
  { title: '离开（区间累计）', dataIndex: 'exit', width: 125 },
  { title: '经过（区间累计）', dataIndex: 'passing', width: 125 },
]

function venueRangeTotalsRows(): HikVenueRangeTotalRow[] {
  const list = peopleData.value?.venuesRangeTotals
  return Array.isArray(list) ? list : []
}

/** Arco Table summary：区间累计数值列合计（合计行固定在表格底部，见 scroll.y） */
function venueRangeTotalsSummary(ctx: { data: HikVenueRangeTotalRow[] }) {
  const rows = Array.isArray(ctx.data) ? ctx.data : []
  let enter = 0
  let exit = 0
  let passing = 0
  for (const r of rows) {
    enter += Number(r.enter) || 0
    exit += Number(r.exit) || 0
    passing += Number(r.passing) || 0
  }
  return [
    {
      venueId: '合计',
      venueName: '—',
      enter,
      exit,
      passing,
    },
  ]
}

/** 各场馆当日表：数值列合计；在馆(推算) 为汇总进入/离开的推算值（与各馆在馆之和一致） */
function venuesDailySummary(ctx: { data: HikPeopleCountingVenue[] }) {
  const rows = Array.isArray(ctx.data) ? ctx.data : []
  let enter = 0
  let exit = 0
  let passing = 0
  let updateCount = 0
  for (const r of rows) {
    enter += Number(r.enter) || 0
    exit += Number(r.exit) || 0
    passing += Number(r.passing) || 0
    updateCount += Number(r.updateCount) || 0
  }
  return [
    {
      venueId: '合计',
      venueName: '—',
      enter,
      exit,
      passing,
      updateCount,
    },
  ]
}

function missingRangeDatesText(): string {
  const arr = peopleData.value?.venuesRangeMissingDates
  if (!Array.isArray(arr) || !arr.length) return ''
  return arr.filter(Boolean).join('、')
}

const venueColumns = [
  { title: '场馆ID', dataIndex: 'venueId', width: 140, ellipsis: true, tooltip: true },
  { title: '场馆名称', dataIndex: 'venueName', width: 180, ellipsis: true, tooltip: true },
  { title: '进入', dataIndex: 'enter', width: 96 },
  { title: '离开', dataIndex: 'exit', width: 96 },
  { title: '经过', dataIndex: 'passing', width: 96 },
  { title: '在馆(推算)', slotName: 'net', width: 110 },
  { title: '更新次数', dataIndex: 'updateCount', width: 110 },
  { title: '最后更新', slotName: 'venueLastUpdate', width: 190, ellipsis: true, tooltip: true },
  { title: '包含组', slotName: 'included', width: 220, ellipsis: true, tooltip: true },
  { title: '馆内子组', slotName: 'nested', width: 100 },
]

const hourlyDetailColumns = [
  { title: '小时', dataIndex: 'hourLabel', width: 100 },
  { title: '进入', dataIndex: 'enter', width: 90 },
  { title: '离开', dataIndex: 'exit', width: 90 },
  { title: '经过', dataIndex: 'passing', width: 90 },
]

function hourlyRowsForVenue(row: HikVenueHourlyRow) {
  const h = row.hourly ?? []
  return h.map((b) => ({
    key: `${row.venueId}-${b.hour}`,
    hourLabel: b.hourLabel != null && String(b.hourLabel).trim() ? b.hourLabel : String(b.hour),
    enter: b.enter,
    exit: b.exit,
    passing: b.passing,
  }))
}

const deviceColumns = [
  { title: '槽位', dataIndex: 'deviceSlot', width: 72 },
  { title: '名称', dataIndex: 'name', width: 120, ellipsis: true, tooltip: true },
  { title: 'IP', dataIndex: 'ip', width: 130 },
  { title: '端口', dataIndex: 'port', width: 80 },
  { title: '状态', slotName: 'devStatus', width: 120 },
  { title: '说明', dataIndex: 'statusHint', ellipsis: true, tooltip: true },
  { title: '探测', slotName: 'probe', width: 200, ellipsis: true, tooltip: true },
]

function probeSummary(row: HikDeviceLinkRow): string {
  const p = row.networkProbe
  if (!p) return '—'
  const parts = [
    p.tcpReachable !== undefined ? (p.tcpReachable ? 'TCP 已连通' : 'TCP 未连通') : '',
    p.tcpConnectLatencyMs != null ? `延迟 ${p.tcpConnectLatencyMs} ms` : '',
    p.icmpReachable !== undefined ? (p.icmpReachable ? 'ICMP 已通' : 'ICMP 不可用') : '',
    (p.summaryHint || '').trim(),
  ].filter(Boolean)
  return parts.length ? parts.join('，') : '—'
}

function healthServiceStatusText(status: string | undefined): string {
  const u = String(status || '').toUpperCase()
  if (u === 'UP') return '正常'
  if (u === 'DEGRADED') return '降级'
  return (status || '').trim() || '—'
}

/** 表格展示：YYYY-MM-DD HH:mm:ss（本地时区） */
function formatZhYmdHms(ts: string | number | undefined | null): string {
  if (ts == null || ts === '') return '—'
  let d: Date
  if (typeof ts === 'number') {
    d = new Date(ts)
  } else {
    const s = String(ts).trim()
    if (/^\d+$/.test(s)) {
      const n = Number(s)
      const ms = n > 1e11 ? n : n * 1000
      d = new Date(ms)
    } else {
      d = new Date(s)
    }
  }
  if (Number.isNaN(d.getTime())) return String(ts)
  const p = (n: number) => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}

function formatServiceTimestamp(ts: string | number | undefined): string {
  if (ts == null || ts === '') return ''
  if (typeof ts === 'number') {
    const d = new Date(ts)
    return Number.isNaN(d.getTime()) ? String(ts) : d.toLocaleString('zh-CN')
  }
  const s = String(ts).trim()
  if (/^\d+$/.test(s)) {
    const n = Number(s)
    const ms = n > 1e11 ? n : n * 1000
    const d = new Date(ms)
    return Number.isNaN(d.getTime()) ? s : d.toLocaleString('zh-CN')
  }
  const d2 = new Date(s)
  return Number.isNaN(d2.getTime()) ? s : d2.toLocaleString('zh-CN')
}

function deviceLinkStatusText(s: string | undefined): string {
  const u = String(s || '').trim().toUpperCase()
  const m: Record<string, string> = {
    ONLINE: '在线',
    LOGIN_FAILED: '登录失败',
    ARM_FAILED: '布防失败',
  }
  return m[u] || (s || '').trim() || '—'
}

const nestedSubgroupColumns = [
  { title: '统计组ID', dataIndex: 'groupId', ellipsis: true, tooltip: true },
  { title: '名称', dataIndex: 'groupName', ellipsis: true, tooltip: true },
  { title: '进入', dataIndex: 'enter', width: 80 },
  { title: '离开', dataIndex: 'exit', width: 80 },
  { title: '经过', dataIndex: 'passing', width: 80 },
]
</script>

<template>
  <a-card title="客流监控 / 海康客流接口">
    <a-tabs v-model:active-key="activeTab" type="rounded">
      <a-tab-pane key="realtime" title="实时汇总">
        <a-space wrap style="margin-bottom: 16px">
          <span style="color: var(--color-text-2)">统计起始日</span>
          <a-date-picker v-model="peopleCountingDate" value-format="YYYY-MM-DD" style="width: 160px" />
          <span style="color: var(--color-text-2)">统计结束日</span>
          <a-date-picker
            v-model="peopleCountingEndDate"
            value-format="YYYY-MM-DD"
            allow-clear
            style="width: 160px"
            placeholder="可选，闭区间结束日"
          />
          <span class="muted">留空则仅查询起始日单日（不传 end_date）</span>
          <a-button type="primary" :loading="loadingPc" @click="refreshPeopleCounting(true)">拉取客流</a-button>
          <a-button :loading="loadingHealth" @click="refreshHealth">健康检查</a-button>
          <a-button @click="refreshAll">全部刷新</a-button>
          <span v-if="lastFetchAt" class="muted">上次拉取：{{ lastFetchAt }}</span>
        </a-space>

        <a-card v-if="healthData || healthError" title="服务健康状态" style="margin-bottom: 16px" size="small">
          <template v-if="healthData">
            <a-space direction="vertical" fill>
              <a-space wrap>
                <a-tag :color="healthStatusColor(healthData.status)">{{
                  healthServiceStatusText(healthData.status)
                }}</a-tag>
                <span class="muted">配置设备路数：{{ healthData.nvrConfigured ?? '—' }}</span>
                <span class="muted">已成功登录路数：{{ healthData.nvrLoggedIn ?? '—' }}</span>
                <span class="muted">设备数量：{{ healthData.deviceCount ?? '—' }}</span>
                <span class="muted">统计组条目数：{{ healthData.groupCount ?? '—' }}</span>
                <span class="muted" v-if="healthData.timestamp">{{
                  `返回时间：${formatServiceTimestamp(healthData.timestamp)}`
                }}</span>
              </a-space>
              <a-typography-text v-if="healthData.hint" type="warning">{{ healthData.hint }}</a-typography-text>
            </a-space>
          </template>
          <a-typography-text v-else type="danger">{{ healthError }}</a-typography-text>
        </a-card>

        <a-card v-if="peopleData && peopleData.code === 200" title="当日客流汇总" style="margin-bottom: 16px" size="small">
          <a-row :gutter="16">
            <a-col :span="6">
              <div class="sum-label">数据日期</div>
              <div class="sum-value">{{ peopleData.date ?? '—' }}</div>
            </a-col>
            <a-col :span="6">
              <a-statistic title="进入（合计）" :value="peopleData.total.enter" />
            </a-col>
            <a-col :span="6">
              <a-statistic title="离开（合计）" :value="peopleData.total.exit" />
            </a-col>
            <a-col :span="6">
              <a-statistic title="经过（合计）" :value="peopleData.total.passing" />
            </a-col>
          </a-row>
          <div style="margin-top: 8px; color: var(--color-text-3); font-size: 12px">
            累计接收数据条数：{{ peopleData.total.dataCount }}
          </div>
          <div
            v-if="peopleData.rangeFrom || peopleData.rangeTo"
            style="margin-top: 8px; color: var(--color-text-3); font-size: 12px"
          >
            查询区间：{{ peopleData.rangeFrom ?? '—' }} ～ {{ peopleData.rangeTo ?? '—' }}（快照日为「数据日期」，区间内累计见下方表格）
          </div>
        </a-card>

        <a-card
          v-if="peopleData && peopleData.code === 200 && peopleRangeSectionVisible"
          title="区间内各馆累计人数"
          style="margin-bottom: 16px"
          size="small"
        >
          <a-typography-text v-if="peopleData.venuesRangeTotalsNote" type="warning" style="display: block; margin-bottom: 8px">
            {{ peopleData.venuesRangeTotalsNote }}
          </a-typography-text>
          <div v-if="missingRangeDatesText()" class="muted" style="margin-bottom: 10px; font-size: 13px">
            区间内缺失归档的日期：{{ missingRangeDatesText() }}
          </div>
          <a-table
            v-if="venueRangeTotalsRows().length"
            row-key="venueId"
            :columns="venueRangeTotalsColumns"
            :data="venueRangeTotalsRows()"
            :pagination="false"
            size="small"
            :scroll="{ x: 800, y: 200 }"
            :summary="venueRangeTotalsSummary"
          />
          <div v-else class="muted" style="font-size: 13px">暂无区间内各馆累计数据</div>
        </a-card>

        <a-empty v-if="peopleData === null && !loadingPc" description="暂无数据，请先拉取客流" />

        <a-alert
          v-else-if="peopleData && peopleData.code !== 200"
          type="warning"
          style="margin-bottom: 16px"
        >
          接口返回非 200：{{ peopleData.message || peopleData.code }}
        </a-alert>

        <a-card title="各场馆当日人数统计" style="margin-bottom: 16px" size="small">
          <a-table
            row-key="venueId"
            :columns="venueColumns"
            :data="venues"
            :pagination="false"
            size="small"
            :scroll="{ x: 1300, y: 280 }"
            :summary="venuesDailySummary"
          >
            <template #net="{ record }">{{ netInVenue(record) }}</template>
            <template #venueLastUpdate="{ record }">{{ formatZhYmdHms(record.lastUpdate) }}</template>
            <template #included="{ record }">{{ formatIncludedGroups(record.includedGroups) }}</template>
            <template #nested="{ record }">
              <a-button
                v-if="(record.groups?.length ?? 0) > 0"
                type="text"
                size="mini"
                @click="openVenueNestedModal(record)"
              >
                {{ record.groups!.length }} 组
              </a-button>
              <span v-else>—</span>
            </template>
            <template #summary-cell="{ column, record }">
              <template v-if="column.dataIndex === 'venueId'">合计</template>
              <template v-else-if="column.dataIndex === 'venueName'">—</template>
              <template v-else-if="column.dataIndex === 'enter'">{{ record.enter }}</template>
              <template v-else-if="column.dataIndex === 'exit'">{{ record.exit }}</template>
              <template v-else-if="column.dataIndex === 'passing'">{{ record.passing }}</template>
              <template v-else-if="column.slotName === 'net'">{{ netInVenue(record) }}</template>
              <template v-else-if="column.dataIndex === 'updateCount'">{{ record.updateCount }}</template>
              <template v-else-if="column.slotName === 'venueLastUpdate'">—</template>
              <template v-else-if="column.slotName === 'included'">—</template>
              <template v-else-if="column.slotName === 'nested'">—</template>
              <template v-else>—</template>
            </template>
          </a-table>
          <div v-if="venues.length === 0" style="margin-top: 8px; color: var(--color-text-3); font-size: 12px">
            暂无场馆数据时，请检查服务端场馆与统计组映射是否已正确配置。
          </div>
        </a-card>
      </a-tab-pane>

      <a-tab-pane key="hourly" title="场馆按小时">
        <a-space wrap style="margin-bottom: 12px">
          <a-date-picker v-model="hourlyDate" value-format="YYYY-MM-DD" style="width: 160px" />
          <a-select
            v-model="hourlyVenueId"
            placeholder="可选：搜索并筛选单个场馆"
            allow-clear
            allow-search
            :filter-option="filterHourlyVenueOption"
            style="width: 280px"
            :options="hourlyVenueFilterOptions"
          />
          <a-button type="primary" :loading="loadingHourly" @click="queryHourly">按小时查询</a-button>
        </a-space>
        <a-typography-text type="secondary" style="display: block; margin-bottom: 12px">
          须选择查询日期；可按需指定场馆。数据来自服务内存按小时汇总，过早历史日期可能均为 0。
        </a-typography-text>

        <template v-if="hourlyData?.venues?.length">
          <div v-for="vr in hourlyData.venues || []" :key="vr.venueId" style="margin-bottom: 16px">
            <a-card size="small" :title="(vr.venueName || vr.venueId) + `（${vr.venueId}）`">
              <a-table
                :columns="hourlyDetailColumns"
                :data="hourlyRowsForVenue(vr)"
                row-key="key"
                :pagination="false"
                size="small"
              />
            </a-card>
          </div>
        </template>
        <a-empty v-else-if="!loadingHourly" description="请选择日期后查询" />
      </a-tab-pane>

      <a-tab-pane key="devices" title="设备链路 / 重试">
        <a-space wrap style="margin-bottom: 12px">
          <a-switch v-model="deviceProbe">
            <template #checked>含网络探测（较慢）</template>
            <template #unchecked>仅快照</template>
          </a-switch>
          <a-button :loading="loadingDevices" @click="loadDeviceLinks">加载设备链路</a-button>
        </a-space>
        <a-typography-text type="secondary" style="display: block; margin-bottom: 12px">
          开启网络探测会向各设备端口发起连通性检测，耗时更长；服务端可关闭此项（将返回无权访问）。
        </a-typography-text>

        <a-card v-if="deviceLinksData" title="设备列表" size="small" style="margin-bottom: 16px">
          <div v-if="deviceLinksData.probedAt" class="muted" style="margin-bottom: 8px">
            探测完成时间：{{ formatServiceTimestamp(deviceLinksData.probedAt) }}
          </div>
          <div v-if="deviceLinksData.probeDetail" class="muted" style="margin-bottom: 8px">
            {{ deviceLinksData.probeDetail }}
          </div>
          <a-table
            row-key="__k"
            :columns="deviceColumns"
            :data="
              (deviceLinksData.devices || []).map((d, i) => ({
                ...d,
                __k: `${d.deviceSlot}-${d.ip}-${i}`,
              }))
            "
            :pagination="false"
            size="small"
            :scroll="{ x: 1000, y: 220 }"
          >
            <template #devStatus="{ record }">{{ deviceLinkStatusText(record.status) }}</template>
            <template #probe="{ record }">{{ probeSummary(record) }}</template>
          </a-table>
        </a-card>

        <a-card title="设备重试登录" size="small">
          <a-space direction="vertical" fill>
            <a-select
              v-model="selectedRetrySlots"
              :options="retryOptions()"
              multiple
              placeholder="默认为失败设备；可选场馆名称与 IP 对应项"
              allow-clear
              style="width: 100%; max-width: 720px"
            />
            <a-space wrap>
              <a-button type="primary" status="danger" :loading="retrySubmitting" @click="confirmRetryLogin">
                发起重试登录
              </a-button>
              <span class="muted">向服务下发重试指令，不勾选从磁盘重新加载配置（如有需要可由运维在服务端操作）</span>
            </a-space>
          </a-space>
        </a-card>
      </a-tab-pane>
    </a-tabs>

    <a-modal v-model:visible="nestedModalVisible" title="馆内分组明细" :footer="false" width="800px">
      <template v-if="nestedModalVenue">
        <a-typography-text type="secondary" style="margin-bottom: 8px; display: block">
          {{ nestedModalVenue.venueName }} / {{ nestedModalVenue.venueId }}
        </a-typography-text>
        <a-table
          row-key="groupId"
          :columns="nestedSubgroupColumns"
          :data="(nestedModalVenue.groups || []) as HikPeopleCountingGroup[]"
          :pagination="false"
          size="small"
        />
      </template>
    </a-modal>
  </a-card>
</template>

<style scoped>
.muted {
  color: var(--color-text-3);
  font-size: 13px;
}

code {
  padding: 0 4px;
  background: var(--color-fill-2);
  border-radius: 2px;
  font-size: 12px;
}

.sum-label {
  color: var(--color-text-2);
  font-size: 12px;
  margin-bottom: 4px;
}

.sum-value {
  font-size: 20px;
  font-weight: 500;
}

</style>
