<script lang="ts">
export default { name: 'DubbingView' }
</script>
<template>
  <div class="dubbing-page page-fullscreen">
    <PageHeader title="绘本配音" subtitle="为每一页添加 AI 配音或亲自录音" :showBack="true" />

    <div v-if="loading" class="loading-state">
      <loading-outlined class="loading-icon" spin />
      <div class="loading-text">正在加载…</div>
    </div>

    <template v-else>
      <div class="content page-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">
              <picture-outlined />
            </div>
          </div>
          <div v-if="currentPage.text" class="page-text-card">
            {{ currentPage.text }}
          </div>
        </div>

        <!-- 音频控制 -->
        <div class="audio-controls">
          <template v-if="currentAudioSrc">
            <button class="play-btn" @click="togglePlay">
              <pause-outlined v-if="isPlaying" />
              <caret-right-outlined v-else />
            </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>
            <span class="audio-source-tag" :class="audioSourceClass">
              <component :is="audioSourceIcon" />
              <span>{{ audioSourceLabel }}</span>
            </span>
          </template>
          <div v-else class="audio-empty">
            <sound-outlined />
            <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()"
          >
            <audio-outlined />
            <span>{{ isRecording ? '正在录音，松开结束' : '按住录音' }}</span>
          </button>
          <div class="ai-btns">
            <button
              class="ai-btn single"
              :disabled="voicingSingle || voicingAll || isRecording"
              @click="voiceSingle"
            >
              <thunderbolt-outlined />
              <span>{{ voicingSingle ? '配音中…' : 'AI 配音此页' }}</span>
            </button>
            <button
              class="ai-btn all"
              :disabled="voicingSingle || voicingAll || isRecording"
              @click="voiceAllConfirm"
            >
              <thunderbolt-outlined />
              <span>{{ voicingAll ? '配音中…' : '全部 AI 配音' }}</span>
            </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">
                <picture-outlined />
              </div>
              <div class="thumb-num">{{ i === 0 ? '封面' : i }}</div>
              <div v-if="p.audioUrl || p.localBlob" class="thumb-voice-flag">
                <sound-filled />
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- 底部 -->
      <div class="page-bottom dubbing-bottom">
        <button class="btn-primary finish-btn" :disabled="submitting" @click="finish">
          <template v-if="submitting">
            <loading-outlined spin />
            <span>提交中…</span>
          </template>
          <template v-else>
            <span>完成配音</span>
            <arrow-right-outlined />
          </template>
        </button>
        <div v-if="localCount > 0" class="local-hint">
          <audio-outlined />
          <span>{{ localCount }} 页录音将在提交时上传</span>
        </div>
      </div>
    </template>

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

    <!-- 确认弹窗 -->
    <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 lang="ts">
import { ref, computed, onActivated, onBeforeUnmount, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
  LoadingOutlined,
  PictureOutlined,
  CaretRightOutlined,
  PauseOutlined,
  SoundOutlined,
  SoundFilled,
  AudioOutlined,
  ThunderboltOutlined,
  ArrowRightOutlined,
} from '@ant-design/icons-vue'
import PageHeader from '@/components/aicreate/PageHeader.vue'
import { getWorkDetail, voicePage, ossUpload, batchUpdateAudio, finishDubbing } from '@/api/aicreate'
import { useAicreateStore } from '@/stores/aicreate'
import { STATUS, getRouteByStatus } from '@/utils/aicreate/status'

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

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

const voicingSingle = ref(false)
const voicingAll = ref(false)

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

// Audio playback state
let audioEl: HTMLAudioElement | null = 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 audioSourceClass = computed(() => {
  const p = currentPage.value
  if (p.localBlob) return 'local'
  if (p.isAiVoice === false) return 'human'
  return 'ai'
})
const audioSourceIcon = computed(() => {
  const cls = audioSourceClass.value
  if (cls === 'ai') return ThunderboltOutlined
  return AudioOutlined
})
const audioSourceLabel = computed(() => {
  const cls = audioSourceClass.value
  if (cls === 'local') return '录音'
  if (cls === 'human') return '录音'
  return 'AI'
})

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: string) {
  toast.value = msg
  setTimeout(() => { toast.value = '' }, 2500)
}

// -- 确认弹窗 --
const confirmVisible = ref(false)
const confirmTitle = ref('')
const confirmContent = ref('')
const confirmOkText = ref('确认')
const confirmCancelText = ref('取消')
let confirmResolve: ((v: boolean) => void) | null = null

function showConfirm(content: string, options: any = {}): Promise<boolean> {
  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: number) {
  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
    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()

  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: number) {
  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: any) => {
      if (e.data.size > 0) recordedChunks.push(e.data)
    }

    mediaRecorder.onstop = () => {
      stream.getTracks().forEach((t: any) => t.stop())

      if (recordedChunks.length === 0) return

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

      if (blob.size < 1000) {
        showToast('录音太短，请重新录制')
        return
      }

      const p = pages.value[idx.value]
      if (p) {
        p.localBlob = blob
        p.isAiVoice = false
        showToast('录音完成')
        autoAdvance()
      }
    }

    mediaRecorder.start()
    isRecording.value = true
  } catch (e: any) {
    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
    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
          p.isAiVoice = true
        }
      }
      showToast('AI 配音成功')
    } else {
      showToast('配音失败，请重试')
    }
  } catch (e: any) {
    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
    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: any) {
    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) {
      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('保存配音…')
      await batchUpdateAudio(workId.value, audioPages)
    } else {
      showToast('完成配音…')
      await finishDubbing(workId.value)
    }

    store.workDetail = null
    showToast('配音完成')
    setTimeout(
      () =>
        router.push({
          name: 'PublicCreateEditInfo',
          params: { workId: String(workId.value || '') },
        }),
      800,
    )
  } catch (e: any) {
    try {
      const check = await getWorkDetail(workId.value)
      if (check?.status >= 5) {
        store.workDetail = null
        showToast('配音已完成')
        setTimeout(
          () =>
            router.push({
              name: 'PublicCreateEditInfo',
              params: { workId: String(workId.value || '') },
            }),
          800,
        )
        return
      }
    } catch { /* ignore */ }
    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
    }
    const w = store.workDetail

    if (w.status >= STATUS.DUBBED) {
      const nextRoute = getRouteByStatus(w.status, w.workId)
      if (nextRoute) { router.replace(nextRoute); return }
    }

    pages.value = (w.pageList || []).map((p: any) => ({
      pageNum: p.pageNum,
      text: p.text,
      imageUrl: p.imageUrl,
      audioUrl: p.audioUrl || null,
      localBlob: null,
      isAiVoice: p.audioUrl ? true : null,
    }))
  } catch { /* fallback */ }
  loading.value = false
}

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

<style lang="scss" scoped>
.dubbing-page {
  background: var(--ai-bg);
}

/* ---------- 加载 ---------- */
.loading-state {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.loading-icon {
  font-size: 44px;
  color: var(--ai-primary);
  margin-bottom: 14px;
}
.loading-text {
  font-size: 14px;
  color: var(--ai-text-sub);
  font-weight: 500;
}

/* ---------- 内容区 ---------- */
.content {
  padding: 14px 16px 8px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  -webkit-overflow-scrolling: touch;
}

/* ---------- 当前页大图 + 文字 ---------- */
.page-display {
  display: flex;
  flex-direction: column;
  gap: 0;
}
.page-image-wrap {
  position: relative;
  border-radius: 18px;
  overflow: hidden;
  background: #1e1b4b;
  box-shadow: 0 8px 24px rgba(99, 102, 241, 0.18);
}
.page-image {
  width: 100%;
  display: block;
  object-fit: contain;
  max-height: 32vh;
  background: #1e1b4b;
}
.placeholder {
  height: 180px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 48px;
  color: rgba(255, 255, 255, 0.3);
}
.page-badge-pill {
  position: absolute;
  top: 10px;
  left: 10px;
  background: var(--ai-gradient);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  padding: 4px 14px;
  border-radius: 20px;
  z-index: 2;
  box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
  letter-spacing: 0.5px;
}

.page-text-card {
  background: #fff;
  border: 1px solid rgba(99, 102, 241, 0.06);
  border-top: none;
  border-radius: 0 0 18px 18px;
  margin-top: -8px;
  padding: 14px 18px 12px;
  font-size: 14px;
  line-height: 1.7;
  color: var(--ai-text);
  box-shadow: 0 4px 14px rgba(99, 102, 241, 0.06);
  font-weight: 500;
  position: relative;
  z-index: 1;
}

/* ---------- 音频控件 ---------- */
.audio-controls {
  display: flex;
  align-items: center;
  gap: 12px;
  background: #fff;
  border: 1px solid rgba(99, 102, 241, 0.06);
  border-radius: var(--ai-radius);
  padding: 12px 14px;
  box-shadow: 0 2px 12px rgba(99, 102, 241, 0.05);
}
.play-btn {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: none;
  background: var(--ai-gradient);
  color: #fff;
  font-size: 16px;
  cursor: pointer;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 14px rgba(99, 102, 241, 0.32);
  transition: transform 0.2s;

  &:active { transform: scale(0.92); }
}
.audio-progress {
  flex: 1;
  min-width: 0;
}
.audio-bar {
  height: 5px;
  background: rgba(99, 102, 241, 0.1);
  border-radius: 3px;
  overflow: hidden;
}
.audio-bar-fill {
  height: 100%;
  background: var(--ai-gradient);
  border-radius: 3px;
  transition: width 0.3s;
}
.audio-time {
  font-size: 11px;
  color: var(--ai-text-sub);
  margin-top: 4px;
  display: block;
  font-weight: 500;
}
.audio-source-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 700;
  padding: 4px 10px;
  border-radius: 10px;
  white-space: nowrap;
  flex-shrink: 0;

  :deep(.anticon) { font-size: 11px; }

  &.ai {
    background: rgba(99, 102, 241, 0.1);
    color: var(--ai-primary);
  }
  &.local, &.human {
    background: rgba(239, 68, 68, 0.1);
    color: #ef4444;
  }
}

.audio-empty {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 4px 0;
  font-size: 13px;
  color: var(--ai-text-sub);
  font-weight: 500;

  :deep(.anticon) { font-size: 16px; }
}

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

/* 按住录音 */
.record-btn {
  width: 100%;
  padding: 14px;
  border-radius: 22px;
  border: 1.5px solid rgba(239, 68, 68, 0.35);
  background: rgba(239, 68, 68, 0.04);
  color: #ef4444;
  font-weight: 700;
  font-size: 15px;
  cursor: pointer;
  transition: all 0.2s;
  user-select: none;
  -webkit-user-select: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;

  :deep(.anticon) { font-size: 18px; }

  &:active,
  &.recording {
    background: linear-gradient(135deg, #ef4444, #dc2626);
    color: #fff;
    border-color: #dc2626;
    box-shadow: 0 6px 22px rgba(239, 68, 68, 0.32);
  }

  &:disabled {
    opacity: 0.4;
    pointer-events: none;
  }
}

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

  :deep(.anticon) { font-size: 14px; }

  &.single {
    background: var(--ai-gradient);
    color: #fff;
    border: none;
    box-shadow: 0 4px 14px rgba(99, 102, 241, 0.32);

    &:hover { transform: translateY(-1px); }
  }

  &.all {
    background: #fff;
    color: var(--ai-primary);
    border: 1.5px solid rgba(99, 102, 241, 0.3);
    box-shadow: 0 2px 10px rgba(99, 102, 241, 0.06);

    &:hover {
      border-color: var(--ai-primary);
      background: rgba(99, 102, 241, 0.04);
    }
  }

  &:active { transform: scale(0.97); }
  &:disabled { opacity: 0.4; pointer-events: none; }
}

/* ---------- 缩略图横向胶卷 ---------- */
.thumb-section { margin-top: 2px; }
.thumb-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
  padding: 0 2px;
}
.thumb-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--ai-text);
}
.thumb-count {
  font-size: 12px;
  color: var(--ai-primary);
  font-weight: 700;
}

.thumb-strip {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 4px 2px 8px;
  scroll-snap-type: x proximity;
  scroll-padding: 0 50%;

  scrollbar-width: none;
  -ms-overflow-style: none;
  &::-webkit-scrollbar { display: none; }
}

.thumb-item {
  position: relative;
  flex-shrink: 0;
  width: 88px;
  aspect-ratio: 16 / 9;
  border-radius: 10px;
  overflow: hidden;
  cursor: pointer;
  border: 2px solid transparent;
  background: #1e1b4b;
  box-shadow: 0 2px 10px rgba(99, 102, 241, 0.08);
  transition: all 0.25s ease;
  scroll-snap-align: center;

  &:hover {
    transform: translateY(-2px);
  }

  &.active {
    border-color: var(--ai-primary);
    transform: translateY(-3px) scale(1.06);
    box-shadow: 0 8px 22px rgba(99, 102, 241, 0.36);
    z-index: 1;
  }

  &.voiced .thumb-num {
    background: linear-gradient(to top, rgba(16, 185, 129, 0.85), transparent);
  }
}
.thumb-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.thumb-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  color: rgba(255, 255, 255, 0.3);
}
.thumb-num {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 8px 4px 3px;
  background: linear-gradient(to top, rgba(15, 12, 41, 0.85), transparent);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  text-align: center;
  letter-spacing: 0.5px;
  line-height: 1;
  transition: background 0.3s;
}
.thumb-voice-flag {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--ai-success);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 9px;
  box-shadow: 0 2px 6px rgba(16, 185, 129, 0.4);
}

/* ---------- 底部 ---------- */
.dubbing-bottom {
  border-top: 1px solid rgba(99, 102, 241, 0.06);
}
.finish-btn {
  display: flex !important;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 16px !important;
  padding: 14px 0 !important;
  border-radius: 28px !important;

  :deep(.anticon) { font-size: 16px; }
}
.local-hint {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  font-size: 12px;
  color: var(--ai-text-sub);
  margin-top: 8px;
  font-weight: 500;

  :deep(.anticon) { font-size: 13px; color: var(--ai-primary); }
}

/* ---------- Toast ---------- */
.toast {
  position: fixed;
  top: 64px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(30, 27, 75, 0.92);
  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 6px 20px rgba(15, 12, 41, 0.25);
  backdrop-filter: blur(8px);
}
.fade-enter-active,
.fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from,
.fade-leave-to { opacity: 0; }

/* ---------- 确认弹窗 ---------- */
.confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 12, 41, 0.5);
  backdrop-filter: blur(4px);
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.confirm-modal {
  background: #fff;
  border-radius: 20px;
  padding: 24px;
  min-width: 280px;
  max-width: 360px;
  width: 100%;
  box-shadow: 0 20px 60px rgba(15, 12, 41, 0.25);
  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: 700;
  color: var(--ai-text);
  margin-bottom: 10px;
}
.confirm-content {
  font-size: 14px;
  color: var(--ai-text-sub);
  line-height: 1.6;
  margin-bottom: 22px;
}
.confirm-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}
.confirm-btn {
  padding: 8px 18px;
  border-radius: 16px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;

  &.cancel {
    border: 1px solid rgba(99, 102, 241, 0.2);
    background: #fff;
    color: var(--ai-text-sub);
    &:hover { border-color: var(--ai-primary); color: var(--ai-primary); }
  }
  &.ok {
    border: none;
    background: var(--ai-gradient);
    color: #fff;
    box-shadow: 0 4px 12px rgba(99, 102, 241, 0.32);
    &:hover { transform: translateY(-1px); }
  }
}
</style>
