<!--
  会议主页面（Task 11）

  职责：
  - 组装 VideoGrid / VideoTile / MeetingToolbar / MemberPanel / InviteDialog / NetworkBadge
  - 把 Pinia 的 participants + remoteConsumers + 本地 track 投影成 Grid 所需的 tile 列表
  - 处理麦/摄/邀请/成员/挂断交互，挂断时提供主持人"结束会议 / 仅自己离开"双选

  数据流：
  - 进入本页前必须已通过 preview.vue 完成 createAndEnter / joinAndEnter
  - onLoad 读取 query.code，若 store 未在会或 code 不匹配则回退到 /pages/meeting/join
-->
<template>
  <view class="room">
    <!-- 顶部信息栏 -->
    <view class="top-bar">
      <view class="top-left">
        <view v-if="showCloudLawBack" class="cloud-law-back" @click="onCloudLawBack">
          <text class="cloud-law-back-icon">‹</text>
        </view>
        <view class="top-left-info">
        <text class="meeting-title">{{ meetingTitle }}</text>
        <view class="code-pill" @click="openInvite">
          <text class="code-text">{{ formattedCode }}</text>
          <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
            <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
          </svg>
        </view>
        </view>
      </view>
      <view class="top-right">
        <view class="state-pill" :class="stateClass">
          <view class="state-dot"></view>
          <text>{{ stateText }}</text>
        </view>
        <!-- Phase B 录制指示器：所有人可见，点击对 host 触发停止；非 host 仅展示 -->
        <view
          v-if="isRecording || recordingLoading"
          class="recording-indicator"
          :class="{ uploading: recordingLoading && !isRecording }"
          :title="recordingHintText"
        >
          <view class="rec-dot"></view>
          <text class="rec-text">{{ isRecording ? 'REC' : '上传中' }}</text>
        </view>
        <NetworkBadge :level="networkLevel" />
        <text class="timer">{{ durationText }}</text>
      </view>
    </view>

    <!-- 主区：左视频 + 右侧聊天面板（仅 chatVisible 时侧栏展开） -->
    <view class="body">
      <view ref="videoCol" class="video-col" :class="{ 'has-screen': !!screenTile }">
        <!-- Phase 3：有屏幕共享时，屏幕画面独占大画面区，参与者网格降级为底部横向条带 -->
        <view v-if="screenTile" class="screen-stage">
          <view class="screen-stage-tile">
            <VideoTile
              :user-id="screenTile.userId"
              :name="screenTile.name + '（屏幕共享）'"
              :is-local="screenTile.isLocal"
              :is-host="screenTile.isHost"
              :audio-enabled="false"
              :video-enabled="true"
              :audio-track="null"
              :video-track="screenTile.videoTrack"
              video-fit="contain"
              :is-speaking="false"
            />
          </view>
          <view class="screen-strip">
            <view v-for="t in stripTiles" :key="t.key" class="strip-cell">
              <VideoTile
                :user-id="t.userId"
                :name="t.name"
                :is-local="t.isLocal"
                :is-host="t.isHost"
                :audio-enabled="t.audioEnabled"
                :video-enabled="t.videoEnabled"
                :audio-track="t.audioTrack"
                :video-track="t.videoTrack"
                :is-speaking="!!speakingMap[t.userId]"
                :compact="true"
              />
            </view>
          </view>
        </view>

        <VideoGrid v-else :tiles="gridTiles">
          <template #tile="{ tile }">
            <VideoTile
              :user-id="tile.userId"
              :name="tile.name"
              :is-local="tile.isLocal"
              :is-host="tile.isHost"
              :audio-enabled="tile.audioEnabled"
              :video-enabled="tile.videoEnabled"
              :audio-track="tile.audioTrack"
              :video-track="tile.videoTrack"
              :is-speaking="!!speakingMap[tile.userId]"
            />
          </template>
        </VideoGrid>

        <!-- 自视频浮窗（Task 15）：仅桌面端浮窗模式开启时显示；图钉切换后本地 tile 回到网格里 -->
        <SelfVideoFloat
          v-if="floatEnabled && selfTile"
          :user-id="selfTile.userId"
          :name="selfTile.name"
          :is-host="selfTile.isHost"
          :audio-enabled="selfTile.audioEnabled"
          :video-enabled="selfTile.videoEnabled"
          :audio-track="selfTile.audioTrack"
          :video-track="selfTile.videoTrack"
          :is-speaking="!!speakingMap[selfTile.userId]"
          :container="videoCol"
          @pin-click="togglePin"
        />
      </view>
      <view class="chat-col" :class="{ open: chatVisible }">
        <ChatPanel
          :visible="chatVisible"
          :messages="chatMessages"
          :current-user-id="myUserId"
          :display-name-map="displayNameMap"
          :has-more="chatHasMore"
          :loading-more="chatLoadingMore"
          @close="closeChat"
          @send="onChatSend"
          @load-more="onChatLoadMore"
          @request-initial-load="onChatRequestInitialLoad"
        />
      </view>
    </view>

    <!-- 底部工具条 -->
    <MeetingToolbar
      :audio-enabled="audioEnabled"
      :video-enabled="videoEnabled"
      :audio-loading="audioLoading"
      :video-loading="videoLoading"
      :member-count="tiles.length"
      :unread-chat-count="unreadChatCount"
      :allow-chat="allowChat"
      :all-muted="allMuted"
      :screen-sharing="screenSharingByMe"
      :screen-loading="screenLoading"
      :screen-disabled="screenDisabled"
      :screen-disabled-reason="screenDisabledReason"
      :show-recording="isHost"
      :recording="isRecording"
      :recording-loading="recordingLoading"
      :replace-chat-with-screen-share="replaceChatWithScreenShare"
      @mic-toggle="onMicToggle"
      @cam-toggle="onCamToggle"
      @screen-toggle="onScreenToggle"
      @recording-toggle="onRecordingToggle"
      @invite="openInvite"
      @members="openMembers"
      @chat="openChat"
      @leave="onLeaveClick"
    />

    <!-- 成员抽屉 -->
    <MemberPanel
      :visible="memberVisible"
      :participants="participants"
      :current-user-id="myUserId"
      :host-id="hostId"
      :is-host="isHost"
      :display-name-map="displayNameMap"
      @close="memberVisible = false"
      @kick="onKickMember"
      @transfer-host="onTransferHost"
      @mute-member="onMuteMember"
    />

    <!-- 邀请弹窗 -->
    <InviteDialog
      :visible="inviteVisible"
      :room-code="roomCode"
      :password="invitePassword"
      :meeting-title="meetingTitle"
      :join-link="joinLink"
      @close="inviteVisible = false"
    />

    <!-- 离会确认弹窗 -->
    <view v-if="leaveDialogVisible" class="leave-mask" @click.self="leaveDialogVisible = false">
      <view class="leave-modal">
        <view class="leave-title">确认{{ isHost ? '结束或离开' : '离开' }}会议？</view>
        <view class="leave-desc">
          {{ isHost ? '作为主持人，你可以选择结束会议（所有成员都会离开），或仅自己离开（自动转让给其他成员）。' : '离开后可通过会议号重新加入。' }}
        </view>
        <view class="leave-actions">
          <button class="leave-btn leave-cancel" @click="leaveDialogVisible = false">取消</button>
          <button v-if="isHost" class="leave-btn leave-leave" @click="confirmLeaveSelf">仅自己离开</button>
          <button class="leave-btn leave-danger" @click="confirmEndOrLeave">
            {{ isHost ? '结束会议' : '离开会议' }}
          </button>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { useMeetingStore } from '@/store/meeting'
import { useUserStore } from '@/store/user'
import {
  MEETING_LOCAL_STATE_CONNECTED,
  MEETING_LOCAL_STATE_CONNECTING,
  MEETING_LOCAL_STATE_JOINING,
  MEETING_LOCAL_STATE_RECONNECTING,
  MEETING_LOCAL_STATE_LEAVING,
  MEETING_LOCAL_STATE_ENDED,
  MEETING_ENDED_REASON_LABEL,
  MEETING_RECORDING_STATUS_RECORDING,
  MEETING_RECORDING_STATUS_UPLOADING,
  MEETING_RECORDING_STATUS_READY,
  MEETING_RECORDING_STATUS_FAILED,
  MEETING_RECORDING_STATUS_LABEL
} from '@/constants/meeting'

import VideoGrid from '@/components/meeting/VideoGrid.vue'
import VideoTile from '@/components/meeting/VideoTile.vue'
import MeetingToolbar from '@/components/meeting/MeetingToolbar.vue'
import MemberPanel from '@/components/meeting/MemberPanel.vue'
import InviteDialog from '@/components/meeting/InviteDialog.vue'
import NetworkBadge from '@/components/meeting/NetworkBadge.vue'
import ChatPanel from '@/components/meeting/ChatPanel.vue'
import SelfVideoFloat from '@/components/meeting/SelfVideoFloat.vue'
import {
  isCloudLawEmbed,
  isCloudLawLawRole,
  minimizeCloudLawMeeting,
  notifyCloudLawCallEnded,
  getMiniProgram
} from '@/utils/cloudLawMiniProgram'
import {
  isCloudLawLawRoleInMeeting,
  isCloudLawPcIframe,
  notifyCloudLawBackendEnd,
  notifyCloudLawParentCallEnded,
  queryCloudLawCallStatus
} from '@/utils/cloudLawVoiceCall'

const meetingStore = useMeetingStore()
const userStore = useUserStore()

/** 云律律师端：工具栏用共享屏幕替换聊天 */
const cloudLawRole = ref('')
const cloudLawCallId = ref('')
const cloudLawApiBase = ref('')
const minimizingToMiniProgram = ref(false)
const replaceChatWithScreenShare = computed(() => String(cloudLawRole.value).toLowerCase() === 'law')
const showCloudLawBack = computed(() => isCloudLawEmbed() && isCloudLawLawRole(cloudLawRole.value))

const memberVisible = ref(false)
const inviteVisible = ref(false)
const chatVisible = ref(false)
const leaveDialogVisible = ref(false)
const audioLoading = ref(false)
const videoLoading = ref(false)
const screenLoading = ref(false)
const chatLoadingMore = ref(false)
const networkLevel = ref(4) // 持续接入 getStats 后动态更新（Task 16 进一步收敛）
const startedAt = ref(0)
const nowTs = ref(Date.now())
let timerHandle = null
let viewportCleanup = null

/** 视频区容器引用：传给 SelfVideoFloat 计算吸附边界 */
const videoCol = ref(null)

/** 说话者映射由 meeting store 探测出（Task 15），浮窗和网格都订阅它来驱动流光动效 */
const speakingMap = computed(() => meetingStore.speakingMap)

/** 全员静音标记，用于 Task 15 工具栏冷色氛围（允许单人时恒为 false） */
const allMuted = computed(() => meetingStore.isAllMuted)

// ============ 基础映射 ============

const myUserId = computed(() => userStore.userInfo?.id || 0)
const isHost = computed(() => meetingStore.isHost)
/**
 * 修正 participants：本地用户的 audio_enabled/video_enabled 优先使用 store
 * 本地状态（localAudioEnabled/localVideoEnabled），因为后端不会给自己广播
 * member.state.changed，若只依赖 REST 详情易和实际操作脱节
 */
const participants = computed(() => {
  const uid = myUserId.value
  return meetingStore.activeParticipants.map(p => {
    if (p.user_id !== uid) return p
    return {
      ...p,
      audio_enabled: meetingStore.localAudioEnabled,
      video_enabled: meetingStore.localVideoEnabled
    }
  })
})
const hostId = computed(() => meetingStore.currentRoom?.host_id || 0)
const roomCode = computed(() => meetingStore.currentRoom?.room_code || '')
const meetingTitle = computed(() => meetingStore.currentRoom?.title || '会议')
const allowChat = computed(() => {
  const settings = meetingStore.currentRoom?.settings
  if (!settings) return true
  try {
    const parsed = typeof settings === 'string' ? JSON.parse(settings) : settings
    return parsed?.allow_chat !== false
  } catch {
    return true
  }
})
const invitePassword = computed(() => meetingStore.draftCreatePayload?.password || '')
const chatMessages = computed(() => meetingStore.chatMessages)
const chatHasMore = computed(() => meetingStore.chatHasMore)
const unreadChatCount = computed(() => meetingStore.chatUnreadCount)

const formattedCode = computed(() => {
  const d = String(roomCode.value || '').replace(/\D/g, '')
  if (d.length !== 9) return d
  return `${d.slice(0, 3)}-${d.slice(3, 6)}-${d.slice(6)}`
})

const joinLink = computed(() => {
  // #ifdef H5
  const base = `${window.location.origin}${window.location.pathname}`
  return `${base}#/pages/meeting/join?code=${roomCode.value}`
  // #endif
  // #ifndef H5
  return `echochat://meeting/${roomCode.value}`
  // #endif
})

// ============ 状态 pill ============

const stateClass = computed(() => {
  switch (meetingStore.localState) {
    case MEETING_LOCAL_STATE_CONNECTED: return 'state-connected'
    case MEETING_LOCAL_STATE_RECONNECTING: return 'state-reconnecting'
    case MEETING_LOCAL_STATE_CONNECTING:
    case MEETING_LOCAL_STATE_JOINING: return 'state-connecting'
    case MEETING_LOCAL_STATE_LEAVING: return 'state-leaving'
    case MEETING_LOCAL_STATE_ENDED: return 'state-ended'
    default: return 'state-idle'
  }
})
const stateText = computed(() => {
  switch (meetingStore.localState) {
    case MEETING_LOCAL_STATE_CONNECTED: return '已连接'
    case MEETING_LOCAL_STATE_RECONNECTING: return '重连中'
    case MEETING_LOCAL_STATE_CONNECTING:
    case MEETING_LOCAL_STATE_JOINING: return '连接中'
    case MEETING_LOCAL_STATE_LEAVING: return '正在离开'
    case MEETING_LOCAL_STATE_ENDED: return '已结束'
    default: return '等待中'
  }
})

// ============ 计时 ============

const durationText = computed(() => {
  if (!startedAt.value) return '00:00'
  const diff = Math.max(0, Math.floor((nowTs.value - startedAt.value) / 1000))
  const h = Math.floor(diff / 3600)
  const m = Math.floor((diff % 3600) / 60)
  const s = diff % 60
  const pad = (n) => (n < 10 ? `0${n}` : String(n))
  return h > 0 ? `${pad(h)}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`
})

// ============ 媒体状态 ============

const audioEnabled = computed(() => meetingStore.localAudioEnabled)
const videoEnabled = computed(() => meetingStore.localVideoEnabled)

// ============ Phase 3 屏幕共享 ============

/** 当前会议是否已有屏幕共享（任意成员） */
const screenSharingActive = computed(() => !!meetingStore.screenShare?.ownerUserId)
/** 屏幕共享者是否就是自己（决定工具栏按钮高亮 + 文案） */
const screenSharingByMe = computed(() => {
  const owner = meetingStore.screenShare?.ownerUserId
  return !!owner && owner === myUserId.value
})
/** H5 桌面浏览器才有 getDisplayMedia；非 H5 / 移动端 / 老内核都禁用按钮 */
const screenSupported = computed(() => {
  // #ifdef H5
  return typeof navigator !== 'undefined' &&
         !!navigator.mediaDevices &&
         typeof navigator.mediaDevices.getDisplayMedia === 'function'
  // #endif
  // #ifndef H5
  return false
  // #endif
})
/** 按钮禁用条件：平台不支持 / 已有他人在共享 */
const screenDisabled = computed(() => {
  if (!screenSupported.value) return true
  if (screenSharingActive.value && !screenSharingByMe.value) return true
  return false
})
const screenDisabledReason = computed(() => {
  if (!screenSupported.value) return '当前浏览器不支持屏幕共享'
  if (screenSharingActive.value && !screenSharingByMe.value) return '已有成员正在共享屏幕'
  return ''
})

/**
 * 屏幕共享大画面 tile：当前会议有屏幕共享时返回 tile 描述对象，否则 null
 *
 * track 来源分两路：
 *   - 自己共享：从 store.getLocalTrack('screen') 取本地 track
 *   - 他人共享：从 store.getRemoteTrack(ownerUid, 'screen') 取远端 Consumer track
 *
 * 触发重算：依赖 screenShare reactive + localTrackVersion + remoteTrackVersion
 */
const screenTile = computed(() => {
  const owner = meetingStore.screenShare?.ownerUserId
  if (!owner) return null
  // 触发依赖收集，让 store 内 producer/consumer 变化也能让本 computed 重算
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
  localTrackVersion.value
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
  remoteTrackVersion.value

  const isLocal = owner === myUserId.value
  const track = isLocal
    ? meetingStore.getLocalTrack('screen')
    : meetingStore.getRemoteTrack(owner, 'screen')
  // track 还没就绪时（screen.started 已到但 producer.new 未到 / consumer 未建好）暂不显示大画面，
  // 等下次重算（_onProducerNew 完成后 remoteTrackVersion 变化触发）
  if (!track) return null

  return {
    key: `screen_${owner}`,
    userId: owner,
    name: isLocal ? '你' : nameOf(owner),
    isLocal,
    isHost: owner === hostId.value,
    videoTrack: track
  }
})

/** 屏幕共享期间，底部条带显示参会者（含本地 camera tile） */
const stripTiles = computed(() => {
  if (!screenTile.value) return []
  // 直接复用 tiles 列表（含本地）；后续可按需限制数量
  return tiles.value
})

/** 监听 store.getLocalTrack 的变化：localProducers 变化即 bump 版本（含屏幕共享 producer） */
const localTrackVersion = computed(() => {
  const lp = meetingStore.localProducers
  return `${lp.audio || ''}-${lp.video || ''}-${lp.screen || ''}`
})

/** 远端 track 版本：让 tiles 在 slot.audio/slot.video/slot.screen 变化时重新计算 */
const remoteTrackVersion = computed(() => {
  const parts = []
  const map = meetingStore.remoteConsumers
  Object.keys(map).forEach((uid) => {
    const slot = map[uid]
    parts.push(`${uid}:${slot?.audio?.id || ''}:${slot?.video?.id || ''}:${slot?.screen?.id || ''}`)
  })
  return parts.join('|')
})

/** 参与者姓名缓存：user_name 来自后端 DTO */
const displayNameMap = computed(() => {
  const m = {}
  participants.value.forEach(p => {
    if (p.user_name) m[p.user_id] = p.user_name
  })
  const me = userStore.userInfo
  if (me?.id) m[me.id] = me.nickname || me.username || m[me.id]
  return m
})

const nameOf = (uid) => {
  const me = userStore.userInfo
  if (me?.id === uid) return me.nickname || me.username || `你`
  return displayNameMap.value[uid] || `用户 ${uid}`
}

/** 把 store 状态投影为 VideoGrid 需要的 tile 列表 */
const tiles = computed(() => {
  // 监听 track 版本让 computed 跟随远端/本地 track 变化重算
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
  localTrackVersion.value
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
  remoteTrackVersion.value

  const list = []
  const myUid = myUserId.value
  participants.value.forEach(p => {
    if (!p.is_active && p.is_active !== undefined) return
    const isLocal = p.user_id === myUid
    let audioTrack = null
    let videoTrack = null
    if (isLocal) {
      audioTrack = meetingStore.getLocalTrack('audio')
      videoTrack = meetingStore.getLocalTrack('video')
    } else {
      audioTrack = meetingStore.getRemoteTrack(p.user_id, 'audio')
      videoTrack = meetingStore.getRemoteTrack(p.user_id, 'video')
    }
    // Task 15 回归修复：tile 是否渲染 <video>/<audio> 以 track 实际存在为准
    // 参会者的 audio_enabled/video_enabled 仅作为 MemberPanel 状态图标色彩依据，
    // 不再决定 VideoTile 媒体元素的挂载；否则一旦 state 字段滞后就会屏蔽实际画面
    const tile = {
      key: `u_${p.user_id}`,
      userId: p.user_id,
      name: isLocal ? '你' : nameOf(p.user_id),
      isLocal,
      isHost: p.user_id === hostId.value,
      audioEnabled: isLocal ? audioEnabled.value : !!audioTrack,
      videoEnabled: isLocal ? videoEnabled.value : !!videoTrack,
      audioTrack,
      videoTrack
    }
    list.push(tile)
  })
  // 若本地用户尚未出现在 participants 里（极端情况：REST 延迟），兜底追加一个本地 tile
  if (myUid && !list.some(t => t.userId === myUid)) {
    list.unshift({
      key: `u_${myUid}`,
      userId: myUid,
      name: '你',
      isLocal: true,
      isHost: hostId.value === myUid,
      audioEnabled: audioEnabled.value,
      videoEnabled: videoEnabled.value,
      audioTrack: meetingStore.getLocalTrack('audio'),
      videoTrack: meetingStore.getLocalTrack('video')
    })
  }
  return list
})

// ============ Task 15 浮窗模式 ============

/**
 * 浮窗开关：从 store.uiPrefs 读取，默认桌面端开启浮窗
 * 点击图钉按钮时 togglePin 切换，本地 tile 会在浮窗 <-> 网格间迁移
 */
const floatEnabled = computed(() => meetingStore.uiPrefs?.selfVideoFloat !== false)

/**
 * 自视频 tile：浮窗模式下从 tiles 中剥离出来单独挂载
 * 若 floatEnabled=false 则返回 null，自视频仍保留在 Grid 里
 */
const selfTile = computed(() => {
  if (!floatEnabled.value) return null
  return tiles.value.find(t => t.isLocal) || null
})

/**
 * 网格 tiles：浮窗开启时剔除本地 tile（只渲染远端），否则保持原始
 * 这样 VideoGrid 的 layout-1/2/3/N 档位判断基于远端人数，符合用户认知
 */
const gridTiles = computed(() => {
  if (!floatEnabled.value) return tiles.value
  return tiles.value.filter(t => !t.isLocal)
})

const togglePin = () => {
  meetingStore.uiPrefs.selfVideoFloat = !meetingStore.uiPrefs.selfVideoFloat
}

// ============ 事件处理 ============

const onMicToggle = async () => {
  audioLoading.value = true
  try {
    if (audioEnabled.value) {
      await meetingStore.stopLocalAudio()
    } else {
      await meetingStore.startLocalAudio(meetingStore.devicePreview?.audioDeviceId)
    }
  } catch (err) {
    uni.showToast({ title: err?.message || '操作失败', icon: 'none' })
  } finally {
    audioLoading.value = false
  }
}

const onCamToggle = async () => {
  videoLoading.value = true
  try {
    if (videoEnabled.value) {
      await meetingStore.stopLocalVideo()
    } else {
      await meetingStore.startLocalVideo(meetingStore.devicePreview?.videoDeviceId)
    }
  } catch (err) {
    uni.showToast({ title: err?.message || '操作失败', icon: 'none' })
  } finally {
    videoLoading.value = false
  }
}

/**
 * 工具栏共享屏幕切换
 *
 * 状态机：
 *   - 未在共享 + 平台支持 + 无他人共享 → 调 startScreenShare
 *   - 自己已在共享 → 调 stopScreenShare
 *   - 其他场景由 MeetingToolbar.emitIf 拦截，不会进入本函数
 *
 * 容错：getDisplayMedia 用户取消会抛 NotAllowedError，对用户体验友好的提示是"已取消"
 */
const onScreenToggle = async () => {
  if (screenLoading.value) return
  screenLoading.value = true
  try {
    if (screenSharingByMe.value) {
      await meetingStore.stopScreenShare()
    } else {
      await meetingStore.startScreenShare()
    }
  } catch (err) {
    // NotAllowedError = 用户在系统选择器中点了取消，静默处理；其他错误才提示
    if (err?.name !== 'NotAllowedError') {
      uni.showToast({ title: err?.message || '屏幕共享失败', icon: 'none' })
    }
  } finally {
    screenLoading.value = false
  }
}

// ============ 录制（Phase B） ============

/** 当前是否处于"录制中"（按钮高亮 + 顶部红点） */
const isRecording = computed(() => meetingStore.recording.status === MEETING_RECORDING_STATUS_RECORDING)
/** 录制按钮 loading：REST 调用期间或后端异步上传期间均禁用 */
const recordingLoading = computed(() => meetingStore.recording.pending || meetingStore.recording.status === MEETING_RECORDING_STATUS_UPLOADING)
/** 录制中提示文案（VideoGrid 上方红点 hover/aria） */
const recordingHintText = computed(() => {
  const r = meetingStore.recording
  if (!r.status) return ''
  return MEETING_RECORDING_STATUS_LABEL[r.status] || r.status
})

/** 切换录制：仅 host；按钮 emitIf 已防抖，这里再做 try/catch + toast */
const onRecordingToggle = async () => {
  if (!isHost.value) return
  if (recordingLoading.value) return
  try {
    if (isRecording.value) {
      await meetingStore.stopRecording()
      uni.showToast({ title: '录制已停止，正在上传…', icon: 'none' })
    } else {
      await meetingStore.startRecording()
      uni.showToast({ title: '已开始录制', icon: 'success' })
    }
  } catch (err) {
    uni.showToast({ title: err?.message || '录制操作失败', icon: 'none' })
  }
}

/** 监听录制终态：ready → 提示已就绪；failed → 错误提示 */
watch(() => meetingStore.recording.status, (newStatus, oldStatus) => {
  if (newStatus === oldStatus) return
  if (newStatus === MEETING_RECORDING_STATUS_READY) {
    uni.showToast({ title: '录制已就绪', icon: 'success' })
  } else if (newStatus === MEETING_RECORDING_STATUS_FAILED) {
    const reason = meetingStore.recording.failureReason || '未知原因'
    uni.showToast({ title: `录制失败：${reason}`, icon: 'none', duration: 3000 })
  }
})

const openInvite = () => { inviteVisible.value = !inviteVisible.value }
const openMembers = () => { memberVisible.value = !memberVisible.value }
const openChat = () => {
  // 点击工具栏聊天按钮时进行切换：已打开 → 关闭；未打开 → 打开并标记已读
  if (chatVisible.value) {
    chatVisible.value = false
    return
  }
  chatVisible.value = true
  meetingStore.markChatsRead()
}
const closeChat = () => { chatVisible.value = false }

watch(unreadChatCount, (count) => {
  if (count > 0 && chatVisible.value) {
    meetingStore.markChatsRead()
  }
})

const onChatSend = async (content) => {
  try {
    await meetingStore.sendChat(content)
  } catch (err) {
    uni.showToast({ title: err?.message || '发送失败', icon: 'none' })
    throw err
  }
}

const onChatLoadMore = async () => {
  if (chatLoadingMore.value) return
  chatLoadingMore.value = true
  try {
    await meetingStore.loadMoreChats(30)
  } catch (err) {
    uni.showToast({ title: err?.message || '加载失败', icon: 'none' })
  } finally {
    chatLoadingMore.value = false
  }
}

const onChatRequestInitialLoad = async () => {
  if (meetingStore.chatHistoryLoaded) return
  await onChatLoadMore()
}

const onKickMember = async (uid) => {
  const confirmed = await new Promise(resolve => {
    uni.showModal({
      title: '踢出成员',
      content: `确认将 ${nameOf(uid)} 移出会议？`,
      success: (r) => resolve(r.confirm),
      fail: () => resolve(false)
    })
  })
  if (!confirmed) return
  try {
    await meetingStore.kickMember(uid)
    uni.showToast({ title: '已踢出', icon: 'success' })
  } catch (err) {
    uni.showToast({ title: err?.message || '操作失败', icon: 'none' })
  }
}

const onMuteMember = async ({ userId: uid, mute }) => {
  try {
    await meetingStore.muteMember(uid, mute)
    uni.showToast({ title: mute ? '已请对方静音' : '已请对方开麦', icon: 'none' })
  } catch (err) {
    uni.showToast({ title: err?.message || '操作失败', icon: 'none' })
  }
}

const onTransferHost = async (uid) => {
  const confirmed = await new Promise(resolve => {
    uni.showModal({
      title: '转让主持人',
      content: `确认将主持人身份转让给 ${nameOf(uid)}？`,
      success: (r) => resolve(r.confirm),
      fail: () => resolve(false)
    })
  })
  if (!confirmed) return
  try {
    await meetingStore.transferHost(uid)
    uni.showToast({ title: '已转让', icon: 'success' })
  } catch (err) {
    uni.showToast({ title: err?.message || '操作失败', icon: 'none' })
  }
}

const onCloudLawBack = () => {
  if (minimizingToMiniProgram.value) return
  minimizingToMiniProgram.value = true
  meetingStore.leave().catch(() => {})
  minimizeCloudLawMeeting(cloudLawCallId.value)
}

const onLeaveClick = () => {
  if (showCloudLawBack.value) {
    onCloudLawBack()
    return
  }
  leaveDialogVisible.value = true
}

const confirmLeaveSelf = async () => {
  leaveDialogVisible.value = false
  let failed = false
  try {
    await meetingStore.leave()
  } catch (err) {
    failed = true
    uni.showToast({ title: err?.message || '操作失败', icon: 'none' })
  } finally {
    leaveAndExit(failed ? '' : '已离开会议')
  }
}

const confirmEndOrLeave = async () => {
  leaveDialogVisible.value = false
  const tip = isHost.value ? '会议已结束' : '已离开会议'
  let failed = false
  try {
    if (isHost.value) {
      await meetingStore.endMeeting()
      // room.ended WS 广播会触发 store 内 _onRoomEnded 清理 state；保险起见再 leave 一次容错
      await meetingStore.leave().catch(() => {})
    } else {
      await meetingStore.leave()
    }
  } catch (err) {
    failed = true
    uni.showToast({ title: err?.message || '操作失败', icon: 'none' })
  } finally {
    leaveAndExit(failed ? '' : tip)
  }
}

// 退出会议视频界面：先给提示，再退出。
// 云律律师端「返回」→ 最小化并弹出「返回会议」；真正挂断/结束 → cloudLawCallEnded。
const leaveAndExit = (tip, ended = true) => {
  if (tip) {
    uni.showToast({ title: tip, icon: 'none', duration: 1200 })
  }
  setTimeout(() => redirectHome(ended), tip ? 600 : 800)
}

let meetingExited = false
const redirectHome = (ended = true) => {
  if (meetingExited) return
  if (isCloudLawEmbed() && isCloudLawLawRole(cloudLawRole.value)) {
    meetingExited = true
    const minimize = minimizingToMiniProgram.value || !ended
    if (minimize) {
      if (minimizeCloudLawMeeting(cloudLawCallId.value)) return
    } else if (notifyCloudLawCallEnded(cloudLawCallId.value)) {
      return
    }
    meetingExited = false
  }
  uni.reLaunch({ url: '/pages/index/index' })
}

const syncViewportHeight = () => {
  // #ifdef H5
  const height = window.visualViewport?.height || window.innerHeight
  if (height > 0) {
    document.documentElement.style.setProperty('--meeting-vh', `${height}px`)
  }
  // #endif
}

let cloudLawStatusPollTimer = null
let cloudLawParticipantStopWatch = null
let cloudLawSoloCheckTimer = null
let cloudLawConsultEnding = false
const hadRemoteParticipant = ref(false)

const getMyUserId = () => userStore.userInfo?.id || userStore.userInfo?.user_id

const countRemoteActiveParticipants = () => {
  const myUid = getMyUserId()
  return meetingStore.activeParticipants.filter(p => String(p.user_id) !== String(myUid)).length
}

const stopCloudLawStatusPoll = () => {
  if (cloudLawStatusPollTimer) {
    clearInterval(cloudLawStatusPollTimer)
    cloudLawStatusPollTimer = null
  }
}

const handleCloudLawConsultEnded = async (tip = '用户已退出咨询') => {
  if (cloudLawConsultEnding || !isCloudLawLawRoleInMeeting(cloudLawRole.value)) return
  cloudLawConsultEnding = true
  stopCloudLawStatusPoll()
  if (cloudLawSoloCheckTimer) {
    clearTimeout(cloudLawSoloCheckTimer)
    cloudLawSoloCheckTimer = null
  }
  try {
    if (cloudLawCallId.value) {
      await notifyCloudLawBackendEnd(cloudLawCallId.value, 'law', cloudLawApiBase.value)
      if (isCloudLawPcIframe()) {
        notifyCloudLawParentCallEnded(cloudLawCallId.value)
      } else if (getMiniProgram()) {
        notifyCloudLawCallEnded(cloudLawCallId.value)
      }
    }
    if (meetingStore.isInMeeting) {
      await meetingStore.leave().catch(() => {})
    }
    uni.showToast({ title: tip, icon: 'none' })
    if (!isCloudLawPcIframe() && !getMiniProgram()) {
      setTimeout(() => redirectHome(true), 800)
    }
  } finally {
    cloudLawConsultEnding = false
  }
}

const pollCloudLawCallStatusOnce = async () => {
  if (!isCloudLawLawRoleInMeeting(cloudLawRole.value) || !cloudLawCallId.value || cloudLawConsultEnding) return
  if (meetingStore.localState === MEETING_LOCAL_STATE_ENDED || meetingStore.localState === MEETING_LOCAL_STATE_LEAVING) return
  const data = await queryCloudLawCallStatus(cloudLawCallId.value, 'law', cloudLawApiBase.value)
  const status = String(data?.status || data?.callStatus || '').toLowerCase()
  if (['ended', 'cancelled', 'failed'].includes(status)) {
    await handleCloudLawConsultEnded('咨询已结束')
  }
}

const startCloudLawStatusPoll = () => {
  if (!isCloudLawLawRoleInMeeting(cloudLawRole.value) || !cloudLawCallId.value) return
  stopCloudLawStatusPoll()
  pollCloudLawCallStatusOnce()
  cloudLawStatusPollTimer = setInterval(pollCloudLawCallStatusOnce, 1000)
}

const scheduleCloudLawSoloCheck = () => {
  if (cloudLawSoloCheckTimer) {
    clearTimeout(cloudLawSoloCheckTimer)
  }
  cloudLawSoloCheckTimer = setTimeout(async () => {
    cloudLawSoloCheckTimer = null
    if (!hadRemoteParticipant.value || cloudLawConsultEnding) return
    if (meetingStore.localState !== MEETING_LOCAL_STATE_CONNECTED) return
    if (countRemoteActiveParticipants() === 0) {
      await handleCloudLawConsultEnded('用户已退出咨询')
    }
  }, 500)
}

// ============ 生命周期 ============

let stateStopWatch = null
let popstateHandler = null

// P2-3 修复：redirectTo 后必须 return，避免 onMounted 继续执行闪默认 UI
// 使用 module-scoped 标记，onMounted 读取后判断是否提前退出
let redirectingToJoin = false

onLoad((query) => {
  cloudLawRole.value = query?.cloudLawRole || query?.cloud_law_role || ''
  cloudLawCallId.value = query?.callId || query?.call_id || ''
  cloudLawApiBase.value = query?.cloudLawApiBase || query?.cloud_law_api_base || ''
  if (!cloudLawRole.value) {
    // #ifdef H5
    try {
      cloudLawRole.value = window.sessionStorage.getItem('echo_cloud_law_meeting_role') || ''
    } catch (e) {}
    // #endif
  }
  // 刷新页面 / 直链访问 room 时 store 为空，回跳 join 避免白屏
  if (!meetingStore.isInMeeting) {
    const paramCode = (query?.code || '').replace(/\D/g, '')
    redirectingToJoin = true
    uni.redirectTo({ url: `/pages/meeting/join${paramCode ? `?code=${paramCode}` : ''}` })
    return
  }
})

onMounted(() => {
  // P2-3 修复：onLoad 已 redirectTo 跳转时，避免本页继续初始化定时器与 watcher
  if (redirectingToJoin) return

  // #ifdef H5
  syncViewportHeight()
  const viewport = window.visualViewport
  const onViewportChange = () => syncViewportHeight()
  window.addEventListener('resize', onViewportChange)
  window.addEventListener('orientationchange', onViewportChange)
  viewport?.addEventListener?.('resize', onViewportChange)
  viewport?.addEventListener?.('scroll', onViewportChange)
  viewportCleanup = () => {
    window.removeEventListener('resize', onViewportChange)
    window.removeEventListener('orientationchange', onViewportChange)
    viewport?.removeEventListener?.('resize', onViewportChange)
    viewport?.removeEventListener?.('scroll', onViewportChange)
  }
  // #endif

  const startIso = meetingStore.currentRoom?.started_at || meetingStore.currentRoom?.created_at
  startedAt.value = startIso ? Date.parse(startIso) : Date.now()
  timerHandle = setInterval(() => { nowTs.value = Date.now() }, 1000)

  // 会议被主持人/后端结束（ENDED 状态）时，弹提示并回首页
  stateStopWatch = watch(() => meetingStore.localState, (s) => {
    if (s === MEETING_LOCAL_STATE_ENDED) {
      if (minimizingToMiniProgram.value) {
        minimizingToMiniProgram.value = false
        redirectHome(false)
        return
      }
      const reason = meetingStore.lastEndedReason
      const label = MEETING_ENDED_REASON_LABEL?.[reason] || '会议已结束'
      uni.showToast({ title: label, icon: 'none' })
      // 云律律师端（含 PC iframe）：用户挂断后同步云律后端 + 通知父页面释放浮窗
      if (isCloudLawLawRoleInMeeting(cloudLawRole.value) && cloudLawCallId.value) {
        notifyCloudLawBackendEnd(cloudLawCallId.value, 'law', cloudLawApiBase.value)
        if (isCloudLawPcIframe()) {
          notifyCloudLawParentCallEnded(cloudLawCallId.value)
          return
        }
        if (getMiniProgram()) {
          notifyCloudLawCallEnded(cloudLawCallId.value)
          return
        }
      }
      setTimeout(() => redirectHome(true), 1200)
    }
  })

  // 云律律师端：轮询通话状态 + 对方离会后自动收尾（用户挂断不一定能推到 room.ended）
  if (isCloudLawLawRoleInMeeting(cloudLawRole.value)) {
    startCloudLawStatusPoll()
    cloudLawParticipantStopWatch = watch(
      () => meetingStore.activeParticipants,
      () => {
        const remoteCount = countRemoteActiveParticipants()
        if (remoteCount > 0) {
          hadRemoteParticipant.value = true
          return
        }
        if (hadRemoteParticipant.value && remoteCount === 0) {
          scheduleCloudLawSoloCheck()
        }
      },
      { deep: true }
    )
  }

  // #ifdef H5
  if (showCloudLawBack.value) {
    try {
      history.pushState({ cloudLawMeeting: 1 }, '')
      popstateHandler = () => {
        if (!minimizingToMiniProgram.value) {
          onCloudLawBack()
        }
      }
      window.addEventListener('popstate', popstateHandler)
    } catch (e) {}
  }
  // #endif
})

onBeforeUnmount(() => {
  if (timerHandle) {
    clearInterval(timerHandle)
    timerHandle = null
  }
  if (viewportCleanup) { viewportCleanup(); viewportCleanup = null }
  if (stateStopWatch) { stateStopWatch(); stateStopWatch = null }
  stopCloudLawStatusPoll()
  if (cloudLawParticipantStopWatch) {
    cloudLawParticipantStopWatch()
    cloudLawParticipantStopWatch = null
  }
  if (cloudLawSoloCheckTimer) {
    clearTimeout(cloudLawSoloCheckTimer)
    cloudLawSoloCheckTimer = null
  }
  // #ifdef H5
  if (popstateHandler) {
    window.removeEventListener('popstate', popstateHandler)
    popstateHandler = null
  }
  // #endif
})

onUnload(() => {
  // 物理返回 / reLaunch 时兜底清理，避免 store 残留脏状态
  if (meetingStore.isInMeeting && meetingStore.localState !== MEETING_LOCAL_STATE_LEAVING) {
    meetingStore.leave().catch(() => {})
  }
  // Task 16 资源清理：若停留在"会议已结束"页面后物理返回，清空 pinia state
  // 避免 currentRoom / participants / chatMessages 等大对象常驻内存
  if (meetingStore.localState === MEETING_LOCAL_STATE_ENDED) {
    meetingStore.exitEndedRoom()
  }
})
</script>

<style scoped>
.room {
  /* 使用 fixed + 100vw/100vh 双重保险：
     某些祖先（uni-page-wrapper）带 transform 时 fixed 会相对该祖先而非视口；
     显式 100vw/100vh 可保证始终铺满整个浏览器窗口。 */
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  height: 100svh;
  height: 100dvh;
  height: var(--meeting-vh, 100dvh);
  min-width: 100vw;
  min-height: 100vh;
  min-height: 100svh;
  min-height: 100dvh;
  min-height: var(--meeting-vh, 100dvh);
  max-height: 100vh;
  max-height: 100svh;
  max-height: 100dvh;
  max-height: var(--meeting-vh, 100dvh);
  display: flex;
  flex-direction: column;
  background: #0F172A;
  color: #F3F4F6;
  /* 必须高于 CustomTabBar（z-index:999），否则从带 tabBar 的页面
     navigateTo 进会议室后，上一页遗留的 tabBar 会悬浮覆盖 toolbar
     导致静音/摄像头/邀请/成员/聊天按钮全部被拦截 */
  z-index: 1000;
}

.cloud-law-back {
  width: 64rpx;
  height: 64rpx;
  margin-right: 12rpx;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.12);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.cloud-law-back-icon {
  font-size: 44rpx;
  line-height: 1;
  color: #FFFFFF;
  font-weight: 300;
}
.top-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: calc(16rpx + env(safe-area-inset-top)) 24rpx 16rpx;
  gap: 16rpx;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0));
  position: relative;
  z-index: 5;
  flex-shrink: 0;
}

.top-left {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 6rpx;
  min-width: 0;
  flex: 1;
}
.top-left-info {
  display: flex;
  flex-direction: column;
  gap: 6rpx;
  min-width: 0;
  flex: 1;
}
.meeting-title {
  font-size: 28rpx;
  font-weight: 600;
  color: #F9FAFB;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.code-pill {
  display: inline-flex;
  align-items: center;
  gap: 8rpx;
  padding: 4rpx 14rpx;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 999rpx;
  color: #F3F4F6;
  font-size: 22rpx;
  cursor: pointer;
  transition: background-color 0.15s ease;
  width: fit-content;
}
.code-pill:hover { background: rgba(255, 255, 255, 0.2); }
.code-text {
  font-family: 'SF Mono', 'Menlo', monospace;
  letter-spacing: 1rpx;
}

.top-right {
  display: flex;
  align-items: center;
  gap: 12rpx;
  flex-shrink: 0;
}
.state-pill {
  display: inline-flex;
  align-items: center;
  gap: 6rpx;
  padding: 4rpx 12rpx;
  border-radius: 999rpx;
  font-size: 20rpx;
  background: rgba(16, 185, 129, 0.2);
  color: #86EFAC;
}
.state-pill.state-reconnecting {
  background: rgba(245, 158, 11, 0.25);
  color: #FBBF24;
}
.state-pill.state-connecting { background: rgba(59, 130, 246, 0.2); color: #93C5FD; }
.state-pill.state-leaving, .state-pill.state-ended {
  background: rgba(107, 114, 128, 0.3);
  color: #D1D5DB;
}
.state-dot {
  width: 12rpx;
  height: 12rpx;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 0 0 8rpx currentColor;
}
.state-pill.state-reconnecting .state-dot,
.state-pill.state-connecting .state-dot {
  animation: pulse 1.2s infinite ease-in-out;
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}
.timer {
  font-size: 22rpx;
  font-family: 'SF Mono', 'Menlo', monospace;
  color: rgba(255, 255, 255, 0.75);
  letter-spacing: 1rpx;
}

/* Phase B 录制状态指示器（顶栏） */
.recording-indicator {
  display: inline-flex;
  align-items: center;
  gap: 6rpx;
  padding: 4rpx 12rpx;
  border-radius: 999rpx;
  background: rgba(239, 68, 68, 0.18);
  border: 1rpx solid rgba(239, 68, 68, 0.55);
  color: #FCA5A5;
  font-size: 20rpx;
  font-weight: 600;
  letter-spacing: 1rpx;
}
.recording-indicator.uploading {
  background: rgba(245, 158, 11, 0.18);
  border-color: rgba(245, 158, 11, 0.55);
  color: #FCD34D;
}
.rec-dot {
  width: 12rpx;
  height: 12rpx;
  border-radius: 50%;
  background: #EF4444;
  animation: rec-blink 1s infinite ease-in-out;
}
.recording-indicator.uploading .rec-dot {
  background: #F59E0B;
}
@keyframes rec-blink {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.4; transform: scale(0.7); }
}

.body {
  flex: 1;
  min-height: 0;
  background: #0B1220;
  overflow: hidden;
  display: flex;
  flex-direction: row;
}
.video-col {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  overflow: hidden;
}

/* Phase 3 屏幕共享专属布局：大画面 + 底部横向条带 */
.video-col.has-screen { flex-direction: column; }
.screen-stage {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 12rpx;
  padding: 12rpx;
}
.screen-stage-tile {
  flex: 1;
  min-height: 0;
  background: #000;
  border-radius: 16rpx;
  overflow: hidden;
  /* 屏幕画面用 contain 而非 cover，避免桌面边缘内容被裁切 */
}
.screen-stage-tile :deep(video) {
  object-fit: contain !important;
  background: #000;
}
.screen-strip {
  flex-shrink: 0;
  height: 160rpx;
  display: flex;
  flex-direction: row;
  gap: 12rpx;
  overflow-x: auto;
  overflow-y: hidden;
}
.screen-strip .strip-cell {
  flex: 0 0 220rpx;
  height: 100%;
  border-radius: 12rpx;
  overflow: hidden;
  background: #111827;
}

.chat-col {
  width: 0;
  min-height: 0;
  flex-shrink: 0;
  overflow: hidden;
  transition: width 0.24s ease;
  border-left: 1rpx solid rgba(255, 255, 255, 0.06);
  background: #1F2937;
}
.chat-col.open { width: 640rpx; }

@media (max-width: 750px) {
  .body {
    padding-bottom: calc(142rpx + env(safe-area-inset-bottom));
    box-sizing: border-box;
  }
  /* 小屏幕：聊天面板改为全屏浮层，避免挤压视频 */
  .chat-col { position: fixed; top: 0; right: 0; bottom: 0; z-index: 205; }
  .chat-col.open { width: 100vw; }
}

/* 离会弹窗 */
.leave-mask {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 220;
  padding: 32rpx;
}
.leave-modal {
  width: 100%;
  max-width: 560rpx;
  background: #FFFFFF;
  color: #111827;
  border-radius: 24rpx;
  padding: 36rpx 32rpx 28rpx;
  box-shadow: 0 24rpx 48rpx rgba(0, 0, 0, 0.35);
}
.leave-title {
  font-size: 32rpx;
  font-weight: 600;
  margin-bottom: 16rpx;
}
.leave-desc {
  font-size: 26rpx;
  color: #4B5563;
  line-height: 1.6;
  margin-bottom: 32rpx;
}
.leave-actions {
  display: flex;
  gap: 16rpx;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.leave-btn {
  all: unset;
  cursor: pointer;
  padding: 18rpx 32rpx;
  border-radius: 12rpx;
  font-size: 26rpx;
  font-weight: 600;
  transition: background-color 0.12s ease;
  position: relative;
}
/* 同样抹除 uni-app H5 自动注入的 ::after 遮罩（inset: 0 -1200px -80px 0），
   否则 cancel 按钮的 ::after 会覆盖到右侧确认按钮区域，
   导致点击 "取消" 命中 "结束会议" 逻辑。 */
.leave-btn::after {
  content: none !important;
  display: none !important;
}
.leave-cancel {
  background: #F3F4F6;
  color: #374151;
}
.leave-cancel:hover { background: #E5E7EB; }
.leave-leave {
  background: #DBEAFE;
  color: #1D4ED8;
}
.leave-leave:hover { background: #BFDBFE; }
.leave-danger {
  background: #EF4444;
  color: #FFFFFF;
}
.leave-danger:hover { background: #DC2626; }
</style>
