<script setup lang="ts">
import { Capacitor } from '@capacitor/core'
import { Geolocation } from '@capacitor/geolocation'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import {
  Check,
  Clock,
  Download,
  Grid,
  Location,
  Plus,
  Refresh,
  Search,
  Upload,
  Warning,
} from '@element-plus/icons-vue'
import * as QRCode from 'qrcode'
import * as echarts from 'echarts/core'
import { LineChart, PieChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile, importExcel } from '../api/excel'
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'

echarts.use([
  LineChart,
  PieChart,
  GridComponent,
  LegendComponent,
  TooltipComponent,
  CanvasRenderer,
])

const loading = ref(false)
const detailLoading = ref(false)
const statisticsLoading = ref(false)
const terms = ref<any[]>([])
const tasks = ref<any[]>([])
const sheets = ref<any[]>([])
const selectedTask = ref<any>(null)
const selectedSheet = ref<any>(null)
const sheetDetail = ref<any>(null)
const statistics = ref<any>(null)
const createDialog = ref(false)
const qrDialog = ref(false)
const createSubmitting = ref(false)
const teacherLocationLoading = ref(false)
const qrDataUrl = ref('')
const qrCheckInUrl = ref('')
const qrChallengeExpiresAt = ref(0)
const isNativeApp = Capacitor.isNativePlatform()
const now = ref(Date.now())
const fileInput = ref<HTMLInputElement>()
const termId = ref<string>()
const taskKeyword = ref('')
const taskPage = ref(1)
const taskPageSize = 12
const taskTotal = ref(0)
const sheetKeyword = ref('')
const sheetStatus = ref<string>()
const sheetMethod = ref<string>()
const sheetPage = ref(1)
const sheetPageSize = 12
const sheetTotal = ref(0)
const activeMode = ref<'rollcall' | 'statistics'>('rollcall')
const studentKeyword = ref('')
const classFilter = ref('')
const attentionFilter = ref('')
const statusChartEl = ref<HTMLElement>()
const trendChartEl = ref<HTMLElement>()
const createForm = ref({
  name: '',
  attendanceDate: '',
  checkInMethod: 'Manual',
  checkInDurationMinutes: 15,
  targetLatitude: null as number | null,
  targetLongitude: null as number | null,
  locationRadiusMeters: 100,
  locationAccuracyMeters: null as number | null,
})
const chartInstances: echarts.ECharts[] = []
let clockTimer: number | undefined
let qrRefreshTimer: number | undefined

const statusOptions = [
  { value: 'Present', label: '出勤' },
  { value: 'Absent', label: '缺勤' },
  { value: 'Late', label: '迟到' },
  { value: 'Leave', label: '请假' },
  { value: 'Excused', label: '免修' },
]
const numericStatusNames: Record<number, string> = {
  1: 'Present', 2: 'Absent', 3: 'Late', 4: 'Leave', 5: 'Excused',
}
const statusLabels: Record<string, string> = Object.fromEntries(
  statusOptions.map(item => [item.value, item.label]),
)

const classOptions = computed(() => {
  const names = (statistics.value?.students ?? []).map((item: any) => item.className)
  return [...new Set<string>(names)].sort((a, b) => a.localeCompare(b, 'zh-CN'))
})

const filteredStudents = computed(() => {
  const keyword = studentKeyword.value.trim().toLocaleLowerCase()
  return (statistics.value?.students ?? []).filter((student: any) => {
    if (keyword) {
      const text = `${student.studentNumber} ${student.studentName}`.toLocaleLowerCase()
      if (!text.includes(keyword)) return false
    }
    if (classFilter.value && student.className !== classFilter.value) return false
    if (attentionFilter.value === 'abnormal' &&
        student.absentCount === 0 && student.lateCount === 0) return false
    if (attentionFilter.value === 'below90' &&
        (student.attendanceRate === null || student.attendanceRate >= 90)) return false
    return true
  })
})

async function loadTasks(resetPage = false) {
  loading.value = true
  try {
    if (resetPage) taskPage.value = 1
    const response = (await http.get('/attendance/my-tasks', {
      params: {
        academicTermId: termId.value || undefined,
        keyword: taskKeyword.value.trim() || undefined,
        page: taskPage.value,
        pageSize: taskPageSize,
      },
    })).data
    tasks.value = response.items
    taskTotal.value = response.total
    const preferred = tasks.value.find((item: any) => item.id === selectedTask.value?.id)
      ?? tasks.value[0]
    if (preferred) await selectTask(preferred)
    else {
      selectedTask.value = null
      sheets.value = []
      sheetTotal.value = 0
      statistics.value = null
    }
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    loading.value = false
  }
}

async function selectTask(task: any) {
  selectedTask.value = task
  selectedSheet.value = null
  sheetDetail.value = null
  statistics.value = null
  studentKeyword.value = ''
  classFilter.value = ''
  attentionFilter.value = ''
  sheetPage.value = 1
  sheetKeyword.value = ''
  sheetStatus.value = undefined
  sheetMethod.value = undefined
  await loadSheets()
  if (activeMode.value === 'statistics') await loadStatistics()
}

async function loadSheets(resetPage = false) {
  if (!selectedTask.value) return
  try {
    if (resetPage) sheetPage.value = 1
    const response = (await http.get('/attendance/sheets', {
      params: {
        teachingTaskId: selectedTask.value.id,
        keyword: sheetKeyword.value.trim() || undefined,
        status: sheetStatus.value,
        checkInMethod: sheetMethod.value,
        page: sheetPage.value,
        pageSize: sheetPageSize,
      },
    })).data
    sheets.value = response.items
    sheetTotal.value = response.total
    if (!sheets.value.some((sheet: any) => sheet.id === selectedSheet.value?.id)) {
      selectedSheet.value = null
      sheetDetail.value = null
    }
  } catch (error) {
    sheets.value = []
    sheetTotal.value = 0
    ElMessage.error(apiErrorMessage(error))
  }
}

async function selectSheet(sheet: any) {
  selectedSheet.value = sheet
  detailLoading.value = true
  try {
    sheetDetail.value = (await http.get(`/attendance/sheets/${sheet.id}`)).data
  } catch (error) {
    sheetDetail.value = null
    ElMessage.error(apiErrorMessage(error))
  } finally {
    detailLoading.value = false
  }
}

async function loadStatistics() {
  if (!selectedTask.value) return
  statisticsLoading.value = true
  try {
    statistics.value = (
      await http.get(`/attendance/tasks/${selectedTask.value.id}/statistics`)
    ).data
    await nextTick()
    renderStatisticsCharts()
  } catch (error) {
    statistics.value = null
    ElMessage.error(apiErrorMessage(error))
  } finally {
    statisticsLoading.value = false
  }
}

function openCreate() {
  createForm.value = {
    name: '',
    attendanceDate: new Date().toISOString().slice(0, 10),
    checkInMethod: 'Manual',
    checkInDurationMinutes: 15,
    targetLatitude: null,
    targetLongitude: null,
    locationRadiusMeters: 100,
    locationAccuracyMeters: null,
  }
  createDialog.value = true
}

async function captureTeacherLocation() {
  if (isNativeApp) {
    teacherLocationLoading.value = true
    try {
      let permission = await Geolocation.checkPermissions()
      if (permission.location !== 'granted') {
        permission = await Geolocation.requestPermissions({ permissions: ['location'] })
      }
      if (permission.location !== 'granted') {
        throw Object.assign(new Error('permission denied'), { code: 1 })
      }
      const position = await Geolocation.getCurrentPosition({
        enableHighAccuracy: true,
        timeout: 12000,
        maximumAge: 0,
      })
      applyTeacherLocation(position)
      return true
    } catch (error: any) {
      const message = error?.code === 1
        ? '定位权限被拒绝，请在系统设置中允许本应用获取精确位置。'
        : '暂时无法获取位置，请检查手机定位服务后重试。'
      ElMessage.error(message)
      return false
    } finally {
      teacherLocationLoading.value = false
    }
  }
  if (!navigator.geolocation) {
    ElMessage.error('当前电脑没有可用定位，请改用教师手机 App 发起定位签到。')
    return false
  }
  teacherLocationLoading.value = true
  try {
    const position = await new Promise<GeolocationPosition>((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(resolve, reject, {
        enableHighAccuracy: true,
        timeout: 12000,
        maximumAge: 0,
      })
    })
    applyTeacherLocation(position)
    return true
  } catch (error: any) {
    const message = error?.code === 1
      ? '定位权限被拒绝，请在浏览器地址栏中允许本网站使用位置信息。'
      : error?.code === 3
        ? '获取位置超时，请移到信号较好的位置后重试。'
        : '暂时无法获取位置，请检查系统定位服务。'
    ElMessage.error(message)
    return false
  } finally {
    teacherLocationLoading.value = false
  }
}

function applyTeacherLocation(position: {
  coords: { latitude: number; longitude: number; accuracy: number }
}) {
  createForm.value.targetLatitude = Number(position.coords.latitude.toFixed(7))
  createForm.value.targetLongitude = Number(position.coords.longitude.toFixed(7))
  createForm.value.locationAccuracyMeters = Math.round(position.coords.accuracy)
  ElMessage.success('已获取教师手机当前位置')
}

async function createSheet() {
  if (!createForm.value.name.trim()) {
    ElMessage.warning('请填写考勤表名称。')
    return
  }
  if (createForm.value.checkInMethod === 'Location' &&
      (createForm.value.targetLatitude === null ||
       createForm.value.targetLongitude === null) &&
      !await captureTeacherLocation()) return
  createSubmitting.value = true
  try {
    const { data } = await http.post('/attendance/sheets', {
      teachingTaskId: selectedTask.value.id,
      name: createForm.value.name,
      attendanceDate: new Date(createForm.value.attendanceDate).toISOString(),
      checkInMethod: createForm.value.checkInMethod,
      checkInDurationMinutes: createForm.value.checkInMethod === 'Manual'
        ? null
        : createForm.value.checkInDurationMinutes,
      targetLatitude: createForm.value.checkInMethod === 'Location'
        ? createForm.value.targetLatitude
        : null,
      targetLongitude: createForm.value.checkInMethod === 'Location'
        ? createForm.value.targetLongitude
        : null,
      locationRadiusMeters: createForm.value.checkInMethod === 'Location'
        ? createForm.value.locationRadiusMeters
        : null,
    })
    createDialog.value = false
    ElMessage.success(createForm.value.checkInMethod === 'Manual'
      ? '考勤表已建立'
      : '签到活动已发起')
    await selectTask(selectedTask.value)
    const createdSheet = sheets.value.find((sheet: any) => sheet.id === data.id)
    if (createdSheet) {
      await selectSheet(createdSheet)
      if (data.checkInMethod === 'QrCode') await showQrCode()
    }
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  } finally {
    createSubmitting.value = false
  }
}

function isOnlineMethod(method: string | number) {
  return method === 'QrCode' || method === 'Location' || method === 2 || method === 3
}

function methodLabel(method: string | number) {
  if (method === 'QrCode' || method === 2) return '扫码签到'
  if (method === 'Location' || method === 3) return '定位签到'
  return '教师点名'
}

function isQrCode(method: string | number) {
  return method === 'QrCode' || method === 2
}

function serverUtcTime(value: string | null | undefined) {
  if (!value) return Number.NaN
  const normalized = /(?:Z|[+-]\d{2}:\d{2})$/i.test(value) ? value : `${value}Z`
  return new Date(normalized).getTime()
}

function formatServerTime(value: string) {
  return new Date(serverUtcTime(value)).toLocaleTimeString('zh-CN', {
    hour: '2-digit',
    minute: '2-digit',
  })
}

function isCheckInOpen(sheet: any) {
  if (!sheet || !isDraft(sheet.status) || !isOnlineMethod(sheet.checkInMethod)) return false
  const start = serverUtcTime(sheet.checkInStartsAt)
  const end = serverUtcTime(sheet.checkInEndsAt)
  return start <= now.value && now.value < end
}

function remainingLabel(sheet: any) {
  if (!sheet?.checkInEndsAt) return ''
  const remaining = serverUtcTime(sheet.checkInEndsAt) - now.value
  if (remaining <= 0) return '签到已结束'
  const minutes = Math.floor(remaining / 60000)
  const seconds = Math.floor((remaining % 60000) / 1000)
  return `${minutes}分${String(seconds).padStart(2, '0')}秒后结束`
}

async function refreshQrCode(showError: boolean) {
  const sheet = sheetDetail.value?.sheet
  if (!sheet || !isCheckInOpen(sheet)) return false
  try {
    const { data } = await http.get(`/attendance/sheets/${sheet.id}/qr-challenge`)
    const publicBase = import.meta.env.VITE_PUBLIC_BASE_URL ?? window.location.origin
    qrCheckInUrl.value =
      `${publicBase}/my-attendance?token=${encodeURIComponent(data.token)}`
    qrDataUrl.value = await QRCode.toDataURL(qrCheckInUrl.value, {
      width: 360,
      margin: 2,
      errorCorrectionLevel: 'M',
      color: { dark: '#172b4d', light: '#ffffff' },
    })
    qrChallengeExpiresAt.value = serverUtcTime(data.expiresAt)
    return true
  } catch (error) {
    if (showError) ElMessage.error(apiErrorMessage(error))
    return false
  }
}

async function showQrCode() {
  stopQrRefresh()
  if (!await refreshQrCode(true)) return
  qrDialog.value = true
  qrRefreshTimer = window.setInterval(async () => {
    if (!qrDialog.value || !isCheckInOpen(sheetDetail.value?.sheet)) return
    await refreshQrCode(false)
  }, 10000)
}

function stopQrRefresh() {
  if (qrRefreshTimer) {
    window.clearInterval(qrRefreshTimer)
    qrRefreshTimer = undefined
  }
}

function qrChallengeRemainingLabel() {
  const seconds = Math.max(0, Math.ceil((qrChallengeExpiresAt.value - now.value) / 1000))
  return `${seconds} 秒内有效`
}

const riskLabels: Record<string, string> = {
  SharedDevice: '多人共用设备',
  HighFrequency: '高频尝试',
  RepeatedFailures: '多次失败',
  MissingDeviceId: '缺少设备标识',
}

function devicePlatformLabel(value: string | null | undefined) {
  if (value === 'android') return 'Android App'
  if (value === 'ios') return 'iOS App'
  if (value === 'web') return '浏览器'
  return value || '未知设备'
}

async function closeCheckIn() {
  if (!sheetDetail.value?.sheet) return
  try {
    await ElMessageBox.confirm(
      '提前结束后，学生将不能再扫码或定位签到，仍可由教师调整名单。',
      '提前结束签到',
      { type: 'warning', confirmButtonText: '结束签到', cancelButtonText: '继续签到' },
    )
    await http.post(`/attendance/sheets/${sheetDetail.value.sheet.id}/close-check-in`)
    ElMessage.success('签到已结束')
    qrDialog.value = false
    stopQrRefresh()
    await selectTask(selectedTask.value)
  } catch (error: any) {
    if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
  }
}

async function saveRecords() {
  if (!sheetDetail.value) return
  try {
    await http.put(`/attendance/sheets/${sheetDetail.value.sheet.id}/records`, {
      records: sheetDetail.value.sheet.records.map((record: any) => ({
        studentId: record.studentId,
        status: record.status,
        notes: record.notes,
      })),
    })
    ElMessage.success('考勤记录已保存')
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

async function submitSheet() {
  try {
    await http.post(`/attendance/sheets/${sheetDetail.value.sheet.id}/submit`)
    ElMessage.success('考勤表已提交')
    await selectTask(selectedTask.value)
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

async function removeSheet(sheet: any) {
  try {
    await ElMessageBox.confirm(`确定删除考勤表“${sheet.name}”吗？`, '删除考勤表', {
      type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消',
    })
    await http.delete(`/attendance/sheets/${sheet.id}`)
    ElMessage.success('考勤表已删除')
    await selectTask(selectedTask.value)
  } catch (error: any) {
    if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
  }
}

function chooseImportFile() {
  fileInput.value?.click()
}

async function handleImport(event: Event) {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  input.value = ''
  if (!file || !sheetDetail.value) return
  try {
    const { data } = await importExcel(
      `/attendance/sheets/${sheetDetail.value.sheet.id}/import`,
      file,
    )
    ElMessage.success(`已从 Excel 更新 ${data.updated} 条考勤记录`)
    await selectSheet(selectedSheet.value)
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

async function exportSheet() {
  if (!selectedSheet.value) return
  try {
    await downloadApiFile(
      `/attendance/sheets/${selectedSheet.value.id}/export.xlsx`,
      `考勤表-${selectedSheet.value.name}.xlsx`,
    )
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

async function exportStatistics() {
  if (!selectedTask.value) return
  try {
    await downloadApiFile(
      `/attendance/tasks/${selectedTask.value.id}/statistics.xlsx`,
      `课程考勤统计-${selectedTask.value.taskNumber}.xlsx`,
    )
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

function normalizeStatus(status: string | number) {
  return typeof status === 'number' ? numericStatusNames[status] : status
}

function statusLabel(status: string | number) {
  return statusLabels[normalizeStatus(status)] ?? '未知'
}

function isSubmitted(status: string | number) {
  return normalizeStatus(status) === 'Submitted' || status === 2
}

function isDraft(status: string | number) {
  return normalizeStatus(status) === 'Draft' || status === 1
}

function batchStatus(status: string) {
  if (!sheetDetail.value?.sheet?.records) return
  sheetDetail.value.sheet.records.forEach((record: any) => {
    record.status = status
  })
}

function countStatus(status: string) {
  return sheetDetail.value?.sheet?.records
    ?.filter((record: any) => normalizeStatus(record.status) === status).length ?? 0
}

function distributionClass(status: string | number) {
  return `status-${normalizeStatus(status).toLocaleLowerCase()}`
}

function formatRate(value: number | null | undefined) {
  return value === null || value === undefined ? '暂无' : `${value.toFixed(1)}%`
}

function rateClass(value: number | null) {
  if (value === null) return 'no-rate'
  if (value < 80) return 'low-rate'
  if (value < 90) return 'mid-rate'
  return 'good-rate'
}

function statusShare(count: number) {
  const total = statistics.value?.summary?.recordCount ?? 0
  return total > 0 ? `${count * 100 / total}%` : '0%'
}

function formatChartDate(value: string) {
  return new Intl.DateTimeFormat('zh-CN', {
    month: 'numeric',
    day: 'numeric',
  }).format(new Date(value))
}

function disposeCharts() {
  chartInstances.splice(0).forEach(chart => chart.dispose())
}

function renderStatisticsCharts() {
  disposeCharts()
  if (!statistics.value || statistics.value.summary.submittedSheetCount === 0) return

  const palette = ['#2d8975', '#b34e48', '#c78724', '#79579a', '#718096']
  if (statusChartEl.value) {
    const chart = echarts.init(statusChartEl.value)
    chart.setOption({
      color: palette,
      tooltip: {
        trigger: 'item',
        formatter: '{b}<br/>{c} 条 · {d}%',
      },
      legend: {
        bottom: 0,
        itemWidth: 10,
        itemHeight: 10,
        textStyle: { color: '#5f6b7a', fontSize: 11 },
      },
      series: [{
        name: '考勤状态',
        type: 'pie',
        radius: ['54%', '74%'],
        center: ['50%', '44%'],
        avoidLabelOverlap: true,
        label: {
          formatter: '{b}\n{d}%',
          color: '#344054',
          fontSize: 11,
          lineHeight: 16,
        },
        labelLine: { length: 10, length2: 8 },
        data: statistics.value.statusDistribution.map((item: any) => ({
          name: item.label,
          value: item.count,
        })),
      }],
    })
    chartInstances.push(chart)
  }

  if (trendChartEl.value) {
    const chart = echarts.init(trendChartEl.value)
    chart.setOption({
      color: ['#176b87'],
      tooltip: {
        trigger: 'axis',
        valueFormatter: (value: any) => value === null ? '暂无' : `${value}%`,
      },
      grid: { left: 42, right: 18, top: 18, bottom: 48 },
      xAxis: {
        type: 'category',
        boundaryGap: false,
        data: statistics.value.sessions.map((item: any) => formatChartDate(item.attendanceDate)),
        axisLine: { lineStyle: { color: '#d8dee7' } },
        axisLabel: { color: '#667085', fontSize: 10 },
      },
      yAxis: {
        type: 'value',
        min: 0,
        max: 100,
        interval: 25,
        axisLabel: { formatter: '{value}%', color: '#667085', fontSize: 10 },
        splitLine: { lineStyle: { color: '#edf0f4' } },
      },
      series: [{
        name: '出勤率',
        type: 'line',
        smooth: 0.25,
        symbolSize: 7,
        data: statistics.value.sessions.map((item: any) => item.attendanceRate),
        lineStyle: { width: 3 },
        areaStyle: { color: 'rgba(23, 107, 135, 0.10)' },
      }],
    })
    chartInstances.push(chart)
  }
}

function resizeCharts() {
  chartInstances.forEach(chart => chart.resize())
}

watch(activeMode, async mode => {
  if (mode === 'statistics' && selectedTask.value) {
    await loadStatistics()
  } else {
    disposeCharts()
  }
})

onMounted(async () => {
  window.addEventListener('resize', resizeCharts)
  clockTimer = window.setInterval(() => { now.value = Date.now() }, 1000)
  terms.value = (await http.get('/base-data/terms')).data
  termId.value = defaultAcademicTermId(terms.value)
  await loadTasks()
})

onUnmounted(() => {
  window.removeEventListener('resize', resizeCharts)
  if (clockTimer) window.clearInterval(clockTimer)
  stopQrRefresh()
  disposeCharts()
})
</script>

<template>
  <div class="page-stack attendance-page">
    <section class="page-intro">
      <div>
        <span class="section-kicker">ATTENDANCE</span>
        <h2>教学点名</h2>
        <p>完成课堂点名，并持续查看本课程每位学生的出勤表现。</p>
      </div>
      <el-button :icon="Refresh" @click="() => loadTasks()">刷新</el-button>
    </section>

    <section class="attendance-toolbar">
      <el-input
        v-model="taskKeyword"
        clearable
        placeholder="课程、任务号或教学班"
        @keyup.enter="loadTasks(true)"
        @clear="loadTasks(true)"
      />
      <el-select v-model="termId" clearable placeholder="全部学期" @change="loadTasks(true)">
        <el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
      </el-select>
      <el-button :icon="Search" @click="loadTasks(true)">查询</el-button>
      <span>共 {{ taskTotal }} 个教学班</span>
      <el-radio-group v-model="activeMode" class="mode-switch" size="small">
        <el-radio-button value="rollcall">点名记录</el-radio-button>
        <el-radio-button value="statistics">课程统计</el-radio-button>
      </el-radio-group>
    </section>

    <section
      class="attendance-workspace"
      :class="{ 'statistics-mode': activeMode === 'statistics' }"
      v-loading="loading"
    >
      <aside class="attendance-task-list">
        <button
          v-for="task in tasks"
          :key="task.id"
          type="button"
          :class="{ active: selectedTask?.id === task.id }"
          @click="selectTask(task)"
        >
          <span>{{ task.courseCode }} · {{ task.taskNumber }}</span>
          <b>{{ task.courseName }}</b>
          <small>{{ task.termName }} · {{ task.studentCount }} 名学生 · {{ task.sheetCount }} 张考勤表</small>
        </button>
        <el-empty v-if="!tasks.length" description="没有可点名的教学班" />
        <el-pagination
          v-if="taskTotal > taskPageSize"
          small
          background
          layout="prev, pager, next"
          :current-page="taskPage"
          :page-size="taskPageSize"
          :total="taskTotal"
          @current-change="(page: number) => { taskPage = page; loadTasks() }"
        />
      </aside>

      <template v-if="activeMode === 'rollcall'">
        <main class="attendance-detail">
          <template v-if="!selectedTask">
            <el-empty description="请选择一个教学班" />
          </template>
          <template v-else>
            <header class="attendance-subhead">
              <div>
                <strong>{{ selectedTask.courseName }}</strong>
                <span>{{ selectedTask.taskNumber }} · {{ selectedTask.studentCount }} 名学生</span>
              </div>
              <el-button type="primary" :icon="Plus" @click="openCreate">新建考勤表</el-button>
            </header>
            <div class="attendance-sheet-filters">
              <el-input
                v-model="sheetKeyword"
                clearable
                placeholder="考勤表名称或备注"
                @keyup.enter="loadSheets(true)"
                @clear="loadSheets(true)"
              />
              <el-select v-model="sheetStatus" clearable placeholder="全部状态" @change="loadSheets(true)">
                <el-option label="草稿" value="Draft" />
                <el-option label="已提交" value="Submitted" />
              </el-select>
              <el-select v-model="sheetMethod" clearable placeholder="全部方式" @change="loadSheets(true)">
                <el-option label="教师点名" value="Manual" />
                <el-option label="扫码签到" value="QrCode" />
                <el-option label="定位签到" value="Location" />
              </el-select>
              <el-button :icon="Search" @click="loadSheets(true)">筛选</el-button>
            </div>
            <div class="attendance-sheet-list">
              <button
                v-for="sheet in sheets"
                :key="sheet.id"
                type="button"
                :class="{ active: selectedSheet?.id === sheet.id }"
                @click="selectSheet(sheet)"
              >
                <div>
                  <b>
                    {{ sheet.name }}
                    <span
                      v-if="isOnlineMethod(sheet.checkInMethod)"
                      class="method-mark"
                    >{{ methodLabel(sheet.checkInMethod) }}</span>
                  </b>
                  <span>{{ new Date(sheet.attendanceDate).toLocaleDateString('zh-CN') }}</span>
                </div>
                <div class="sheet-stats">
                  <span
                    v-if="isOnlineMethod(sheet.checkInMethod)"
                    class="checked-in"
                  >已签到 {{ sheet.checkedInCount }}</span>
                  <span class="present">出勤 {{ sheet.presentCount }}</span>
                  <span class="absent" v-if="sheet.absentCount">缺勤 {{ sheet.absentCount }}</span>
                  <span class="late" v-if="sheet.lateCount">迟到 {{ sheet.lateCount }}</span>
                  <span class="leave" v-if="sheet.leaveCount">请假 {{ sheet.leaveCount }}</span>
                </div>
                <i>{{ isSubmitted(sheet.status) ? '已提交' : '草稿' }}</i>
                <el-button
                  v-if="isDraft(sheet.status)"
                  link
                  type="danger"
                  size="small"
                  @click.stop="removeSheet(sheet)"
                >删除</el-button>
              </button>
              <el-empty v-if="!sheets.length" description="尚未创建考勤表，点击右上方按钮新建" />
            </div>
            <el-pagination
              v-if="sheetTotal > sheetPageSize"
              small
              background
              layout="total, prev, pager, next"
              :current-page="sheetPage"
              :page-size="sheetPageSize"
              :total="sheetTotal"
              @current-change="(page: number) => { sheetPage = page; loadSheets() }"
            />
          </template>
        </main>

        <aside v-if="sheetDetail" class="attendance-panel" v-loading="detailLoading">
          <header>
            <div>
              <strong>{{ sheetDetail.sheet.name }}</strong>
              <span>{{ sheetDetail.sheet.courseCode }} · {{ sheetDetail.sheet.taskNumber }}</span>
              <small>
                {{ new Date(sheetDetail.sheet.attendanceDate).toLocaleDateString('zh-CN') }}
                · {{ methodLabel(sheetDetail.sheet.checkInMethod) }}
                · {{ isSubmitted(sheetDetail.sheet.status) ? '已提交' : '草稿' }}
              </small>
            </div>
            <div class="panel-actions">
              <el-button
                v-if="sheetDetail.canEdit && isQrCode(sheetDetail.sheet.checkInMethod)"
                size="small"
                :icon="Grid"
                @click="showQrCode"
              >显示签到码</el-button>
              <el-button
                v-if="sheetDetail.canEdit && isCheckInOpen(sheetDetail.sheet)"
                size="small"
                type="warning"
                plain
                @click="closeCheckIn"
              >提前结束</el-button>
              <el-button
                v-if="sheetDetail.canEdit"
                size="small"
                :icon="Upload"
                @click="chooseImportFile"
              >导入 Excel</el-button>
              <input
                ref="fileInput"
                class="visually-hidden"
                type="file"
                accept=".xlsx"
                @change="handleImport"
              />
              <el-button size="small" :icon="Download" @click="exportSheet">导出 Excel</el-button>
              <el-button
                v-if="sheetDetail.canEdit"
                type="primary"
                size="small"
                :icon="Check"
                @click="saveRecords"
              >保存</el-button>
              <el-button
                v-if="sheetDetail.canEdit"
                type="success"
                size="small"
                @click="submitSheet"
              >提交</el-button>
            </div>
          </header>
          <div
            v-if="isOnlineMethod(sheetDetail.sheet.checkInMethod)"
            class="check-in-console"
            :class="{ open: isCheckInOpen(sheetDetail.sheet) }"
          >
            <div class="check-in-signal">
              <span />
              {{ isCheckInOpen(sheetDetail.sheet) ? '签到进行中' : '签到已结束' }}
            </div>
            <strong>
              {{ sheetDetail.sheet.checkedInCount }}
              <small>/ {{ sheetDetail.sheet.records.length }} 人已自主签到</small>
            </strong>
            <p>
              <Clock />
              {{ remainingLabel(sheetDetail.sheet) }}
              <template v-if="sheetDetail.sheet.locationRadiusMeters">
                · 签到点 {{ sheetDetail.sheet.locationRadiusMeters }} 米内有效
              </template>
            </p>
            <p
              v-if="sheetDetail.sheet.riskSummary?.riskStudentCount"
              class="risk-summary"
            >
              <Warning />
              {{ sheetDetail.sheet.riskSummary.riskStudentCount }} 人存在签到风险信号，
              其中共用设备 {{ sheetDetail.sheet.riskSummary.sharedDeviceStudentCount }} 人
            </p>
          </div>
          <div v-if="sheetDetail.canEdit" class="batch-row">
            <span>批量设置：</span>
            <el-button size="small" @click="batchStatus('Present')">全部出勤</el-button>
            <el-button size="small" @click="batchStatus('Absent')">全部缺勤</el-button>
            <el-button size="small" @click="batchStatus('Late')">全部迟到</el-button>
            <small>出勤 {{ countStatus('Present') }} · 缺勤 {{ countStatus('Absent') }} · 迟到 {{ countStatus('Late') }} · 请假 {{ countStatus('Leave') }} · 免修 {{ countStatus('Excused') }}</small>
          </div>
          <el-table :data="sheetDetail.sheet.records" class="data-table" size="small">
            <el-table-column label="学号" width="120">
              <template #default="{ row }"><span class="registry-number">{{ row.studentNumber }}</span></template>
            </el-table-column>
            <el-table-column label="姓名" min-width="110">
              <template #default="{ row }">
                <span>{{ row.name }}</span>
                <el-tag v-if="row.isExempt" size="small" type="success" class="student-flag">免修</el-tag>
                <el-tag v-if="row.isDeferred" size="small" type="warning" class="student-flag">缓考</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="考勤" width="110">
              <template #default="{ row }">
                <el-select v-if="sheetDetail.canEdit" v-model="row.status" size="small">
                  <el-option
                    v-for="option in statusOptions"
                    :key="option.value"
                    :label="option.label"
                    :value="option.value"
                  />
                </el-select>
                <span v-else>{{ statusLabel(row.status) }}</span>
              </template>
            </el-table-column>
            <el-table-column
              v-if="isOnlineMethod(sheetDetail.sheet.checkInMethod)"
              label="自主签到"
              min-width="250"
            >
              <template #default="{ row }">
                <div v-if="row.checkInAt" class="record-check-in">
                  <b>{{ methodLabel(row.checkedInMethod) }}</b>
                  <span>{{ formatServerTime(row.checkInAt) }}</span>
                  <small v-if="row.checkInDistanceMeters !== null">
                    距签到点 {{ Math.round(row.checkInDistanceMeters) }} 米
                  </small>
                  <small v-if="row.checkInAudit">
                    {{ devicePlatformLabel(row.checkInAudit.devicePlatform) }}
                    · 设备 {{ row.checkInAudit.deviceCode || '未识别' }}
                  </small>
                  <small v-if="row.checkInAudit?.ipAddress">
                    IP {{ row.checkInAudit.ipAddress }}
                    <template v-if="row.checkInAudit.sameIpStudentCount > 1">
                      · 本次同 IP {{ row.checkInAudit.sameIpStudentCount }} 人
                    </template>
                  </small>
                  <div v-if="row.riskFlags?.length" class="check-in-risks">
                    <el-tag
                      v-for="flag in row.riskFlags"
                      :key="flag"
                      size="small"
                      type="danger"
                    >{{ riskLabels[flag] || flag }}</el-tag>
                  </div>
                  <small v-if="row.failedAttemptCount">
                    共尝试 {{ row.attemptCount }} 次，失败 {{ row.failedAttemptCount }} 次
                  </small>
                </div>
                <span v-else class="muted-cell">尚未签到</span>
              </template>
            </el-table-column>
            <el-table-column label="备注" min-width="120">
              <template #default="{ row }">
                <el-input
                  v-if="sheetDetail.canEdit"
                  v-model="row.notes"
                  size="small"
                  maxlength="300"
                />
                <span v-else>{{ row.notes || '—' }}</span>
              </template>
            </el-table-column>
          </el-table>
        </aside>
      </template>

      <main
        v-else
        class="attendance-statistics"
        v-loading="statisticsLoading"
      >
        <el-empty v-if="!selectedTask" description="请选择一个教学班查看统计" />
        <template v-else-if="statistics">
          <header class="statistics-head">
            <div>
              <span>课程出勤档案</span>
              <strong>{{ statistics.course.courseName }}</strong>
              <small>{{ statistics.course.courseCode }} · {{ statistics.course.taskNumber }} · {{ statistics.course.termName }}</small>
            </div>
            <el-button :icon="Download" type="primary" plain @click="exportStatistics">
              导出统计 Excel
            </el-button>
          </header>

          <section class="summary-ledger">
            <div class="rate-signal">
              <span>课程总体出勤率</span>
              <strong>{{ formatRate(statistics.summary.overallAttendanceRate) }}</strong>
              <small>出勤与迟到计为到课</small>
            </div>
            <div class="summary-facts">
              <div><b>{{ statistics.summary.studentCount }}</b><span>统计学生</span></div>
              <div><b>{{ statistics.summary.submittedSheetCount }}</b><span>已提交点名</span></div>
              <div><b>{{ statistics.summary.perfectAttendanceCount }}</b><span>全勤学生</span></div>
              <div><b>{{ statistics.summary.abnormalStudentCount }}</b><span>有缺勤或迟到</span></div>
            </div>
            <div class="status-rail" aria-label="考勤状态占比">
              <span
                v-for="item in statistics.statusDistribution"
                :key="item.status"
                :class="distributionClass(item.status)"
                :style="{ width: statusShare(item.count) }"
                :title="`${item.label} ${item.count} 条`"
              />
            </div>
            <p>仅统计已提交点名；免修不计入应到次数，请假计入应到但不计到课。</p>
          </section>

          <template v-if="statistics.summary.submittedSheetCount > 0">
            <section class="statistics-charts">
              <article>
                <header>
                  <strong>考勤状态构成</strong>
                  <span>累计 {{ statistics.summary.recordCount }} 条记录</span>
                </header>
                <div ref="statusChartEl" class="statistics-chart" />
              </article>
              <article>
                <header>
                  <strong>历次点名出勤率</strong>
                  <span>按点名日期顺序展示</span>
                </header>
                <div ref="trendChartEl" class="statistics-chart" />
              </article>
            </section>
          </template>
          <div v-else class="statistics-empty">
            <strong>还没有可统计的数据</strong>
            <span>提交第一张考勤表后，这里会生成状态分布和出勤率趋势。</span>
          </div>

          <section class="student-statistics">
            <header>
              <div>
                <strong>学生出勤明细</strong>
                <span>当前显示 {{ filteredStudents.length }} / {{ statistics.students.length }} 人，默认从低出勤率开始</span>
              </div>
              <div class="student-filters">
                <el-input
                  v-model="studentKeyword"
                  :prefix-icon="Search"
                  clearable
                  placeholder="搜索学号或姓名"
                />
                <el-select v-model="classFilter" clearable placeholder="全部行政班">
                  <el-option v-for="name in classOptions" :key="name" :label="name" :value="name" />
                </el-select>
                <el-select v-model="attentionFilter" clearable placeholder="全部学生">
                  <el-option label="有缺勤或迟到" value="abnormal" />
                  <el-option label="出勤率低于 90%" value="below90" />
                </el-select>
              </div>
            </header>
            <el-table :data="filteredStudents" size="small" max-height="520">
              <el-table-column prop="studentNumber" label="学号" width="128" fixed>
                <template #default="{ row }">
                  <span class="registry-number">{{ row.studentNumber }}</span>
                </template>
              </el-table-column>
              <el-table-column prop="studentName" label="姓名" width="105" fixed />
              <el-table-column prop="className" label="行政班" min-width="145" show-overflow-tooltip />
              <el-table-column label="出勤率" min-width="180" sortable :sort-method="(a: any, b: any) => (a.attendanceRate ?? 101) - (b.attendanceRate ?? 101)">
                <template #default="{ row }">
                  <div class="student-rate" :class="rateClass(row.attendanceRate)">
                    <div><span :style="{ width: `${row.attendanceRate ?? 0}%` }" /></div>
                    <b>{{ formatRate(row.attendanceRate) }}</b>
                  </div>
                </template>
              </el-table-column>
              <el-table-column prop="presentCount" label="出勤" width="72" sortable />
              <el-table-column prop="absentCount" label="缺勤" width="76" sortable>
                <template #default="{ row }">
                  <el-tag v-if="row.absentCount" size="small" type="danger">{{ row.absentCount }}</el-tag>
                  <span v-else>0</span>
                </template>
              </el-table-column>
              <el-table-column prop="lateCount" label="迟到" width="76" sortable>
                <template #default="{ row }">
                  <el-tag v-if="row.lateCount" size="small" type="warning">{{ row.lateCount }}</el-tag>
                  <span v-else>0</span>
                </template>
              </el-table-column>
              <el-table-column prop="leaveCount" label="请假" width="72" sortable />
              <el-table-column prop="excusedCount" label="免修" width="72" sortable />
              <el-table-column prop="requiredCount" label="应到" width="72" sortable />
            </el-table>
          </section>
        </template>
      </main>
    </section>

    <el-dialog v-model="createDialog" title="发起课堂考勤" width="620px">
      <el-form label-position="top">
        <el-form-item label="考勤名称" required>
          <el-input v-model="createForm.name" placeholder="如：第3周课堂点名" maxlength="120" />
        </el-form-item>
        <el-form-item label="考勤日期" required>
          <el-date-picker
            v-model="createForm.attendanceDate"
            value-format="YYYY-MM-DD"
            class="full-width"
          />
        </el-form-item>
        <el-form-item label="签到方式" required>
          <div class="method-options">
            <button
              v-for="method in [
                { value: 'Manual', icon: Check, name: '教师点名', hint: '逐人确认，可随时修改' },
                { value: 'QrCode', icon: Grid, name: '扫码签到', hint: '投屏二维码，学生登录后签到' },
                { value: 'Location', icon: Location, name: '定位签到', hint: '在指定签到点范围内签到' },
              ]"
              :key="method.value"
              type="button"
              :class="{ active: createForm.checkInMethod === method.value }"
              @click="createForm.checkInMethod = method.value"
            >
              <el-icon><component :is="method.icon" /></el-icon>
              <b>{{ method.name }}</b>
              <span>{{ method.hint }}</span>
            </button>
          </div>
        </el-form-item>
        <div v-if="createForm.checkInMethod !== 'Manual'" class="online-settings">
          <el-form-item label="签到时长" required>
            <el-input-number
              v-model="createForm.checkInDurationMinutes"
              :min="1"
              :max="180"
              :step="5"
              controls-position="right"
            />
            <span class="field-unit">分钟</span>
          </el-form-item>
          <template v-if="createForm.checkInMethod === 'Location'">
            <el-form-item label="有效范围" required>
              <el-input-number
                v-model="createForm.locationRadiusMeters"
                :min="20"
                :max="1000"
                :step="10"
                controls-position="right"
              />
              <span class="field-unit">米</span>
            </el-form-item>
            <div class="location-anchor">
              <div>
                <Location />
                <p>
                  <b>教师当前位置作为签到点</b>
                  <span v-if="createForm.targetLatitude !== null">
                    已定位，精度约 {{ createForm.locationAccuracyMeters }} 米
                  </span>
                  <span v-else>
                    {{ isNativeApp
                      ? '创建前需要允许 App 获取精确位置'
                      : '电脑无定位时，请改用教师手机 App 发起' }}
                  </span>
                </p>
              </div>
              <el-button
                :loading="teacherLocationLoading"
                @click="captureTeacherLocation"
              >{{ createForm.targetLatitude === null ? '获取位置' : '重新定位' }}</el-button>
            </div>
          </template>
        </div>
      </el-form>
      <template #footer>
        <el-button @click="createDialog = false">取消</el-button>
        <el-button
          type="primary"
          :loading="createSubmitting"
          @click="createSheet"
        >{{ createForm.checkInMethod === 'Manual' ? '建立考勤表' : '立即发起签到' }}</el-button>
      </template>
    </el-dialog>

    <el-dialog
      v-model="qrDialog"
      class="qr-dialog"
      title="课堂扫码签到"
      width="520px"
      align-center
      @closed="stopQrRefresh"
    >
      <div v-if="sheetDetail" class="qr-stage">
        <div class="qr-course">
          <span>{{ sheetDetail.sheet.courseCode }} · {{ sheetDetail.sheet.taskNumber }}</span>
          <strong>{{ sheetDetail.sheet.name }}</strong>
          <small>{{ sheetDetail.sheet.courseName }}</small>
        </div>
        <img :src="qrDataUrl" alt="课堂签到二维码" />
        <div class="qr-status" :class="{ ended: !isCheckInOpen(sheetDetail.sheet) }">
          <span />
          {{ isCheckInOpen(sheetDetail.sheet)
            ? `动态二维码 · ${qrChallengeRemainingLabel()}`
            : '本次签到已结束' }}
        </div>
        <p>二维码每 10 秒自动刷新，过期截图和旧链接不能签到</p>
      </div>
      <template #footer>
        <el-button
          v-if="sheetDetail && isCheckInOpen(sheetDetail.sheet)"
          type="warning"
          plain
          @click="closeCheckIn"
        >提前结束签到</el-button>
        <el-button type="primary" @click="qrDialog = false">完成</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<style scoped>
.attendance-toolbar {
  min-height: 56px;
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  border: 1px solid var(--line);
  background: #fbfcfd;
}
.attendance-toolbar .el-input,
.attendance-toolbar .el-select { width: 220px; }
.attendance-toolbar > span { color: var(--muted); font-size: 12px; }
.mode-switch { margin-left: auto; }
.attendance-workspace {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr) 420px;
  min-height: 500px;
  border: 1px solid var(--line);
  background: white;
}
.attendance-workspace.statistics-mode {
  grid-template-columns: 260px minmax(0, 1fr);
}
.attendance-task-list {
  border-right: 1px solid var(--line);
  overflow-y: auto;
  max-height: 720px;
}
.attendance-task-list button {
  display: grid;
  gap: 3px;
  width: 100%;
  padding: 12px 14px;
  border: none;
  border-bottom: 1px solid #edf0f4;
  background: none;
  cursor: pointer;
  text-align: left;
  transition: background .15s;
}
.attendance-task-list button:hover { background: #f5f7fa; }
.attendance-task-list button.active {
  padding-left: 11px;
  background: #e9f3f5;
  border-left: 3px solid #176b87;
}
.attendance-task-list button > span {
  color: var(--teal);
  font: 700 10px/1.2 Consolas, monospace;
}
.attendance-task-list button > b { font-size: 13px; }
.attendance-task-list button > small { color: var(--muted); font-size: 10px; }
.attendance-detail {
  display: flex;
  flex-direction: column;
  min-width: 0;
  border-right: 1px solid var(--line);
}
.attendance-subhead {
  padding: 14px 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  border-bottom: 1px solid var(--line);
  background: #f8fafb;
}
.attendance-subhead strong { font-size: 15px; }
.attendance-subhead span {
  color: var(--muted);
  font-size: 11px;
  display: block;
}
.attendance-sheet-list { flex: 1; overflow-y: auto; }
.attendance-sheet-filters {
  display: flex;
  gap: 8px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  background: #fbfcfd;
}
.attendance-sheet-filters .el-input { flex: 1; min-width: 140px; }
.attendance-sheet-filters .el-select { width: 115px; }
.attendance-task-list :deep(.el-pagination),
.attendance-detail :deep(.el-pagination) {
  justify-content: center;
  padding: 10px;
  border-top: 1px solid var(--line);
}
.attendance-sheet-list > button {
  width: 100%;
  padding: 12px 16px;
  border: none;
  border-bottom: 1px solid #edf0f4;
  background: none;
  cursor: pointer;
  text-align: left;
  display: grid;
  gap: 6px;
  transition: background .15s;
}
.attendance-sheet-list > button:hover { background: #f5f7fa; }
.attendance-sheet-list > button.active {
  padding-left: 13px;
  background: #eef5f0;
  border-left: 3px solid #2d8975;
}
.attendance-sheet-list > button > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}
.attendance-sheet-list > button b { font-size: 14px; }
.attendance-sheet-list > button span { color: var(--muted); font-size: 11px; }
.method-mark {
  margin-left: 5px;
  padding: 2px 5px;
  color: #176b87 !important;
  font-size: 9px !important;
  font-weight: 700;
  vertical-align: middle;
  border: 1px solid #b9d5dc;
  background: #edf7f8;
}
.attendance-sheet-list > button i {
  color: var(--indigo);
  font-size: 10px;
  font-weight: 700;
  font-style: normal;
}
.sheet-stats { display: flex; gap: 8px; flex-wrap: wrap; }
.sheet-stats span { font-size: 10px !important; font-weight: 700; }
.sheet-stats .present { color: #2d8975; }
.sheet-stats .checked-in { color: #176b87; }
.sheet-stats .absent { color: #b34e48; }
.sheet-stats .late { color: #c78724; }
.sheet-stats .leave { color: #79579a; }
.attendance-panel { overflow-y: auto; max-height: 720px; }
.attendance-panel > header {
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
  background: #f8fafb;
}
.attendance-panel > header strong { font-size: 15px; display: block; }
.attendance-panel > header span { color: var(--muted); font-size: 10px; }
.attendance-panel > header small {
  color: var(--indigo);
  font-size: 10px;
  display: block;
}
.panel-actions {
  display: flex;
  gap: 6px;
  margin-top: 8px;
  flex-wrap: wrap;
}
.check-in-console {
  padding: 13px 15px;
  display: grid;
  gap: 4px;
  color: #64748b;
  border-bottom: 1px solid #dce4ed;
  background: #f5f7fa;
}
.check-in-console.open {
  color: #d8f4ec;
  border-color: #21506d;
  background: #17395e;
}
.check-in-signal {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .08em;
}
.check-in-signal > span,
.qr-status > span {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #94a3b8;
}
.check-in-console.open .check-in-signal > span,
.qr-status:not(.ended) > span {
  background: #4fe0b1;
  box-shadow: 0 0 0 4px rgba(79, 224, 177, .14);
}
.check-in-console > strong {
  color: #334155;
  font: 700 30px/1.1 "Arial Narrow", "Microsoft YaHei", sans-serif;
}
.check-in-console.open > strong { color: white; }
.check-in-console > strong small {
  font-size: 11px;
  font-weight: 500;
}
.check-in-console > p {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 10px;
}
.check-in-console > p svg { width: 12px; }
.batch-row {
  padding: 8px 14px;
  display: flex;
  align-items: center;
  gap: 6px;
  border-bottom: 1px solid #edf0f4;
  background: #fff;
}
.batch-row > span { font-size: 11px; color: var(--muted); }
.batch-row > small { margin-left: auto; font-size: 10px; color: var(--muted); }
.student-flag { margin-left: 4px; }
.record-check-in { display: grid; gap: 1px; line-height: 1.25; }
.record-check-in b { color: #176b87; font-size: 10px; }
.record-check-in span,
.record-check-in small,
.muted-cell { color: var(--muted); font-size: 10px; }
.check-in-risks {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 3px;
}
.check-in-risks :deep(.el-tag) { height: 19px; font-size: 9px; }
.risk-summary { color: #ffdbb0 !important; font-weight: 700; }
.full-width { width: 100%; }

.method-options {
  width: 100%;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.method-options > button {
  min-width: 0;
  padding: 13px 10px;
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  gap: 2px 7px;
  color: #344054;
  text-align: left;
  border: 1px solid #d8dee7;
  background: white;
  cursor: pointer;
}
.method-options > button:hover { border-color: #87aeb9; }
.method-options > button.active {
  color: #17395e;
  border-color: #176b87;
  box-shadow: inset 0 -3px #176b87;
  background: #f1f8f9;
}
.method-options .el-icon {
  grid-row: 1 / 3;
  width: 28px;
  height: 28px;
  color: #176b87;
  font-size: 19px;
  background: #e7f2f4;
}
.method-options b { font-size: 12px; }
.method-options span {
  overflow: hidden;
  color: #7b8794;
  font-size: 9px;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.online-settings {
  padding: 13px 15px;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0 16px;
  border: 1px solid #dce4ed;
  background: #f7f9fb;
}
.online-settings :deep(.el-form-item) { margin-bottom: 10px; }
.field-unit { margin-left: 8px; color: var(--muted); font-size: 11px; }
.location-anchor {
  grid-column: 1 / -1;
  padding-top: 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  border-top: 1px solid #dce4ed;
}
.location-anchor > div {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 9px;
}
.location-anchor svg {
  width: 22px;
  color: #176b87;
  flex: 0 0 auto;
}
.location-anchor p { margin: 0; display: grid; }
.location-anchor b { color: #344054; font-size: 11px; }
.location-anchor span { color: var(--muted); font-size: 9px; }

.qr-stage {
  display: grid;
  justify-items: center;
  gap: 11px;
  text-align: center;
}
.qr-course { display: grid; gap: 2px; }
.qr-course span {
  color: #176b87;
  font: 700 10px/1.2 Consolas, monospace;
}
.qr-course strong { color: #172b4d; font-size: 20px; }
.qr-course small { color: var(--muted); }
.qr-stage > img {
  width: min(340px, 78vw);
  aspect-ratio: 1;
  border: 10px solid white;
  box-shadow: 0 0 0 1px #d8dee7, 0 12px 32px rgba(23, 43, 77, .12);
}
.qr-status {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #247261;
  font-size: 12px;
  font-weight: 800;
}
.qr-status.ended { color: #64748b; }
.qr-stage > p { margin: 0; color: var(--muted); font-size: 11px; }
.qr-stage :deep(.el-input) { width: 100%; }

.attendance-statistics {
  min-width: 0;
  overflow: hidden;
  background: #f7f9fb;
}
.statistics-head {
  min-height: 76px;
  padding: 14px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  border-bottom: 1px solid var(--line);
  background: white;
}
.statistics-head > div { display: grid; gap: 2px; }
.statistics-head span {
  color: #176b87;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .12em;
}
.statistics-head strong { color: #172b4d; font-size: 18px; }
.statistics-head small { color: var(--muted); font-size: 11px; }
.summary-ledger {
  display: grid;
  grid-template-columns: minmax(210px, .65fr) minmax(420px, 1.35fr);
  border-bottom: 1px solid var(--line);
  background: white;
}
.rate-signal {
  padding: 22px 24px;
  display: grid;
  align-content: center;
  border-right: 1px solid var(--line);
  background: #17395e;
  color: white;
}
.rate-signal span {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  opacity: .78;
}
.rate-signal strong {
  margin: 5px 0 1px;
  font: 700 42px/1.05 "Arial Narrow", "Microsoft YaHei", sans-serif;
  letter-spacing: -.04em;
}
.rate-signal small { font-size: 10px; opacity: .68; }
.summary-facts {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  min-width: 0;
}
.summary-facts > div {
  padding: 22px 15px 18px;
  display: grid;
  align-content: center;
  gap: 4px;
  border-right: 1px solid #edf0f4;
}
.summary-facts > div:last-child { border-right: none; }
.summary-facts b {
  color: #172b4d;
  font: 700 24px/1 "Arial Narrow", "Microsoft YaHei", sans-serif;
}
.summary-facts span { color: var(--muted); font-size: 10px; }
.status-rail {
  grid-column: 2;
  height: 6px;
  display: flex;
  overflow: hidden;
  background: #edf0f4;
}
.status-rail span { min-width: 0; transition: width .25s ease; }
.status-present { background: #2d8975; }
.status-absent { background: #b34e48; }
.status-late { background: #c78724; }
.status-leave { background: #79579a; }
.status-excused { background: #718096; }
.summary-ledger > p {
  grid-column: 1 / -1;
  margin: 0;
  padding: 8px 20px;
  color: #667085;
  font-size: 10px;
  border-top: 1px solid #edf0f4;
}
.statistics-charts {
  padding: 16px;
  display: grid;
  grid-template-columns: minmax(300px, .8fr) minmax(400px, 1.2fr);
  gap: 14px;
}
.statistics-charts article {
  min-width: 0;
  border: 1px solid var(--line);
  background: white;
}
.statistics-charts article > header {
  padding: 12px 15px 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.statistics-charts article > header strong { color: #172b4d; font-size: 13px; }
.statistics-charts article > header span { color: var(--muted); font-size: 10px; }
.statistics-chart { width: 100%; height: 270px; }
.statistics-empty {
  margin: 16px;
  min-height: 132px;
  display: grid;
  place-content: center;
  gap: 5px;
  text-align: center;
  border: 1px dashed #cbd3df;
  background: white;
}
.statistics-empty strong { color: #344054; font-size: 14px; }
.statistics-empty span { color: var(--muted); font-size: 11px; }
.student-statistics {
  margin: 0 16px 16px;
  border: 1px solid var(--line);
  background: white;
}
.student-statistics > header {
  padding: 13px 15px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  border-bottom: 1px solid var(--line);
}
.student-statistics > header > div:first-child { display: grid; gap: 2px; }
.student-statistics > header strong { color: #172b4d; font-size: 14px; }
.student-statistics > header span { color: var(--muted); font-size: 10px; }
.student-filters {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  flex-wrap: wrap;
}
.student-filters .el-input { width: 190px; }
.student-filters .el-select { width: 150px; }
.student-rate {
  display: grid;
  grid-template-columns: minmax(70px, 1fr) 48px;
  align-items: center;
  gap: 8px;
}
.student-rate > div {
  height: 5px;
  overflow: hidden;
  background: #edf0f4;
}
.student-rate > div > span {
  display: block;
  height: 100%;
  background: #2d8975;
}
.student-rate b {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.student-rate.low-rate b { color: #b34e48; }
.student-rate.low-rate > div > span { background: #b34e48; }
.student-rate.mid-rate b { color: #b06f17; }
.student-rate.mid-rate > div > span { background: #c78724; }
.student-rate.good-rate b { color: #247261; }
.student-rate.no-rate b { color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  .attendance-task-list button,
  .attendance-sheet-list > button,
  .status-rail span {
    transition: none;
  }
}

@media (max-width: 1100px) {
  .attendance-workspace,
  .attendance-workspace.statistics-mode {
    grid-template-columns: 1fr;
  }
  .attendance-task-list {
    max-height: 200px;
    border-right: none;
    border-bottom: 1px solid var(--line);
  }
  .attendance-detail { border-right: none; }
  .attendance-sheet-list { max-height: 240px; }
  .attendance-panel { max-height: 440px; }
  .summary-ledger { grid-template-columns: 220px minmax(0, 1fr); }
  .statistics-charts { grid-template-columns: 1fr 1fr; }
  .student-statistics > header { align-items: flex-start; flex-direction: column; }
  .student-filters { width: 100%; justify-content: flex-start; }
}

@media (max-width: 720px) {
  .attendance-toolbar { align-items: stretch; flex-wrap: wrap; }
  .attendance-toolbar .el-input,
  .attendance-toolbar .el-select { width: 100%; }
  .attendance-sheet-filters { flex-wrap: wrap; }
  .attendance-sheet-filters .el-input,
  .attendance-sheet-filters .el-select { width: 100%; flex-basis: 100%; }
  .attendance-toolbar > span { align-self: center; }
  .mode-switch { margin-left: auto; }
  .statistics-head { align-items: flex-start; flex-direction: column; }
  .summary-ledger { grid-template-columns: 1fr; }
  .rate-signal { border-right: none; }
  .summary-facts { grid-template-columns: repeat(2, 1fr); }
  .summary-facts > div:nth-child(2) { border-right: none; }
  .summary-facts > div:nth-child(-n + 2) { border-bottom: 1px solid #edf0f4; }
  .status-rail { grid-column: 1; }
  .statistics-charts { grid-template-columns: 1fr; padding: 12px; }
  .student-statistics { margin: 0 12px 12px; }
  .student-filters .el-input,
  .student-filters .el-select { width: 100%; }
  .method-options { grid-template-columns: 1fr; }
  .method-options > button { grid-template-columns: 32px minmax(0, 1fr); }
  .online-settings { grid-template-columns: 1fr; }
  .location-anchor { align-items: flex-start; }
  .batch-row { align-items: flex-start; flex-wrap: wrap; }
  .batch-row > small { width: 100%; margin-left: 0; }
}
</style>
