<template>
  <div class="dubbing-page">
    <PageHeader title="绘本配音" subtitle="为每一页添加AI语音或人工配音" :showBack="true" />

    <div v-if="loading" class="loading-state">
      <div class="loading-emojis">
        <span class="loading-emoji e1">🎙️</span>
        <span class="loading-emoji e2">🎵</span>
        <span class="loading-emoji e3">✨</span>
      </div>
      <div class="loading-text">正在加载绘本...</div>
    </div>

    <template v-else>
      <div class="content">
        <!-- 当前页展示 -->
        <div class="page-display">
          <div class="page-image-wrap">
            <div class="page-badge-pill">{{ currentPage.pageNum === 0 ? '封面' : 'P' + currentPage.pageNum }}</div>
            <img v-if="currentPage.imageUrl" :src="currentPage.imageUrl" class="page-image" />
            <div v-else class="page-image placeholder">📖</div>
          </div>
          <div v-if="currentPage.text" class="page-text-card">
            <div class="text-quote-mark">"</div>
            <div class="page-text-content">{{ currentPage.text }}</div>
          </div>
        </div>

        <!-- 音频控制 -->
        <div class="audio-controls">
          <template v-if="currentAudioSrc">
            <div class="audio-row">
              <button class="play-btn" @click="togglePlay">
                <span v-if="isPlaying">⏸</span>
                <span v-else>▶️</span>
              </button>
              <div class="audio-progress">
                <div class="audio-bar">
                  <div class="audio-bar-fill" :style="{ width: playProgress + '%' }" />
                </div>
                <span class="audio-time">{{ formatTime(playCurrentTime) }} / {{ formatTime(playDuration) }}</span>
              </div>
              <div class="audio-source-tag" :class="currentPage.localBlob ? 'local' : currentPage.isAiVoice === false ? 'human' : 'ai'">
                {{ currentPage.localBlob ? '🎤 本地' : currentPage.isAiVoice === false ? '🎤 人工' : '🤖 AI' }}
              </div>
            </div>
          </template>
          <div v-else class="audio-empty">
            <span class="empty-icon">🔇</span>
            <span>暂未配音</span>
          </div>
        </div>

        <!-- 配音按钮 -->
        <div class="voice-actions">
          <button
            class="record-btn"
            :class="{ recording: isRecording }"
            :disabled="voicingSingle || voicingAll"
            @mousedown.prevent="startRecording"
            @mouseup.prevent="stopRecording"
            @mouseleave="isRecording && stopRecording()"
            @touchstart.prevent="startRecording"
            @touchend.prevent="stopRecording"
            @touchcancel="isRecording && stopRecording()"
          >
            <span class="record-icon">{{ isRecording ? '🔴' : '🎤' }}</span>
            <span class="record-text">{{ isRecording ? '录音中... 松开结束' : '按住录音' }}</span>
          </button>
          <div class="ai-btns">
            <button
              class="ai-btn single"
              :disabled="voicingSingle || voicingAll || isRecording"
              @click="voiceSingle"
            >
              <span>🤖</span>
              {{ voicingSingle ? '配音中...' : 'AI配音' }}
            </button>
            <button
              class="ai-btn all"
              :disabled="voicingSingle || voicingAll || isRecording"
              @click="voiceAllConfirm"
            >
              <span>✨</span>
              {{ voicingAll ? '配音中...' : '全部AI' }}
            </button>
          </div>
        </div>

        <!-- 页面缩略图横向列表 -->
        <div class="thumb-section">
          <div class="thumb-header">
            <span class="thumb-title">配音进度</span>
            <span class="thumb-count">{{ voicedCount }}/{{ pages.length }} 已完成</span>
          </div>
          <div class="thumb-strip">
            <div
              v-for="(p, i) in pages" :key="i"
              class="thumb-item" :class="{ active: i === idx, voiced: p.audioUrl || p.localBlob }"
              @click="switchPage(i)"
            >
              <img v-if="p.imageUrl" :src="p.imageUrl" class="thumb-img" />
              <div v-else class="thumb-placeholder">{{ p.pageNum === 0 ? '封' : p.pageNum }}</div>
              <div class="thumb-status">
                <span v-if="p.localBlob" class="status-dot local-dot" />
                <span v-else-if="p.audioUrl" class="status-dot ai-dot" />
                <span v-else class="status-dot empty-dot" />
              </div>
              <div v-if="p.audioUrl || p.localBlob" class="thumb-voice-icon">🔊</div>
            </div>
          </div>
        </div>
      </div>

      <!-- 底部 -->
      <div class="bottom-bar safe-bottom">
        <button class="btn-primary finish-btn" :disabled="submitting" @click="finish">
          {{ submitting ? '提交中...' : '完成配音 →' }}
        </button>
        <div v-if="localCount > 0" class="local-hint">🎤 {{ localCount }}页本地录音将在提交时上传</div>
      </div>
    </template>

    <!-- Toast -->
    <Transition name="fade">
      <div v-if="toast" class="toast">{{ toast }}</div>
    </Transition>

    <!-- 确认弹窗（替代原生 confirm） -->
    <div v-if="confirmVisible" class="confirm-overlay" @click.self="handleConfirmCancel">
      <div class="confirm-modal">
        <div class="confirm-title">{{ confirmTitle }}</div>
        <div class="confirm-content">{{ confirmContent }}</div>
        <div class="confirm-actions">
          <button class="confirm-btn cancel" @click="handleConfirmCancel">{{ confirmCancelText }}</button>
          <button class="confirm-btn ok" @click="handleConfirmOk">{{ confirmOkText }}</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import PageHeader from '@/components/PageHeader.vue'
import { getWorkDetail, voicePage, ossUpload, batchUpdateAudio, finishDubbing } from '@/api'
import { store } from '@/utils/store'
import { STATUS, getRouteByStatus } from '@/utils/status'

const router = useRouter()
const route = useRoute()
const workId = computed(() => route.params.workId || store.workId)

const loading = ref(true)
const submitting = ref(false)
const pages = ref([])
const idx = ref(0)
const toast = ref('')

// Voice state
const voicingSingle = ref(false)
const voicingAll = ref(false)

// Recording state
const isRecording = ref(false)
let mediaRecorder = null
let recordedChunks = []

// Audio playback state
let audioEl = null
const isPlaying = ref(false)
const playProgress = ref(0)
const playCurrentTime = ref(0)
const playDuration = ref(0)

const currentPage = computed(() => pages.value[idx.value] || {})

// 当前页的播放源：优先本地 blob，其次服务端 audioUrl
const currentAudioSrc = computed(() => {
  const p = currentPage.value
  if (p.localBlob) return URL.createObjectURL(p.localBlob)
  return p.audioUrl || null
})

const voicedCount = computed(() => pages.value.filter(p => p.audioUrl || p.localBlob).length)
const localCount = computed(() => pages.value.filter(p => p.localBlob).length)

function showToast(msg) {
  toast.value = msg
  setTimeout(() => { toast.value = '' }, 2500)
}

// ── 确认弹窗（替代原生 confirm） ──
const confirmVisible = ref(false)
const confirmTitle = ref('')
const confirmContent = ref('')
const confirmOkText = ref('确认')
const confirmCancelText = ref('取消')
let confirmResolve = null

/**
 * 显示确认弹窗（替代原生 confirm）
 * @returns {Promise<boolean>}
 */
function showConfirm(content, options = {}) {
  confirmTitle.value = options.title || '确认操作'
  confirmContent.value = content
  confirmOkText.value = options.okText || '确认'
  confirmCancelText.value = options.cancelText || '取消'
  confirmVisible.value = true
  return new Promise((resolve) => { confirmResolve = resolve })
}

function handleConfirmOk() {
  confirmVisible.value = false
  confirmResolve?.(true)
  confirmResolve = null
}

function handleConfirmCancel() {
  confirmVisible.value = false
  confirmResolve?.(false)
  confirmResolve = null
}

function formatTime(sec) {
  if (!sec || isNaN(sec)) return '0:00'
  const m = Math.floor(sec / 60)
  const s = Math.floor(sec % 60)
  return m + ':' + (s < 10 ? '0' : '') + s
}

// ─── Audio Playback ───

function stopAudio() {
  if (audioEl) {
    audioEl.pause()
    audioEl.onerror = null   // 先清回调，再清 src，防止空 src 触发 onerror
    audioEl.onended = null
    audioEl.ontimeupdate = null
    audioEl.src = ''
    audioEl = null
  }
  isPlaying.value = false
  playProgress.value = 0
  playCurrentTime.value = 0
  playDuration.value = 0
}

function togglePlay() {
  const src = currentAudioSrc.value
  if (!src) return

  if (isPlaying.value) {
    audioEl?.pause()
    isPlaying.value = false
    return
  }

  if (!audioEl) audioEl = new Audio()

  // 每次用最新的 src（blob URL 可能变化）
  audioEl.src = src
  audioEl.load()

  audioEl.ontimeupdate = () => {
    playCurrentTime.value = audioEl.currentTime
    playDuration.value = audioEl.duration || 0
    playProgress.value = audioEl.duration ? (audioEl.currentTime / audioEl.duration * 100) : 0
  }
  audioEl.onended = () => { isPlaying.value = false; playProgress.value = 100 }
  audioEl.onerror = () => { isPlaying.value = false; showToast('播放失败') }

  audioEl.play().then(() => { isPlaying.value = true }).catch(() => { showToast('播放失败，请点击重试') })
}

function switchPage(i) {
  stopAudio()
  idx.value = i
}

watch(idx, () => { stopAudio() })

// ─── Manual Recording (按住录音) ───

async function startRecording() {
  if (isRecording.value || voicingSingle.value || voicingAll.value) return

  stopAudio() // 停止播放

  try {
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
      showToast('请使用 HTTPS 访问以启用录音功能')
      return
    }
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
    recordedChunks = []
    mediaRecorder = new MediaRecorder(stream, { mimeType: getSupportedMimeType() })

    mediaRecorder.ondataavailable = (e) => {
      if (e.data.size > 0) recordedChunks.push(e.data)
    }

    mediaRecorder.onstop = () => {
      // 释放麦克风
      stream.getTracks().forEach(t => t.stop())

      if (recordedChunks.length === 0) return

      const blob = new Blob(recordedChunks, { type: mediaRecorder.mimeType })

      // 检查录音大小（太短的过滤掉）
      if (blob.size < 1000) {
        showToast('录音太短，请重新录制')
        return
      }

      // 存入当前页的 localBlob
      const p = pages.value[idx.value]
      if (p) {
        p.localBlob = blob
        p.isAiVoice = false
        showToast('录音完成!')
        // 自动跳到下一个未配音的页面
        autoAdvance()
      }
    }

    mediaRecorder.start()
    isRecording.value = true
  } catch (e) {
    if (e.name === 'NotAllowedError') {
      showToast('请允许麦克风权限后重试')
    } else {
      showToast('无法启动录音: ' + (e.message || '未知错误'))
    }
  }
}

function stopRecording() {
  if (!isRecording.value || !mediaRecorder) return
  isRecording.value = false
  if (mediaRecorder.state === 'recording') {
    mediaRecorder.stop()
  }
}

function getSupportedMimeType() {
  const types = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg']
  for (const t of types) {
    if (MediaRecorder.isTypeSupported(t)) return t
  }
  return ''
}

// 自动跳到下一个未配音的页
function autoAdvance() {
  for (let i = idx.value + 1; i < pages.value.length; i++) {
    if (!pages.value[i].audioUrl && !pages.value[i].localBlob) {
      idx.value = i
      return
    }
  }
  // 都配完了，不跳
}

// ─── AI Voice ───

async function voiceSingle() {
  voicingSingle.value = true
  try {
    const res = await voicePage({ workId: workId.value, voiceAll: false, pageNum: currentPage.value.pageNum })
    const data = res.data
    if (data.voicedPages?.length) {
      for (const vp of data.voicedPages) {
        const p = pages.value.find(x => x.pageNum === vp.pageNum)
        if (p) {
          p.audioUrl = vp.audioUrl
          p.localBlob = null // AI覆盖本地录音
          p.isAiVoice = true
        }
      }
      showToast('AI配音成功')
    } else {
      showToast('配音失败，请重试')
    }
  } catch (e) {
    showToast(e.message || '配音失败')
  } finally {
    voicingSingle.value = false
  }
}

async function voiceAllConfirm() {
  const confirmed = await showConfirm('将为所有未配音的页面生成AI语音，预计需要30-60秒，确认继续？', {
    title: 'AI 配音',
    okText: '开始生成',
    cancelText: '再想想',
  })
  if (!confirmed) return
  voicingAll.value = true
  try {
    const res = await voicePage({ workId: workId.value, voiceAll: true })
    const data = res.data
    if (data.voicedPages) {
      for (const vp of data.voicedPages) {
        const p = pages.value.find(x => x.pageNum === vp.pageNum)
        if (p) {
          p.audioUrl = vp.audioUrl
          p.localBlob = null
          p.isAiVoice = true
        }
      }
    }
    const failed = data.failedPages?.length || 0
    showToast(failed > 0 ? `${data.totalSucceeded}页成功，${failed}页失败` : '全部AI配音完成!')
  } catch (e) {
    showToast(e.message || '配音失败')
  } finally {
    voicingAll.value = false
  }
}

// ─── 完成配音 ───

async function finish() {
  submitting.value = true
  try {
    const pendingLocal = pages.value.filter(p => p.localBlob)

    if (pendingLocal.length > 0) {
      // 有本地录音：上传到 OSS，然后通过 batchUpdateAudio 更新DB并推进状态
      const audioPages = []
      for (let i = 0; i < pendingLocal.length; i++) {
        const p = pendingLocal[i]
        showToast(`上传录音 ${i + 1}/${pendingLocal.length}...`)
        const ext = p.localBlob.type?.includes('webm') ? 'webm' : 'm4a'
        const ossUrl = await ossUpload(p.localBlob, { type: 'aud', ext })
        audioPages.push({ pageNum: p.pageNum, audioUrl: ossUrl })
        p.audioUrl = ossUrl
        p.localBlob = null
      }
      showToast('保存配音...')
      // batchUpdateAudio (C2) 会同时更新音频URL并推进状态 CATALOGED→DUBBED
      await batchUpdateAudio(workId.value, audioPages)
    } else {
      // 无本地录音：仍需调用 C2 API 推进状态 CATALOGED(4)→DUBBED(5)
      // C2 允许空 pages，调用后状态即推进
      showToast('完成配音...')
      await finishDubbing(workId.value)
    }

    store.workDetail = null  // 清除缓存
    showToast('配音完成!')
    setTimeout(() => router.push(`/read/${workId.value}`), 800)
  } catch (e) {
    // 容错：即使报错也检查实际状态，可能请求已经成功但重试触发了CAS失败
    try {
      const check = await getWorkDetail(workId.value)
      if (check?.data?.status >= 5) {
        store.workDetail = null
        showToast('配音已完成')
        setTimeout(() => router.push(`/read/${workId.value}`), 800)
        return
      }
    } catch { /* ignore check error */ }
    showToast('提交失败：' + (e.message || '请重试'))
  } finally {
    submitting.value = false
  }
}

// ─── Load ───

async function loadWork() {
  loading.value = true
  try {
    if (!store.workDetail || store.workDetail.workId !== workId.value) {
      store.workDetail = null
      const res = await getWorkDetail(workId.value)
      store.workDetail = res.data
    }
    const w = store.workDetail

    // 如果作品已完成配音(DUBBED)，直接跳到阅读页
    if (w.status >= STATUS.DUBBED) {
      const route = getRouteByStatus(w.status, w.workId)
      if (route) { router.replace(route); return }
    }

    pages.value = (w.pageList || []).map(p => ({
      pageNum: p.pageNum,
      text: p.text,
      imageUrl: p.imageUrl,
      audioUrl: p.audioUrl || null,
      localBlob: null,     // 本地录音 Blob
      isAiVoice: p.audioUrl ? true : null  // 区分来源
    }))
  } catch { /* fallback empty */ }
  loading.value = false
}

onMounted(loadWork)
onBeforeUnmount(() => {
  stopAudio()
  if (isRecording.value && mediaRecorder?.state === 'recording') {
    mediaRecorder.stop()
  }
})
</script>

<style lang="scss" scoped>
.dubbing-page {
  height: 100vh;
  height: 100dvh;  /* 动态视口高度，兼容移动端地址栏 */
  background: linear-gradient(180deg, #F0F8FF 0%, #FFF5F0 40%, #FFF8E7 70%, #FFFDF7 100%);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.loading-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.loading-emojis {
  display: flex;
  gap: 16px;
  margin-bottom: 12px;
}
.loading-emoji {
  font-size: 36px;
  display: inline-block;
  animation: emojiPop 1.8s ease-in-out infinite;
  &.e1 { animation-delay: 0s; }
  &.e2 { animation-delay: 0.4s; }
  &.e3 { animation-delay: 0.8s; }
}
@keyframes emojiPop {
  0%, 100% { transform: scale(1) rotate(0deg); opacity: 0.5; }
  50% { transform: scale(1.3) rotate(10deg); opacity: 1; }
}
.loading-text { font-size: 16px; color: var(--text-sub); font-weight: 600; }

.content {
  flex: 1;
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 16px;
}

/* 页面展示 */
.page-display { display: flex; flex-direction: column; gap: 10px; }
.page-image-wrap {
  position: relative;
  border-radius: 20px;
  overflow: hidden;
  background: linear-gradient(135deg, #F8F6F0, #F0EDE8);
  box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.page-image {
  width: 100%;
  display: block;
  object-fit: contain;
  max-height: 38vh;
}
.placeholder {
  height: 180px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 48px;
}
.page-badge-pill {
  position: absolute;
  top: 10px;
  left: 10px;
  background: linear-gradient(135deg, rgba(255,107,53,0.9), rgba(255,140,66,0.9));
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  padding: 4px 14px;
  border-radius: 20px;
  backdrop-filter: blur(4px);
  box-shadow: 0 2px 8px rgba(255,107,53,0.3);
}

.page-text-card {
  background: rgba(255,255,255,0.92);
  border-radius: 18px;
  padding: 16px 20px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.05);
  position: relative;
  border-left: 4px solid #FFD166;
}
.text-quote-mark {
  position: absolute;
  top: 4px;
  left: 12px;
  font-size: 32px;
  color: #FFD166;
  opacity: 0.3;
  font-family: serif;
  font-weight: 900;
  line-height: 1;
}
.page-text-content {
  font-size: 15px;
  line-height: 1.8;
  color: var(--text);
  text-align: center;
  padding-top: 4px;
}

/* 音频控制 */
.audio-controls {
  background: rgba(255,255,255,0.92);
  border-radius: 18px;
  padding: 14px 16px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.04);
}
.audio-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.play-btn {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: none;
  background: linear-gradient(135deg, #FF8C42, #FF6B35);
  color: #fff;
  font-size: 18px;
  cursor: pointer;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 3px 12px rgba(255,107,53,0.3);
  transition: transform 0.2s;
  &:active { transform: scale(0.9); }
}
.audio-progress { flex: 1; }
.audio-bar {
  height: 6px;
  background: #F0EDE8;
  border-radius: 3px;
  overflow: hidden;
}
.audio-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, #FF8C42, #FF6B35);
  border-radius: 3px;
  transition: width 0.3s;
}
.audio-time {
  font-size: 11px;
  color: var(--text-sub);
  margin-top: 4px;
  display: block;
}
.audio-source-tag {
  font-size: 11px;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 10px;
  white-space: nowrap;
  flex-shrink: 0;
  &.ai { background: #FFF0E8; color: var(--primary); }
  &.local { background: #FEF2F2; color: #EF4444; }
  &.human { background: #F0F8FF; color: #3B82F6; }
}

.audio-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px 0;
  font-size: 14px;
  color: var(--text-sub);
  font-weight: 500;
}
.empty-icon { font-size: 20px; }

/* 配音按钮 */
.voice-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* 录音大按钮 */
.record-btn {
  width: 100%;
  padding: 16px;
  border-radius: 22px;
  border: 2.5px solid #EF4444;
  background: linear-gradient(135deg, #FEF2F2, #FFF5F5);
  color: #EF4444;
  font-weight: 700;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.2s;
  user-select: none;
  -webkit-user-select: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  box-shadow: 0 3px 12px rgba(239,68,68,0.12);

  &:active, &.recording {
    background: linear-gradient(135deg, #EF4444, #DC2626);
    color: #fff;
    transform: scale(1.02);
    box-shadow: 0 6px 24px rgba(239,68,68,0.3);
    border-color: #DC2626;
  }
  &:disabled {
    opacity: 0.4;
    pointer-events: none;
  }
}
.record-icon { font-size: 22px; }
.record-text { font-size: 16px; }

.ai-btns {
  display: flex;
  gap: 10px;
}
.ai-btn {
  flex: 1;
  padding: 14px 8px;
  font-size: 14px;
  font-weight: 700;
  border-radius: 18px;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  transition: all 0.2s;

  &.single {
    background: linear-gradient(135deg, #FF8C42, #FF6B35);
    color: #fff;
    box-shadow: 0 3px 12px rgba(255,107,53,0.25);
  }
  &.all {
    background: rgba(255,255,255,0.9);
    color: var(--primary);
    border: 2px solid #FFE4D0;
    box-shadow: 0 2px 8px rgba(0,0,0,0.04);
  }
  &:active { transform: scale(0.97); }
  &:disabled { opacity: 0.4; pointer-events: none; }
}

/* 缩略图横向滚动列表 */
.thumb-section { margin-top: 4px; }
.thumb-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}
.thumb-title { font-size: 15px; font-weight: 800; color: var(--text); }
.thumb-count { font-size: 13px; color: var(--primary); font-weight: 700; }

.thumb-strip {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  padding: 6px 2px 12px;
  scrollbar-width: none;
  &::-webkit-scrollbar { display: none; }
}

.thumb-item {
  flex-shrink: 0;
  width: 64px;
  height: 80px;
  border-radius: 12px;
  overflow: hidden;
  border: 2.5px solid transparent;
  cursor: pointer;
  transition: all 0.2s;
  position: relative;
  background: rgba(255,255,255,0.8);
  box-shadow: 0 2px 8px rgba(0,0,0,0.05);

  &.active {
    border-color: var(--primary);
    box-shadow: 0 4px 16px rgba(255,107,53,0.25);
    transform: scale(1.08);
  }
  &.voiced { border-color: #C8E6C9; }
  &.active.voiced { border-color: var(--primary); }
}
.thumb-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.thumb-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #F5F0E8;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-sub);
}
.thumb-status {
  position: absolute;
  bottom: 4px;
  left: 50%;
  transform: translateX(-50%);
}
.status-dot {
  display: block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  &.ai-dot { background: #2EC4B6; box-shadow: 0 0 4px rgba(46,196,182,0.5); }
  &.local-dot { background: #EF4444; box-shadow: 0 0 4px rgba(239,68,68,0.5); }
  &.empty-dot { background: #D1D5DB; }
}
.thumb-voice-icon {
  position: absolute;
  top: 2px;
  right: 3px;
  font-size: 10px;
  opacity: 0.7;
}

/* 底部 */
.bottom-bar {
  flex-shrink: 0;
  padding: 14px 20px 20px;
  padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(transparent, rgba(255,253,247,0.95) 25%);
}
.finish-btn {
  font-size: 17px !important;
  padding: 16px 0 !important;
  border-radius: 28px !important;
  background: linear-gradient(135deg, #FF8C42, #FF6B35) !important;
  box-shadow: 0 6px 24px rgba(255,107,53,0.3) !important;
}
.local-hint {
  text-align: center;
  font-size: 12px;
  color: var(--text-sub);
  margin-top: 8px;
  font-weight: 500;
}

/* Toast */
.toast {
  position: fixed;
  top: 60px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(45,45,63,0.88);
  color: #fff;
  padding: 12px 28px;
  border-radius: 24px;
  font-size: 14px;
  font-weight: 600;
  z-index: 999;
  max-width: 80%;
  text-align: center;
  box-shadow: 0 4px 16px rgba(0,0,0,0.15);
  backdrop-filter: blur(8px);
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }

/* ── 确认弹窗（Ant Design 风格） ── */
.confirm-overlay {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
.confirm-modal {
  background: #fff;
  border-radius: 8px;
  padding: 24px;
  min-width: 280px;
  max-width: 400px;
  box-shadow: 0 6px 30px rgba(0, 0, 0, 0.12);
  animation: confirmFadeIn 0.2s ease;
}
@keyframes confirmFadeIn {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}
.confirm-title {
  font-size: 16px;
  font-weight: 600;
  color: #1f2937;
  margin-bottom: 8px;
}
.confirm-content {
  font-size: 14px;
  color: #6b7280;
  line-height: 1.6;
  margin-bottom: 24px;
}
.confirm-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}
.confirm-btn {
  padding: 5px 16px;
  border-radius: 6px;
  font-size: 14px;
  cursor: pointer;
  height: 32px;
  line-height: 1;
  transition: all 0.2s;

  &.cancel {
    border: 1px solid #d1d5db;
    background: #fff;
    color: #374151;
    &:hover { border-color: #6366f1; color: #6366f1; }
  }
  &.ok {
    border: none;
    background: #6366f1;
    color: #fff;
    font-weight: 500;
    &:hover { background: #4f46e5; }
  }
}
</style>
