<!--
  管理端 - 会议管理列表（Phase B）

  设计系统：与 message/list.vue 一致的 Data-Dense Dashboard 风格

  功能：
  - 顶部统计卡片（历史会议总数 / 进行中 / 今日新建 / 录制总数）
  - 多条件筛选：关键词 / 状态 / 仅含录制 / 时间范围
  - 数据表格：会议号 / 标题 / 主持人 / 状态 / 参与人数 / 录制数 / 时长 / 创建时间
  - 详情弹窗：基础信息 + 参与者列表 + 录制列表（含播放链接 / 失败原因）

  对应后端 API（管理端）：
  - GET /api/v1/admin/meetings
  - GET /api/v1/admin/meetings/:id
  - GET /api/v1/admin/meetings/stats
-->
<template>
  <div class="meeting-list-page">
    <div class="page-header">
      <h2 class="page-title">会议管理</h2>
    </div>

    <!-- 顶部统计卡片 -->
    <div class="stat-cards">
      <el-card class="stat-card" shadow="never">
        <div class="stat-label">会议总数</div>
        <div class="stat-value">{{ stats.total_count }}</div>
      </el-card>
      <el-card class="stat-card stat-card--accent" shadow="never">
        <div class="stat-label">进行中</div>
        <div class="stat-value">{{ stats.active_count }}</div>
      </el-card>
      <el-card class="stat-card" shadow="never">
        <div class="stat-label">今日新建</div>
        <div class="stat-value">{{ stats.today_count }}</div>
      </el-card>
      <el-card class="stat-card stat-card--rec" shadow="never">
        <div class="stat-label">录制总数</div>
        <div class="stat-value">{{ stats.recording_count }}</div>
      </el-card>
    </div>

    <!-- 筛选区 -->
    <el-card class="filter-card" shadow="never">
      <el-form :inline="true" class="filter-form" @submit.prevent="handleSearch">
        <el-form-item label="关键词">
          <el-input
            v-model="filters.keyword"
            placeholder="会议号 / 标题"
            clearable
            style="width: 220px"
            @clear="handleSearch"
          >
            <template #prefix>
              <el-icon><Search /></el-icon>
            </template>
          </el-input>
        </el-form-item>

        <el-form-item label="状态">
          <el-select v-model="filters.status" placeholder="全部" clearable style="width: 120px">
            <el-option label="未开始" :value="0" />
            <el-option label="进行中" :value="1" />
            <el-option label="已结束" :value="2" />
          </el-select>
        </el-form-item>

        <el-form-item label="主持人ID">
          <el-input
            v-model="filters.host_id"
            placeholder="用户ID"
            clearable
            style="width: 120px"
            type="number"
          />
        </el-form-item>

        <el-form-item label="仅含录制">
          <el-switch v-model="filters.has_recording" />
        </el-form-item>

        <el-form-item label="创建时间">
          <el-date-picker
            v-model="dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="YYYY-MM-DD"
            style="width: 260px"
            @change="onDateRangeChange"
          />
        </el-form-item>

        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 数据表格 -->
    <el-card class="table-card" shadow="never">
      <el-table
        v-loading="loading"
        :data="meetingList"
        stripe
        border
        style="width: 100%"
        row-class-name="table-row"
      >
        <el-table-column prop="id" label="ID" width="70" align="center" />

        <el-table-column label="会议号" width="140">
          <template #default="{ row }">
            <span class="room-code">{{ row.room_code }}</span>
          </template>
        </el-table-column>

        <el-table-column label="标题 / 类型" min-width="200">
          <template #default="{ row }">
            <div class="title-cell">
              <span class="meeting-title">{{ row.title || '(未命名会议)' }}</span>
              <el-tag size="small" type="info" effect="plain">{{ row.type_label }}</el-tag>
            </div>
          </template>
        </el-table-column>

        <el-table-column label="主持人" min-width="160">
          <template #default="{ row }">
            <div class="host-cell">
              <div
                class="host-avatar"
                :style="{ backgroundColor: getAvatarColor(row.host_nickname) }"
              >
                <img v-if="row.host_avatar" :src="row.host_avatar" class="host-avatar-img" />
                <span v-else>{{ (row.host_nickname || '?')[0] }}</span>
              </div>
              <div class="host-info">
                <span class="host-name">{{ row.host_nickname || '-' }}</span>
                <span class="host-id">ID: {{ row.host_id }}</span>
              </div>
            </div>
          </template>
        </el-table-column>

        <el-table-column label="状态" width="100" align="center">
          <template #default="{ row }">
            <el-tag :type="statusTagMap[row.status]?.type || 'info'" size="small">
              {{ row.status_label }}
            </el-tag>
          </template>
        </el-table-column>

        <el-table-column label="参与人数" width="100" align="center">
          <template #default="{ row }">
            <span class="num-cell">{{ row.participant_count }}</span>
            <span class="num-suffix">/ {{ row.max_members }}</span>
          </template>
        </el-table-column>

        <el-table-column label="录制" width="100" align="center">
          <template #default="{ row }">
            <el-tag
              v-if="row.recording_count > 0"
              type="warning"
              size="small"
              effect="light"
            >
              {{ row.recording_count }} 段
            </el-tag>
            <span v-else class="text-muted">—</span>
          </template>
        </el-table-column>

        <el-table-column label="时长" width="110" align="center">
          <template #default="{ row }">
            <span>{{ formatDuration(row.duration_sec) }}</span>
          </template>
        </el-table-column>

        <el-table-column prop="created_at" label="创建时间" width="170" align="center" />

        <el-table-column label="操作" width="100" align="center" fixed="right">
          <template #default="{ row }">
            <el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
          </template>
        </el-table-column>
      </el-table>

      <div class="pagination-wrapper">
        <el-pagination
          v-model:current-page="currentPage"
          v-model:page-size="pageSize"
          :page-sizes="[10, 20, 50]"
          :total="total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="fetchMeetingList"
          @current-change="fetchMeetingList"
        />
      </div>
    </el-card>

    <!-- 会议详情弹窗 -->
    <el-dialog
      v-model="detailVisible"
      title="会议详情"
      width="780px"
      destroy-on-close
      class="detail-dialog"
      @close="onDetailDialogClose"
    >
      <div v-loading="detailLoading" class="detail-content">
        <template v-if="currentMeeting">
          <!-- 会议头部 -->
          <div class="detail-header">
            <div class="detail-header-main">
              <div class="detail-room-code">{{ currentMeeting.room_code }}</div>
              <div class="detail-title">{{ currentMeeting.title }}</div>
              <div class="detail-meta">
                <el-tag size="small" :type="statusTagMap[currentMeeting.status]?.type || 'info'">
                  {{ currentMeeting.status_label }}
                </el-tag>
                <el-tag size="small" type="info" effect="plain">{{ currentMeeting.type_label }}</el-tag>
                <span class="meta-divider">·</span>
                <span class="meta-item">主持人 {{ currentMeeting.host_nickname || currentMeeting.host_id }}</span>
                <span class="meta-divider">·</span>
                <span class="meta-item">时长 {{ formatDuration(currentMeeting.duration_sec) }}</span>
              </div>
            </div>
          </div>

          <!-- 元数据栅格 -->
          <div class="detail-grid">
            <div class="detail-grid-item">
              <span class="grid-label">参与人数</span>
              <span class="grid-value">
                {{ currentMeeting.participant_count }} / {{ currentMeeting.max_members }}
              </span>
            </div>
            <div class="detail-grid-item">
              <span class="grid-label">录制段数</span>
              <span class="grid-value">{{ currentMeeting.recording_count }}</span>
            </div>
            <div class="detail-grid-item">
              <span class="grid-label">开始时间</span>
              <span class="grid-value">{{ currentMeeting.started_at || '—' }}</span>
            </div>
            <div class="detail-grid-item">
              <span class="grid-label">结束时间</span>
              <span class="grid-value">
                {{ currentMeeting.ended_at || '—' }}
                <el-tag
                  v-if="currentMeeting.ended_reason"
                  size="small"
                  effect="plain"
                  type="info"
                  style="margin-left: 6px"
                >
                  {{ endedReasonLabel(currentMeeting.ended_reason) }}
                </el-tag>
              </span>
            </div>
          </div>

          <!-- 参与者 -->
          <div class="detail-section">
            <div class="section-header">
              <span>参与者</span>
              <span class="section-count">{{ currentMeeting.participants?.length || 0 }}</span>
            </div>
            <el-table
              :data="currentMeeting.participants || []"
              size="small"
              border
              style="width: 100%"
            >
              <el-table-column label="用户" min-width="180">
                <template #default="{ row }">
                  <div class="participant-cell">
                    <div
                      class="participant-avatar"
                      :style="{ backgroundColor: getAvatarColor(row.nickname) }"
                    >
                      <img v-if="row.avatar" :src="row.avatar" class="participant-avatar-img" />
                      <span v-else>{{ (row.nickname || '?')[0] }}</span>
                    </div>
                    <div class="participant-info">
                      <span class="participant-name">{{ row.nickname || '-' }}</span>
                      <span class="participant-id">ID: {{ row.user_id }}</span>
                    </div>
                  </div>
                </template>
              </el-table-column>
              <el-table-column label="角色" width="100" align="center">
                <template #default="{ row }">
                  <el-tag
                    :type="row.role === 1 ? 'danger' : row.role === 2 ? 'warning' : 'info'"
                    size="small"
                    effect="light"
                  >
                    {{ row.role_label }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column prop="joined_at" label="加入时间" width="170" align="center" />
              <el-table-column label="离开时间" width="170" align="center">
                <template #default="{ row }">
                  <span v-if="row.left_at">{{ row.left_at }}</span>
                  <el-tag v-else type="success" size="small" effect="plain">在线</el-tag>
                </template>
              </el-table-column>
              <el-table-column label="离会原因" width="110" align="center">
                <template #default="{ row }">
                  {{ row.left_reason ? leftReasonLabel(row.left_reason) : '—' }}
                </template>
              </el-table-column>
              <el-table-column label="参会时长" width="100" align="center">
                <template #default="{ row }">{{ formatDuration(row.duration) }}</template>
              </el-table-column>
            </el-table>
          </div>

          <!-- 录制列表 -->
          <div class="detail-section">
            <div class="section-header">
              <span>录制列表</span>
              <span class="section-count">{{ currentMeeting.recordings?.length || 0 }}</span>
            </div>
            <el-empty
              v-if="!currentMeeting.recordings || currentMeeting.recordings.length === 0"
              description="无录制记录"
              :image-size="60"
            />
            <el-table
              v-else
              :data="currentMeeting.recordings"
              size="small"
              border
              style="width: 100%"
            >
              <el-table-column prop="id" label="ID" width="70" align="center" />
              <el-table-column label="发起人" width="100" align="center">
                <template #default="{ row }">ID: {{ row.started_by }}</template>
              </el-table-column>
              <el-table-column label="状态" width="100" align="center">
                <template #default="{ row }">
                  <el-tag :type="recordingStatusTagMap[row.status]?.type || 'info'" size="small">
                    {{ recordingStatusTagMap[row.status]?.label || row.status }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="时长" width="100" align="center">
                <template #default="{ row }">{{ formatDuration(row.duration_sec) }}</template>
              </el-table-column>
              <el-table-column label="文件大小" width="110" align="center">
                <template #default="{ row }">{{ formatBytes(row.size_bytes) }}</template>
              </el-table-column>
              <el-table-column prop="started_at" label="开始时间" width="170" align="center" />
              <el-table-column label="文件" min-width="120" align="center">
                <template #default="{ row }">
                  <a
                    v-if="row.status === 'ready' && row.file_url"
                    :href="row.file_url"
                    target="_blank"
                    class="file-link"
                  >播放 / 下载</a>
                  <span v-else-if="row.status === 'failed'" class="failure-text">
                    {{ row.failure_reason || '失败' }}
                  </span>
                  <!-- recording 兜底：host 失联/未点停止时由 admin 强制结束 -->
                  <el-button
                    v-else-if="row.status === 'recording'"
                    type="danger"
                    link
                    size="small"
                    :loading="forceStopMap[row.id]"
                    @click="handleForceStopRecording(row)"
                  >强制结束</el-button>
                  <span v-else class="text-muted">—</span>
                </template>
              </el-table-column>
              <el-table-column label="转写" width="170" align="center">
                <template #default="{ row }">
                  <!-- 仅 ready 状态的录制可触发转写 -->
                  <template v-if="row.status === 'ready'">
                    <el-tag
                      v-if="transcriptMap[row.id]?.status === 'ready'"
                      type="success"
                      size="small"
                      effect="plain"
                      class="transcript-tag"
                      @click="viewTranscript(row)"
                    >已就绪 · 查看</el-tag>
                    <el-tag
                      v-else-if="transcriptMap[row.id]?.status === 'running' || transcriptMap[row.id]?.status === 'pending'"
                      type="warning"
                      size="small"
                      effect="plain"
                    >
                      <el-icon class="is-loading" style="margin-right:4px"><Loading /></el-icon>
                      转写中
                    </el-tag>
                    <el-tag
                      v-else-if="transcriptMap[row.id]?.status === 'failed'"
                      type="danger"
                      size="small"
                      effect="plain"
                      @click="viewTranscript(row)"
                    >失败 · 查看</el-tag>
                    <el-button
                      v-else
                      type="primary"
                      link
                      size="small"
                      :loading="transcribingMap[row.id]"
                      @click="handleTranscribe(row, false)"
                    >开始转写</el-button>
                    <el-button
                      v-if="transcriptMap[row.id]?.status === 'ready' || transcriptMap[row.id]?.status === 'failed'"
                      type="warning"
                      link
                      size="small"
                      :loading="transcribingMap[row.id]"
                      @click="handleTranscribe(row, true)"
                    >重转</el-button>
                  </template>
                  <span v-else class="text-muted">—</span>
                </template>
              </el-table-column>
            </el-table>
          </div>
        </template>
      </div>
    </el-dialog>

    <!-- 转写文字稿弹窗 -->
    <el-dialog
      v-model="transcriptVisible"
      :title="transcriptDialogTitle"
      width="720px"
      destroy-on-close
      class="transcript-dialog"
    >
      <div v-if="currentTranscript" class="transcript-content">
        <div class="transcript-meta">
          <el-tag :type="transcriptStatusTag(currentTranscript.status)" size="small">
            {{ currentTranscript.status_label }}
          </el-tag>
          <span v-if="currentTranscript.provider_code" class="meta-pill">
            提供商：{{ currentTranscript.provider_code }}
          </span>
          <span v-if="currentTranscript.model_code" class="meta-pill">
            模型：{{ currentTranscript.model_code }}
          </span>
          <span v-if="currentTranscript.language" class="meta-pill">
            语言：{{ currentTranscript.language }}
          </span>
          <span v-if="currentTranscript.duration_sec" class="meta-pill">
            音频时长：{{ formatDuration(currentTranscript.duration_sec) }}
          </span>
        </div>

        <div v-if="currentTranscript.status === 'failed'" class="transcript-error">
          <el-icon><CircleClose /></el-icon>
          <span>{{ currentTranscript.error_msg || '转写失败' }}</span>
        </div>

        <div
          v-else-if="currentTranscript.segments && currentTranscript.segments.length > 0"
          class="transcript-segments"
        >
          <div
            v-for="(seg, idx) in currentTranscript.segments"
            :key="idx"
            class="segment-row"
          >
            <span class="segment-time">{{ formatSegTime(seg.start) }}</span>
            <span class="segment-text">{{ seg.text }}</span>
          </div>
        </div>

        <div v-else-if="currentTranscript.text" class="transcript-text">
          {{ currentTranscript.text }}
        </div>

        <el-empty v-else description="暂无文字稿内容" :image-size="60" />
      </div>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { Search, Loading, CircleClose } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getMeetingList, getMeetingDetail, getMeetingStats, forceStopRecording } from '@/api/meeting'
import { getTranscript, submitTranscribe } from '@/api/transcribe'

const AVATAR_COLORS = ['#2563EB', '#7C3AED', '#059669', '#D97706', '#DC2626', '#0891B2', '#4F46E5', '#BE185D']

const statusTagMap = {
  0: { label: '未开始', type: 'info' },
  1: { label: '进行中', type: 'success' },
  2: { label: '已结束', type: 'warning' }
}

const recordingStatusTagMap = {
  recording: { label: '录制中', type: 'success' },
  uploading: { label: '上传中', type: 'warning' },
  ready: { label: '已就绪', type: 'success' },
  failed: { label: '失败', type: 'danger' }
}

const endedReasonLabels = {
  host_ended: '主持人结束',
  empty_ttl: '空房超时',
  admin_force: '管理员强制',
  system_error: '系统异常'
}
const leftReasonLabels = {
  self: '主动离开',
  kicked: '被移除',
  host_end: '会议结束',
  empty_ttl: '空房超时',
  disconnect: '掉线'
}

const endedReasonLabel = (r) => endedReasonLabels[r] || r
const leftReasonLabel = (r) => leftReasonLabels[r] || r

const loading = ref(false)
const meetingList = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const dateRange = ref(null)
const stats = reactive({
  total_count: 0,
  active_count: 0,
  today_count: 0,
  recording_count: 0
})

const filters = reactive({
  keyword: '',
  status: null,
  host_id: null,
  has_recording: false
})

const detailVisible = ref(false)
const detailLoading = ref(false)
const currentMeeting = ref(null)

const getAvatarColor = (name) => {
  if (!name) return '#94A3B8'
  return AVATAR_COLORS[name.charCodeAt(0) % AVATAR_COLORS.length]
}

const formatDuration = (sec) => {
  if (!sec || sec <= 0) return '—'
  const h = Math.floor(sec / 3600)
  const m = Math.floor((sec % 3600) / 60)
  const s = sec % 60
  if (h > 0) return `${h}h ${m}m`
  if (m > 0) return `${m}m ${s}s`
  return `${s}s`
}

const formatBytes = (b) => {
  if (!b || b <= 0) return '—'
  const units = ['B', 'KB', 'MB', 'GB']
  let n = b
  let i = 0
  while (n >= 1024 && i < units.length - 1) {
    n /= 1024
    i++
  }
  return `${n.toFixed(n >= 100 ? 0 : 1)} ${units[i]}`
}

const onDateRangeChange = (val) => {
  if (!val) {
    filters.start_time = undefined
    filters.end_time = undefined
  }
}

const fetchMeetingList = async () => {
  loading.value = true
  try {
    const params = {
      page: currentPage.value,
      page_size: pageSize.value
    }
    if (filters.keyword) params.keyword = filters.keyword
    if (filters.status != null) params.status = filters.status
    if (filters.host_id) params.host_id = Number(filters.host_id)
    if (filters.has_recording) params.has_recording = true
    if (dateRange.value && dateRange.value.length === 2) {
      params.start_time = dateRange.value[0]
      params.end_time = dateRange.value[1]
    }
    const res = await getMeetingList(params)
    meetingList.value = res.data?.list || []
    total.value = res.data?.total || 0
  } catch (e) {
    ElMessage.error(e?.message || '获取会议列表失败')
  } finally {
    loading.value = false
  }
}

const fetchStats = async () => {
  try {
    const res = await getMeetingStats()
    Object.assign(stats, res.data || {})
  } catch (e) {
    // 统计失败不阻塞列表
    console.warn('[meeting] stats load failed', e)
  }
}

const handleSearch = () => {
  currentPage.value = 1
  fetchMeetingList()
}

const handleReset = () => {
  filters.keyword = ''
  filters.status = null
  filters.host_id = null
  filters.has_recording = false
  dateRange.value = null
  currentPage.value = 1
  fetchMeetingList()
}

const viewDetail = async (row) => {
  detailVisible.value = true
  detailLoading.value = true
  currentMeeting.value = null
  // 切换会议时清空旧的转写状态 + 停止旧轮询，避免上一场会议的状态泄漏
  resetTranscriptState()
  try {
    const res = await getMeetingDetail(row.id)
    currentMeeting.value = res.data
    // 详情拉到后，对每段已 ready 的录制并发拉一次转写状态
    loadAllTranscripts()
  } catch (e) {
    ElMessage.error(e?.message || '获取会议详情失败')
  } finally {
    detailLoading.value = false
  }
}

// ============== 转写：状态、API、轮询 ==============

// recording.id → transcript DTO（含 status/text/segments/...）
const transcriptMap = reactive({})
// recording.id → 是否正在调用 submit（按钮 loading 用）
const transcribingMap = reactive({})
// recording.id → 是否正在调用强制结束（按钮 loading 用）
const forceStopMap = reactive({})
// 当前展开的转写文字稿（用于弹窗）
const transcriptVisible = ref(false)
const currentTranscript = ref(null)
// 轮询定时器：recording.id → setInterval handle
const pollTimers = {}

const transcriptDialogTitle = computed(() => {
  if (!currentTranscript.value) return '转写文字稿'
  return `转写文字稿 - 录制 #${currentTranscript.value.recording_id}`
})

const transcriptStatusTag = (s) => {
  switch (s) {
    case 'ready': return 'success'
    case 'running':
    case 'pending': return 'warning'
    case 'failed': return 'danger'
    default: return 'info'
  }
}

// 0.0 → "00:00"，秒级；STT segment 起止以浮点秒
const formatSegTime = (sec) => {
  if (sec == null) return '00:00'
  const total = Math.floor(sec)
  const m = Math.floor(total / 60)
  const s = total % 60
  return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}

// 拉取一段录制的转写状态；recording 未触发过转写时后端返回 null
const loadOneTranscript = async (recordingId) => {
  try {
    const res = await getTranscript(recordingId)
    if (res?.data) {
      transcriptMap[recordingId] = res.data
      // 终态 → 停止该 id 的轮询
      if (res.data.status === 'ready' || res.data.status === 'failed') {
        stopPolling(recordingId)
      }
    } else {
      // 主动写一个 null，便于模板 v-if 判定为"尚未发起"
      delete transcriptMap[recordingId]
    }
  } catch (e) {
    // 静默：转写查询失败不影响列表展示
    console.warn('[transcribe] 查询转写状态失败', recordingId, e)
  }
}

const loadAllTranscripts = () => {
  const recordings = currentMeeting.value?.recordings || []
  recordings.forEach((r) => {
    if (r.status === 'ready') {
      loadOneTranscript(r.id)
    }
  })
}

// 启动轮询：每 3 秒查一次，直到 ready/failed 或弹窗关闭
const startPolling = (recordingId) => {
  stopPolling(recordingId)
  pollTimers[recordingId] = setInterval(() => {
    loadOneTranscript(recordingId)
  }, 3000)
}

const stopPolling = (recordingId) => {
  if (pollTimers[recordingId]) {
    clearInterval(pollTimers[recordingId])
    delete pollTimers[recordingId]
  }
}

const stopAllPolling = () => {
  Object.keys(pollTimers).forEach((id) => stopPolling(id))
}

const resetTranscriptState = () => {
  stopAllPolling()
  Object.keys(transcriptMap).forEach((k) => delete transcriptMap[k])
  Object.keys(transcribingMap).forEach((k) => delete transcribingMap[k])
}

// 触发转写：force=true 表示已 ready 也强制重跑
const handleTranscribe = async (row, force) => {
  if (transcribingMap[row.id]) return
  transcribingMap[row.id] = true
  try {
    const res = await submitTranscribe(row.id, { force, language: '' })
    transcriptMap[row.id] = res.data
    if (res.data?.status === 'ready' || res.data?.status === 'failed') {
      // 极少见：同步就完成（短音频缓存命中）
      stopPolling(row.id)
    } else {
      ElMessage.success('转写任务已提交，正在处理中…')
      startPolling(row.id)
    }
  } catch (e) {
    // 503 → STT 未配置；400 → 参数 / 录制状态错误；其他 → 默认提示
    const status = e?.response?.status
    const msg = e?.response?.data?.message || e?.message || '提交转写失败'
    if (status === 503) {
      ElMessage.warning('语音转写服务未配置，请联系管理员检查 LLM 配置中心')
    } else {
      ElMessage.error(msg)
    }
  } finally {
    transcribingMap[row.id] = false
  }
}

// 强制结束指定录制（admin 兜底）
// 触发场景：host 失联未点停止 / ffmpeg 异常但 webhook 失效 / 长时间停留 recording 状态
// 后端走完整 stop 状态机：media-server stop → MinIO 上传 → DB ready/failed
const handleForceStopRecording = async (row) => {
  if (forceStopMap[row.id]) return
  try {
    await ElMessageBox.confirm(
      `确认强制结束录制 #${row.id}？将立即停止 ffmpeg 并尝试上传当前已写入的片段到 MinIO。`,
      '强制结束录制',
      { type: 'warning', confirmButtonText: '确认结束', cancelButtonText: '取消' }
    )
  } catch (_) {
    return // 用户取消
  }
  forceStopMap[row.id] = true
  try {
    await forceStopRecording(row.id)
    ElMessage.success('已发起强制结束，正在收尾上传…')
    // 后端 StopRecording 是同步上传 MinIO（Phase B1 取舍），返回时一般已是 ready/failed
    // 重新拉取详情即可看到终态
    if (currentMeeting.value?.id) {
      const res = await getMeetingDetail(currentMeeting.value.id)
      currentMeeting.value = res.data
    }
  } catch (e) {
    const msg = e?.response?.data?.message || e?.message || '强制结束失败'
    ElMessage.error(msg)
  } finally {
    forceStopMap[row.id] = false
  }
}

// 查看转写文字稿
const viewTranscript = (row) => {
  const t = transcriptMap[row.id]
  if (!t) return
  currentTranscript.value = t
  transcriptVisible.value = true
}

// 详情弹窗关闭时停掉所有轮询，防内存泄漏
const onDetailDialogClose = () => {
  stopAllPolling()
}

onMounted(() => {
  fetchMeetingList()
  fetchStats()
})

onBeforeUnmount(() => {
  stopAllPolling()
})
</script>

<style scoped>
.meeting-list-page { padding: 0; }

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.page-title { font-size: 20px; font-weight: 600; color: #1E293B; margin: 0; }

/* 统计卡片 */
.stat-cards {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  margin-bottom: 16px;
}
.stat-card { border: 1px solid #E2E8F0; }
.stat-card.stat-card--accent { border-color: #BFDBFE; background: linear-gradient(135deg, #EFF6FF 0%, #FFFFFF 100%); }
.stat-card.stat-card--rec { border-color: #FCD34D; background: linear-gradient(135deg, #FFFBEB 0%, #FFFFFF 100%); }
.stat-label { font-size: 12px; color: #64748B; text-transform: uppercase; letter-spacing: 0.04em; }
.stat-value { font-size: 26px; font-weight: 700; color: #0F172A; margin-top: 4px; }

.filter-card { margin-bottom: 16px; }
.filter-form { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.table-card { margin-bottom: 16px; }

.room-code {
  font-family: 'SF Mono', 'Menlo', monospace;
  font-weight: 600;
  color: #2563EB;
}
.title-cell { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.meeting-title { font-weight: 500; color: #1E293B; }

.num-cell { font-weight: 600; color: #0F172A; }
.num-suffix { font-size: 12px; color: #94A3B8; margin-left: 2px; }
.text-muted { color: #94A3B8; }

/* 主持人 */
.host-cell { display: flex; align-items: center; gap: 10px; }
.host-avatar {
  width: 32px; height: 32px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 600; color: #FFFFFF; flex-shrink: 0; overflow: hidden;
}
.host-avatar-img { width: 100%; height: 100%; object-fit: cover; }
.host-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.host-name { font-weight: 500; color: #1E293B; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.host-id { font-size: 11px; color: #94A3B8; }

.pagination-wrapper { display: flex; justify-content: flex-end; padding-top: 16px; }

/* ===== 详情弹窗 ===== */
.detail-content { min-height: 200px; }
.detail-header {
  padding: 16px 20px;
  background: linear-gradient(135deg, #F8FAFC 0%, #EFF6FF 100%);
  border-radius: 12px;
  margin-bottom: 20px;
}
.detail-room-code {
  font-family: 'SF Mono', 'Menlo', monospace;
  font-size: 13px;
  color: #2563EB;
  font-weight: 600;
  letter-spacing: 0.05em;
}
.detail-title { font-size: 18px; font-weight: 700; color: #0F172A; margin: 4px 0 8px; }
.detail-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 13px; color: #475569; }
.meta-divider { color: #CBD5E1; }
.meta-item { display: inline-flex; align-items: center; }

.detail-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 20px;
}
.detail-grid-item {
  display: flex; flex-direction: column; gap: 4px;
  padding: 12px 16px; background: #F8FAFC; border-radius: 8px; border: 1px solid #E2E8F0;
}
.grid-label { font-size: 12px; color: #94A3B8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.03em; }
.grid-value { font-size: 14px; color: #1E293B; font-weight: 500; display: inline-flex; align-items: center; }

.detail-section { margin-bottom: 20px; }
.section-header {
  display: flex; align-items: center; gap: 8px;
  font-size: 14px; font-weight: 600; color: #334155;
  margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid #E2E8F0;
}
.section-count { font-size: 12px; color: #94A3B8; font-weight: 500; }

/* 参与者头像 */
.participant-cell { display: flex; align-items: center; gap: 8px; }
.participant-avatar {
  width: 28px; height: 28px; border-radius: 6px;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 600; color: #FFFFFF; flex-shrink: 0; overflow: hidden;
}
.participant-avatar-img { width: 100%; height: 100%; object-fit: cover; }
.participant-info { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.participant-name { font-size: 12px; font-weight: 500; color: #1E293B; }
.participant-id { font-size: 10px; color: #94A3B8; }

.file-link { color: #2563EB; font-size: 13px; text-decoration: none; }
.file-link:hover { text-decoration: underline; }
.failure-text { color: #DC2626; font-size: 12px; }

/* 转写状态 tag 可点击 */
.transcript-tag { cursor: pointer; }
.transcript-tag:hover { opacity: 0.85; }

/* 转写文字稿弹窗 */
.transcript-content { padding: 4px 0 8px; }
.transcript-meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding: 12px 16px; background: #F8FAFC; border-radius: 8px; border: 1px solid #E2E8F0;
  margin-bottom: 14px;
}
.meta-pill {
  font-size: 12px; color: #475569; background: #FFFFFF;
  padding: 2px 10px; border-radius: 999px; border: 1px solid #E2E8F0;
}

.transcript-error {
  display: flex; align-items: center; gap: 8px;
  padding: 16px; background: #FEF2F2; border: 1px solid #FECACA;
  border-radius: 8px; color: #B91C1C; font-size: 13px;
}

.transcript-segments {
  max-height: 480px; overflow-y: auto; padding: 4px 6px 4px 0;
  border: 1px solid #E2E8F0; border-radius: 8px; background: #FFFFFF;
}
.segment-row {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 8px 14px; border-bottom: 1px dashed #F1F5F9;
}
.segment-row:last-child { border-bottom: none; }
.segment-time {
  flex-shrink: 0;
  font-family: 'SF Mono', 'Menlo', monospace;
  font-size: 12px; color: #64748B; padding-top: 2px; min-width: 48px;
}
.segment-text { font-size: 14px; color: #1E293B; line-height: 1.6; flex: 1; }

.transcript-text {
  padding: 16px; background: #FFFFFF; border: 1px solid #E2E8F0;
  border-radius: 8px; font-size: 14px; color: #1E293B; line-height: 1.7;
  white-space: pre-wrap; word-break: break-word;
  max-height: 480px; overflow-y: auto;
}
</style>
