<template>
  <div class="live-broadcast">
    <el-card class="live-broadcast-card">
      <template #header>
        <span>官网视频直播（WebRTC）</span>
      </template>
      <p class="status">{{ status }}</p>
      <p v-if="session" class="viewer-row">
        <el-tag type="info" effect="plain">当前观看人数：{{ viewerCount }}</el-tag>
      </p>
      <div class="form-block">
        <div class="field-row">
          <span class="field-label">画面来源</span>
          <el-radio-group v-model="captureMode" :disabled="!token || switchingCapture">
            <el-radio-button value="camera">仅摄像头</el-radio-button>
            <el-radio-button value="screen_only">仅共享屏幕</el-radio-button>
            <el-radio-button value="screen_pip">共享屏幕 + 摄像头</el-radio-button>
          </el-radio-group>
        </div>
        <div class="field-row">
          <span class="field-label">摄像头</span>
          <el-select
            v-model="selectedCameraId"
            placeholder="默认摄像头"
            clearable
            filterable
            style="width: 100%; max-width: 360px"
            :disabled="!token || switchingCapture"
          >
            <el-option label="系统默认" value="" />
            <el-option
              v-for="d in videoInputs"
              :key="d.deviceId"
              :label="d.label || `摄像头 ${d.deviceId.slice(0, 8)}…`"
              :value="d.deviceId"
            />
          </el-select>
          <el-button :disabled="!token" @click="refreshVideoDevices">刷新列表</el-button>
        </div>
        <div class="field-row">
          <span class="field-label">码率策略</span>
          <el-select
            v-model="bitrateProfile"
            style="width: 100%; max-width: 300px"
            :disabled="!token || switchingCapture"
          >
            <el-option label="省流优先（更多并发）" value="save" />
            <el-option label="均衡（推荐）" value="balanced" />
            <el-option label="清晰优先（更占带宽）" value="clarity" />
          </el-select>
          <el-tag effect="plain" type="info">弱网建议：省流/均衡</el-tag>
        </div>
        <p v-if="session" class="hint-live">
          直播中可切换三种模式；「屏幕+摄像头」下可拖动小窗，观众画面与预览一致（画布 16:9
          铺满）。共享整屏时尽量勿把本管理页选进画面，以免套娃。
        </p>
        <div v-if="session" class="field-row">
          <el-button
            type="primary"
            plain
            :disabled="!token || switchingCapture"
            :loading="switchingCapture"
            @click="applyCaptureSwitch"
          >
            应用切换（不切断直播）
          </el-button>
        </div>
      </div>
      <div class="actions">
        <el-button v-if="!session" type="primary" :disabled="!token" @click="start">开始直播</el-button>
        <el-button v-else type="danger" @click="stop">结束直播</el-button>
      </div>
      <div class="live-preview-layout">
        <div class="live-bw-slot">
          <el-card class="live-bw-panel" shadow="never">
            <template #header>
              <div class="live-bw-head">
                <span>带宽使用情况</span>
                <el-text v-if="bwUpdatedAt" type="info" size="small">{{ bwUpdatedAt }}</el-text>
              </div>
            </template>
            <template v-if="bandwidth">
              <div class="live-bw-metrics">
                <div class="live-bw-line">
                  <span class="live-bw-label">近 60 秒出站</span>
                  <strong class="live-bw-strong">{{ bandwidth.recent_egress_mbps }} Mbps</strong>
                  <span class="live-bw-sub">{{ formatBwBytes(bandwidth.bytes_out_last_60s) }} / 60s</span>
                </div>
                <div class="live-bw-line">
                  <span class="live-bw-label">近 60 秒入站</span>
                  <strong class="live-bw-strong">{{ bandwidth.recent_ingress_mbps }} Mbps</strong>
                  <span class="live-bw-sub">{{ formatBwBytes(bandwidth.bytes_in_last_60s) }} / 60s</span>
                </div>
                <div class="live-bw-line live-bw-line--split">
                  <span class="live-bw-mini">平均出站 {{ bandwidth.avg_egress_mbps }} Mbps</span>
                  <span class="live-bw-mini">运行 {{ formatBwUptime(bandwidth.uptime_seconds) }}</span>
                </div>
              </div>
              <p class="live-bw-footnote">
                为本 Go 进程 HTTP 粗估；前有 Nginx/CDN 时公网带宽可能更高。
              </p>
            </template>
            <el-text v-else type="info" size="small">统计加载中或暂不可用</el-text>
          </el-card>
        </div>
        <div ref="previewWrapRef" class="preview-wrap">
          <video
            v-show="previewLayout !== 'screen_pip'"
            ref="previewMainRef"
            class="preview-main"
            playsinline
            muted
            autoplay
          ></video>
          <video
            v-show="previewLayout === 'screen_pip'"
            ref="previewScreenRef"
            class="preview-main preview-main--fill"
            playsinline
            muted
            autoplay
          ></video>
          <video
            v-show="previewLayout === 'screen_pip'"
            ref="previewCamRef"
            class="preview-pip-drag"
            :style="pipStyle"
            playsinline
            muted
            autoplay
            title="拖动调整小窗位置（观众端同步）"
            @pointerdown.prevent="onPipPointerDown"
          ></video>
        </div>
        <aside class="live-moderation-aside" aria-label="观众与发言管控">
          <el-card class="moderation-card" shadow="never">
        <template #header>
          <div class="moderation-head">
            <span>观众与发言管控</span>
            <el-button size="small" @click="loadModeration">刷新</el-button>
          </div>
        </template>
        <div class="moderation-actions moderation-actions--stack">
          <el-switch
            v-model="muteAll"
            active-text="全体禁言"
            inactive-text="允许发言"
            @change="toggleMuteAll"
          />
          <div class="moderation-inline">
            <el-input v-model.trim="manualUsername" placeholder="按用户名禁言/解禁" style="width: 100%" />
            <el-button type="warning" plain @click="setManualUserMute(true)">禁言用户</el-button>
            <el-button @click="setManualUserMute(false)">解禁用户</el-button>
          </div>
          <div class="moderation-inline">
            <el-input v-model.trim="manualIP" placeholder="按 IP 禁言/解禁" style="width: 100%" />
            <el-button type="warning" plain @click="setManualIPMute(true)">禁言 IP</el-button>
            <el-button @click="setManualIPMute(false)">解禁 IP</el-button>
          </div>
        </div>
        <p v-if="mutedUsernames.length" class="muted-names-line">
          已禁用户（归一化）：
          <el-tag v-for="u in mutedUsernames" :key="u" size="small" type="warning" class="muted-name-tag">
            {{ u }}
          </el-tag>
        </p>
        <p class="moderation-hint">
          弹幕侧登记<strong>完整用户名</strong>便于对号入座；未登录弹幕连接为游客。同 IP 在
          {{ moderationRate.window_ms }}ms 内最多 {{ moderationRate.max_hits }} 次发送会限频。
        </p>
        <h4 class="moderation-subtitle">在线会话</h4>
        <el-table v-loading="moderationLoading" :data="onlineUsers" stripe size="small" class="moderation-table">
          <el-table-column label="用户名" min-width="100" show-overflow-tooltip>
            <template #default="{ row }">
              {{ formatSessionUsername(row) }}
            </template>
          </el-table-column>
          <el-table-column prop="channel" label="通道" width="72" />
          <el-table-column prop="ip" label="IP" min-width="120" show-overflow-tooltip />
          <el-table-column label="在线" width="72">
            <template #default="{ row }">{{ formatSec(row.online_sec) }}</template>
          </el-table-column>
          <el-table-column label="操作" width="168" fixed="right">
            <template #default="{ row }">
              <template v-if="row.username">
                <el-button link type="warning" size="small" @click="toggleUserMute(row.username, true)">
                  禁言
                </el-button>
                <el-button link type="primary" size="small" @click="toggleUserMute(row.username, false)">
                  解禁
                </el-button>
              </template>
              <el-button v-if="!ipMuted(row.ip)" link type="warning" size="small" @click="toggleIP(row.ip, true)">
                禁IP
              </el-button>
              <el-button v-else link type="primary" size="small" @click="toggleIP(row.ip, false)">解IP</el-button>
            </template>
          </el-table-column>
        </el-table>
        <h4 class="moderation-subtitle">按 IP 聚合</h4>
        <el-table v-loading="moderationLoading" :data="onlineIPs" stripe size="small" class="moderation-table">
          <el-table-column prop="ip" label="IP" min-width="120" show-overflow-tooltip />
          <el-table-column prop="count" label="连接" width="64" />
          <el-table-column label="状态" width="72">
            <template #default="{ row }">
              <el-tag :type="row.muted ? 'warning' : 'success'" size="small">
                {{ row.muted ? '禁' : '常' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="100" fixed="right">
            <template #default="{ row }">
              <el-button v-if="!row.muted" link type="warning" size="small" @click="toggleIP(row.ip, true)">
                禁言
              </el-button>
              <el-button v-else link type="primary" size="small" @click="toggleIP(row.ip, false)">解禁</el-button>
            </template>
          </el-table-column>
        </el-table>
          </el-card>
        </aside>
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { useAuthStore } from '../../stores/auth'
import { ElMessage } from 'element-plus'
import {
  getLiveModeration,
  getStats,
  setLiveMuteAll,
  setLiveMuteIP,
  setLiveMuteUser
} from '../../api/admin'
import { startPublishing } from '../../utils/liveWebRTC'

function liveStatusUrl() {
  const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '')
  return base ? `${base}/api/web/live/status` : '/api/web/live/status'
}

const authStore = useAuthStore()
const token = computed(() => authStore.getToken() || '')
const previewWrapRef = ref(null)
const previewMainRef = ref(null)
const previewScreenRef = ref(null)
const previewCamRef = ref(null)
const status = ref('就绪')
const session = ref(null)
const viewerCount = ref(0)
let viewerPollTimer = null
const captureMode = ref('camera')
const selectedCameraId = ref('')
const bitrateProfile = ref('balanced')
const videoInputs = ref([])
const switchingCapture = ref(false)
const moderationLoading = ref(false)
const muteAll = ref(false)
const onlineIPs = ref([])
const onlineUsers = ref([])
const manualIP = ref('')
const manualUsername = ref('')
const mutedUsernames = ref([])
const moderationRate = ref({ window_ms: 3000, max_hits: 10 })
let moderationTimer = null
/** @type {import('vue').Ref<Record<string, unknown> | null>} */
const bandwidth = ref(null)
const bwFetchAt = ref(0)
const bwUpdatedAt = computed(() => {
  if (!bwFetchAt.value) return ''
  return `更新于 ${new Date(bwFetchAt.value).toLocaleTimeString()}`
})

function formatBwBytes(n) {
  if (n == null || !Number.isFinite(Number(n)) || Number(n) < 0) return '—'
  const v = Number(n)
  if (v < 1024) return `${v} B`
  const u = ['KB', 'MB', 'GB', 'TB']
  let x = v
  let i = -1
  do {
    x /= 1024
    i++
  } while (x >= 1024 && i < u.length - 1)
  return `${x < 10 ? x.toFixed(2) : x.toFixed(1)} ${u[i]}`
}

function formatBwUptime(sec) {
  if (sec == null || !Number.isFinite(Number(sec)) || Number(sec) < 0) return '—'
  const s = Math.floor(Number(sec))
  const h = Math.floor(s / 3600)
  const m = Math.floor((s % 3600) / 60)
  const r = s % 60
  if (h > 0) return `${h} 小时 ${m} 分`
  if (m > 0) return `${m} 分 ${r} 秒`
  return `${r} 秒`
}

async function fetchBandwidth() {
  try {
    const res = await getStats()
    const bw = res?.bandwidth
    if (bw && typeof bw === 'object') {
      bandwidth.value = bw
      bwFetchAt.value = Date.now()
    }
  } catch (_) {
    /* 静默，不阻塞开播 */
  }
}

let bwPollTimer = null
/** @type {import('vue').Ref<'camera'|'screen_only'|'screen_pip'>} */
const previewLayout = ref('camera')

/** 与推流画布 1280×720 一致的归一化小窗矩形（左上 + 宽高，0~1） */
const pipNorm = ref(defaultPipNorm())

function defaultPipNorm() {
  const nw = 0.24
  const nh = 0.24
  return {
    nx: 1 - nw - 10 / 1280,
    ny: 1 - nh - 10 / 720,
    nw,
    nh
  }
}

const pipStyle = computed(() => ({
  left: `${pipNorm.value.nx * 100}%`,
  top: `${pipNorm.value.ny * 100}%`,
  width: `${pipNorm.value.nw * 100}%`,
  height: `${pipNorm.value.nh * 100}%`
}))

let pipDragging = false
let pipDragStart = { cx: 0, cy: 0, nx: 0, ny: 0 }

function clamp01(v, lo, hi) {
  return Math.min(hi, Math.max(lo, v))
}

function onPipPointerDown(e) {
  if (previewLayout.value !== 'screen_pip') return
  pipDragging = true
  pipDragStart.cx = e.clientX
  pipDragStart.cy = e.clientY
  pipDragStart.nx = pipNorm.value.nx
  pipDragStart.ny = pipNorm.value.ny
  try {
    e.target.setPointerCapture(e.pointerId)
  } catch (_) {}
  window.addEventListener('pointermove', onPipPointerMove)
  window.addEventListener('pointerup', onPipPointerUp, { once: true })
  window.addEventListener('pointercancel', onPipPointerUp, { once: true })
}

function onPipPointerMove(e) {
  if (!pipDragging || !previewWrapRef.value) return
  const rect = previewWrapRef.value.getBoundingClientRect()
  if (rect.width < 1 || rect.height < 1) return
  const dx = (e.clientX - pipDragStart.cx) / rect.width
  const dy = (e.clientY - pipDragStart.cy) / rect.height
  const { nw, nh } = pipNorm.value
  pipNorm.value = {
    ...pipNorm.value,
    nx: clamp01(pipDragStart.nx + dx, 0, 1 - nw),
    ny: clamp01(pipDragStart.ny + dy, 0, 1 - nh)
  }
}

function onPipPointerUp() {
  pipDragging = false
  window.removeEventListener('pointermove', onPipPointerMove)
}

async function refreshVideoDevices() {
  try {
    if (!navigator.mediaDevices?.enumerateDevices) return
    const list = await navigator.mediaDevices.enumerateDevices()
    videoInputs.value = list.filter((d) => d.kind === 'videoinput')
  } catch (_) {}
}

async function loadModeration() {
  moderationLoading.value = true
  try {
    const res = await getLiveModeration()
    muteAll.value = !!res.mute_all
    onlineIPs.value = Array.isArray(res.online_ips) ? res.online_ips : []
    onlineUsers.value = Array.isArray(res.online_users) ? res.online_users : []
    mutedUsernames.value = Array.isArray(res.muted_usernames) ? res.muted_usernames : []
    moderationRate.value = res.rate_limit || { window_ms: 3000, max_hits: 10 }
  } catch (e) {
    ElMessage.error(e?.response?.data?.error || '加载发言管控失败')
  } finally {
    moderationLoading.value = false
  }
}

function formatSec(s) {
  const v = Math.max(0, Number(s) || 0)
  if (v < 60) return `${v}s`
  const m = Math.floor(v / 60)
  const r = v % 60
  if (m < 60) return `${m}m${r}s`
  const h = Math.floor(m / 60)
  const mm = m % 60
  return `${h}h${mm}m`
}

async function toggleMuteAll(v) {
  try {
    await setLiveMuteAll(!!v)
    ElMessage.success(v ? '已开启全体禁言' : '已关闭全体禁言')
    await loadModeration()
  } catch (e) {
    ElMessage.error(e?.response?.data?.error || '操作失败')
    muteAll.value = !v
  }
}

async function toggleIP(ip, enabled) {
  try {
    await setLiveMuteIP(ip, enabled)
    ElMessage.success(enabled ? `已禁言 ${ip}` : `已解禁 ${ip}`)
    await loadModeration()
  } catch (e) {
    ElMessage.error(e?.response?.data?.error || '操作失败')
  }
}

async function setManualIPMute(enabled) {
  if (!manualIP.value) {
    ElMessage.warning('请先输入 IP')
    return
  }
  await toggleIP(manualIP.value, enabled)
}

async function toggleUserMute(username, enabled) {
  const u = (username || '').trim()
  if (!u) return
  try {
    await setLiveMuteUser(u, enabled)
    ElMessage.success(enabled ? `已禁言用户 ${u}` : `已解禁用户 ${u}`)
    await loadModeration()
  } catch (e) {
    ElMessage.error(e?.response?.data?.error || '操作失败')
  }
}

async function setManualUserMute(enabled) {
  if (!manualUsername.value.trim()) {
    ElMessage.warning('请先输入用户名')
    return
  }
  await toggleUserMute(manualUsername.value.trim(), enabled)
}

function applyPreview(payload) {
  const { layout, main, screen, cam } = payload || {}
  previewLayout.value = layout || 'camera'
  if (previewLayout.value === 'screen_pip') {
    if (previewMainRef.value) previewMainRef.value.srcObject = null
    if (previewScreenRef.value) previewScreenRef.value.srcObject = screen || null
    if (previewCamRef.value) previewCamRef.value.srcObject = cam || null
  } else {
    if (previewScreenRef.value) previewScreenRef.value.srcObject = null
    if (previewCamRef.value) previewCamRef.value.srcObject = null
    if (previewMainRef.value) previewMainRef.value.srcObject = main || null
  }
}

function clearPreview() {
  previewLayout.value = 'camera'
  pipNorm.value = defaultPipNorm()
  if (previewMainRef.value) previewMainRef.value.srcObject = null
  if (previewScreenRef.value) previewScreenRef.value.srcObject = null
  if (previewCamRef.value) previewCamRef.value.srcObject = null
}

async function fetchViewerCount() {
  try {
    const r = await fetch(liveStatusUrl(), { cache: 'no-store' })
    if (!r.ok) return
    const j = await r.json()
    if (typeof j.viewers === 'number') viewerCount.value = j.viewers
  } catch (_) {}
}

function startViewerPoll() {
  stopViewerPoll()
  fetchViewerCount()
  viewerPollTimer = window.setInterval(fetchViewerCount, 2500)
}

function stopViewerPoll() {
  if (viewerPollTimer != null) {
    clearInterval(viewerPollTimer)
    viewerPollTimer = null
  }
}

watch(session, (s) => {
  if (s) {
    startViewerPoll()
  } else {
    stopViewerPoll()
    viewerCount.value = 0
  }
})

async function applyCaptureSwitch() {
  if (!session.value?.switchMode) return
  switchingCapture.value = true
  try {
    await session.value.switchMode(captureMode.value, selectedCameraId.value || '')
  } finally {
    switchingCapture.value = false
  }
}

function start() {
  if (!token.value) {
    status.value = '请先登录'
    return
  }
  status.value = '正在连接…'
  const { stop, switchMode } = startPublishing({
    token: token.value,
    captureMode: captureMode.value,
    videoDeviceId: selectedCameraId.value || '',
    bitrateProfile: bitrateProfile.value,
    onStatus: (s) => {
      status.value = s
    },
    onLocalStream: applyPreview,
    onActiveModeChange: (m) => {
      captureMode.value = m
    },
    getPipRect: () => ({ ...pipNorm.value })
  })
  session.value = { stop, switchMode }
}

function stop() {
  session.value?.stop()
  session.value = null
  clearPreview()
  status.value = '已停止'
}

function onBeforeUnload() {
  session.value?.stop()
}

onMounted(() => {
  document.title = '视频直播开播 - 管理后台'
  window.addEventListener('beforeunload', onBeforeUnload)
  try {
    const v = localStorage.getItem('yh_live_bitrate_profile')
    bitrateProfile.value = v === 'save' || v === 'clarity' ? v : 'balanced'
  } catch (_) {}
  refreshVideoDevices()
  loadModeration()
  moderationTimer = window.setInterval(loadModeration, 5000)
  fetchBandwidth()
  bwPollTimer = window.setInterval(fetchBandwidth, 8000)
})

watch(bitrateProfile, (v) => {
  try {
    localStorage.setItem('yh_live_bitrate_profile', v)
  } catch (_) {}
})

onUnmounted(() => {
  stopViewerPoll()
  if (moderationTimer != null) {
    clearInterval(moderationTimer)
    moderationTimer = null
  }
  if (bwPollTimer != null) {
    clearInterval(bwPollTimer)
    bwPollTimer = null
  }
  window.removeEventListener('beforeunload', onBeforeUnload)
  window.removeEventListener('pointermove', onPipPointerMove)
})

onBeforeRouteLeave(() => {
  stop()
})
</script>

<style scoped>
.live-broadcast {
  max-width: min(1680px, 100%);
}
/* 右侧列：上为带宽卡片、下为观众管控；左侧为预览 */
.live-preview-layout {
  display: grid;
  grid-template-columns: 1fr clamp(280px, 34vw, 420px);
  grid-template-rows: auto auto;
  gap: 12px 16px;
  margin-top: 4px;
  width: 100%;
  align-items: start;
}
.live-bw-slot {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
}
.live-bw-panel :deep(.el-card__header) {
  padding: 10px 14px;
}
.live-bw-panel :deep(.el-card__body) {
  padding: 12px 14px 14px;
}
.live-bw-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 14px;
  font-weight: 600;
}
.live-bw-metrics {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.live-bw-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 12px;
  font-size: 13px;
}
.live-bw-line--split {
  justify-content: space-between;
  color: #606266;
  font-size: 12px;
}
.live-bw-label {
  color: #606266;
  min-width: 5.5em;
}
.live-bw-strong {
  color: #409eff;
  font-size: 15px;
}
.live-bw-sub {
  color: #909399;
  font-size: 12px;
}
.live-bw-mini {
  font-size: 12px;
}
.live-bw-footnote {
  margin: 10px 0 0;
  font-size: 11px;
  line-height: 1.45;
  color: #909399;
}
.preview-wrap {
  grid-column: 1;
  grid-row: 2;
}
.live-moderation-aside {
  grid-column: 2;
  grid-row: 2;
  width: 100%;
  min-width: 0;
  max-height: calc(100vh - 180px);
  overflow-y: auto;
  overflow-x: hidden;
  position: sticky;
  top: 8px;
  align-self: start;
}
/* 仅小屏/手机再上下堆叠 */
@media (max-width: 768px) {
  .live-preview-layout {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
  }
  .live-bw-slot {
    grid-column: 1;
    grid-row: 1;
  }
  .preview-wrap {
    grid-column: 1;
    grid-row: 2;
  }
  .live-moderation-aside {
    grid-column: 1;
    grid-row: 3;
    max-height: none;
    position: static;
    overflow-y: visible;
  }
}
.status {
  color: #409eff;
  margin-bottom: 14px;
  min-height: 1.5em;
}
.viewer-row {
  margin: -6px 0 14px;
}
.form-block {
  margin-bottom: 16px;
}
.field-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}
.field-label {
  font-size: 14px;
  color: #606266;
  min-width: 72px;
}
.hint-live {
  margin: 0 0 10px;
  font-size: 13px;
  line-height: 1.5;
  color: #909399;
  max-width: 720px;
}
.actions {
  margin-bottom: 16px;
}
.preview-wrap {
  position: relative;
  min-width: 0;
  width: 100%;
  max-width: none;
}
.preview-main {
  display: block;
  width: 100%;
  min-height: 320px;
  max-height: min(85vh, 900px);
  border-radius: 8px;
  background: #000;
  object-fit: contain;
  aspect-ratio: 16 / 9;
}
.preview-main--fill {
  object-fit: fill;
}
.preview-pip-drag {
  position: absolute;
  box-sizing: border-box;
  border-radius: 8px;
  border: 3px solid #409eff;
  background: #000;
  object-fit: cover;
  cursor: grab;
  touch-action: none;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
  z-index: 2;
}
.preview-pip-drag:active {
  cursor: grabbing;
}
.moderation-card {
  max-width: 100%;
}
.moderation-actions--stack {
  flex-direction: column;
  align-items: stretch;
}
.moderation-inline {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.muted-names-line {
  margin: 0 0 8px;
  font-size: 12px;
  color: #606266;
  line-height: 1.6;
}
.muted-name-tag {
  margin: 2px 4px 2px 0;
}
.moderation-table {
  width: 100%;
  margin-bottom: 8px;
}
.moderation-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.moderation-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}
.moderation-hint {
  margin: 0 0 10px;
  color: #909399;
  font-size: 12px;
}
.moderation-subtitle {
  margin: 16px 0 10px;
  font-size: 14px;
  color: #303133;
}
</style>
