<template>
    <div class="reception-page">
        <!-- 页头 -->
        <div class="reception-header">
            <div class="header-left">
                <h1 class="header-title">接诊台</h1>
                <!-- <div class="header-desc">显示今日待接诊与已过号患者，左侧选人，右侧处理。</div> -->
                <div class="header-meta">{{ todayMeta }}</div>
            </div>
            <div class="header-right">
                <div class="live-pill">
                    <span class="live-dot" :class="{ paused: pollPaused }"></span>
                    <span>{{ pollPaused ? '已暂停' : `${countdown}s 后刷新` }}</span>
                </div>
            </div>
        </div>

        <!-- 筛选条 -->
        <div class="filter-shell">
            <el-input
                v-model="searchName"
                placeholder="请输入患者姓名"
                clearable
                size="default"
                class="search-input"
                @keyup.enter="handleSearch"
                @clear="handleSearch"
            >
                <template #prefix>
                    <el-icon><Search /></el-icon>
                </template>
            </el-input>
            <div class="filter-tabs">
                <span
                    class="filter-tab"
                    :class="{ active: activeStatus === 1 }"
                    @click="switchStatus(1)"
                >待接诊 {{ waitingTotal }} 人</span>
                <span class="filter-sep">·</span>
                <span
                    class="filter-tab"
                    :class="{ active: activeStatus === 4 }"
                    @click="switchStatus(4)"
                >已过号 {{ passedTotal }} 人</span>
            </div>
        </div>

        <!-- 主体 -->
        <div class="main-area">
            <!-- 左侧：待接诊队列 -->
            <aside class="queue-col">
                <div class="queue-panel">
                    <div class="queue-head">
                        <span class="queue-title">{{ activeStatus === 1 ? '待接诊' : '已过号' }}</span>
                        <span class="queue-meta">{{ total }} 人</span>
                    </div>
                    <div ref="queueScrollRef" v-loading="queueLoading && queue.length === 0" class="queue-list">
                        <div
                            v-for="row in queue"
                            :key="row.id"
                            class="queue-row"
                            :class="{ active: selectedId === row.id, passed: row.status === 4 }"
                            @click="selectRow(row)"
                        >
                            <div class="row-name">
                                <div class="name-line">
                                    {{ row.patient_name || '未命名' }}
                                    <span class="name-meta">{{ formatGenderAge(row) }}</span>
                                    <el-tag v-if="row.status === 4" size="small" type="warning" class="ml-1">已过号</el-tag>
                                </div>
                                <div class="time-line">
                                    {{ formatTimeHM(row.appointment_time) }} · 医助 {{ row.assistant_name || '—' }}
                                </div>
                            </div>
                            <div class="row-actions" @click.stop>
                                <el-tooltip content="通知医助（企业微信）" placement="top">
                                    <button
                                        class="row-icon-btn notify"
                                        :disabled="notifyingId === row.id"
                                        @click="handleNotify(row)"
                                    >
                                        <el-icon v-if="notifyingId === row.id" class="is-loading"><Loading /></el-icon>
                                        <el-icon v-else><ChatDotRound /></el-icon>
                                    </button>
                                </el-tooltip>
                                <el-tooltip content="发起通话" placement="top">
                                    <button class="row-icon-btn call" @click="handleCall(row)">
                                        <el-icon><Phone /></el-icon>
                                    </button>
                                </el-tooltip>
                            </div>
                        </div>

                        <div v-if="queue.length > 0" ref="sentinelRef" class="scroll-sentinel">
                            <span v-if="loadingMore">加载中…</span>
                            <span v-else-if="!hasMore" class="no-more">— 已全部加载 —</span>
                        </div>

                        <el-empty v-if="!queueLoading && queue.length === 0" description="暂无待接诊患者" :image-size="64" />
                    </div>
                </div>
            </aside>

            <!-- 右侧：患者信息 + 病例 + 血糖血压 -->
            <section class="sidebar" v-loading="detailLoading && !detail">
                <template v-if="!selectedId">
                    <div class="placeholder-card">
                        <el-empty description="请从左侧选择一位待接诊患者" />
                    </div>
                </template>

                <template v-else-if="detail">
                    <!-- 患者信息 -->
                    <PatientInfoCard :apt="apt" :diag="diag" />

                    <!-- 患者病例（与 tcm/diagnosis/readonly 共用同一组件，确保两处字段一致） -->
                    <PatientCaseCard :apt="apt" :diag="diag" />

                    <!-- 医生备注 & 舌苔照片 -->
                    <div class="card note-card">
                        <div class="card-title">医生备注 & 舌苔照片 & 检查报告</div>
                        <NoteTimeline
                            :notes="doctorNotes"
                            :diagnosis-id="diag?.id"
                            @refresh="handleDeskOperationRefresh"
                        />
                    </div>

                    <!-- 跟踪备注（只读时间轴；新增入口在「编辑诊单」内） -->
                    <div class="card tracking-note-card">
                        <div class="card-title">跟踪备注</div>
                        <TrackingNoteTimeline :notes="trackingNotes" readonly />
                    </div>

                    <!-- 跟踪信息（与只读病例页同口径，自治 lazy load） -->
                    <div class="card metric-card">
                        <div class="card-title">跟踪信息</div>
                        <TrackingMatrix
                            :diagnosis-id="Number(diag?.id ?? 0)"
                            :age="diag?.age ?? null"
                        />
                    </div>
                </template>
            </section>
        </div>

        <!-- 悬浮操作栏 -->
        <transition name="fade">
            <div v-if="selectedId && detail" class="float-bar">
                <el-button
                    v-perms="['doctor.appointment/addDoctorNote']"
                    class="float-action note"
                    size="large"
                    @click="showNoteDialog = true"
                >
                    备注
                </el-button>
                <el-button   
                    v-perms="['tcm.diagnosis/edit']"
                 class="float-action edit" 
                 size="large" 
                 @click="handleEditDiagnosis">
                    编辑病历
                </el-button>
                <el-button
                    v-if="apt.status === 1 || apt.status === 4"
                    v-perms="['doctor.appointment/complete']"
                    class="float-action finish"
                    size="large"
                    :loading="finishLoading"
                    @click="handleFinish"
                >
                    完成接诊
                </el-button>
            </div>
        </transition>

        <!-- 备注弹窗 -->
        <el-dialog v-model="showNoteDialog" title="添加备注" width="480px" append-to-body>
            <el-input v-model="noteContent" type="textarea" :rows="4" placeholder="输入今日备注..." maxlength="500" show-word-limit />
            <template #footer>
                <el-button @click="showNoteDialog = false">取消</el-button>
                <el-button type="primary" :loading="noteSaving" @click="handleSaveNote">保存</el-button>
            </template>
        </el-dialog>

        <!-- 复用：通话弹窗 + 诊单编辑 -->
        <component :is="ChatDialog" ref="chatDialogRef" />
        <component :is="DiagnosisEdit" ref="diagnosisEditRef" @success="onDiagnosisEditSuccess" />
    </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent, onMounted, onUnmounted } from 'vue'
import { receptionQueue, receptionDetail, notifyAssistant, addDoctorNote } from '@/api/patient'
import NoteTimeline from './components/NoteTimeline.vue'
import PatientInfoCard from '@/views/tcm/diagnosis/components/PatientInfoCard.vue'
import PatientCaseCard from '@/views/tcm/diagnosis/components/PatientCaseCard.vue'
import TrackingMatrix from '@/views/tcm/diagnosis/components/TrackingMatrix.vue'
import TrackingNoteTimeline from '@/views/tcm/diagnosis/components/TrackingNoteTimeline.vue'
import { completeAppointment } from '@/api/doctor'
import { getCallSignature } from '@/api/tcm'
import feedback from '@/utils/feedback'
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
import { formatGender } from '@/utils/diag-display'
import {
    Phone,
    Search,
    ChatDotRound,
    Loading
} from '@element-plus/icons-vue'

const ChatDialog = defineAsyncComponent(() => import('@/components/chat-dialog/index.vue'))
const DiagnosisEdit = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))

const POLL_MS = 5_000
const PAGE_SIZE = 15

interface QueueRow {
    id: number
    patient_id: number
    patient_name: string
    patient_phone?: string
    diagnosis_id?: number
    doctor_id?: number
    doctor_name?: string
    assistant_id?: number
    assistant_name?: string
    appointment_date?: string
    appointment_time?: string
    gender?: number
    age?: number | null
    status?: number
    status_desc?: string
    has_prescription?: number
    remark?: string
}

const activeStatus = ref<1 | 4>(1)
const queue = ref<QueueRow[]>([])
const total = ref(0)
const waitingTotal = ref(0)
const passedTotal = ref(0)
const page = ref(1)
const hasMore = ref(true)
const loadingMore = ref(false)
const queueLoading = ref(false)
const detailLoading = ref(false)
const selectedId = ref<number | null>(null)
const detail = ref<any>(null)
const finishLoading = ref(false)
const searchName = ref('')
const pollPaused = ref(false)
const countdown = ref(POLL_MS / 1000)
const notifyingId = ref<number | null>(null)
let pollTimer: ReturnType<typeof setInterval> | null = null
let countdownTimer: ReturnType<typeof setInterval> | null = null
/** 轮询请求未返回时的防重入标志，避免 5s 间隔下网络波动造成请求堆积 */
let queueInflight = false
let detailInflight = false
let scrollObserver: IntersectionObserver | null = null
let queueRequestSeq = 0
let detailRequestSeq = 0

const queueScrollRef = ref<HTMLElement | null>(null)
const sentinelRef = ref<HTMLElement | null>(null)
const chatDialogRef = ref()
const diagnosisEditRef = ref()
const showNoteDialog = ref(false)
const noteContent = ref('')
const noteSaving = ref(false)

const apt = computed<any>(() => detail.value?.appointment || {})
const diag = computed<any>(() => detail.value?.diagnosis || {})
const doctorNotes = computed<any[]>(() => detail.value?.doctor_notes || [])
const trackingNotes = computed<any[]>(() => detail.value?.tracking_notes || [])

const todayMeta = computed(() => {
    const d = new Date()
    const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][d.getDay()]
    const pad = (n: number) => String(n).padStart(2, '0')
    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${week}`
})


const todayStr = () => {
    const d = new Date()
    const pad = (n: number) => String(n).padStart(2, '0')
    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}

const formatGenderAge = (row: QueueRow) => {
    const parts = [formatGender(row.gender), row.age != null ? row.age + '岁' : '']
    return parts.filter((p) => p && p !== '—').join(' ')
}

const formatTimeHM = (time?: string) => {
    if (!time) return '—'
    return time.replace(/^(\d{2}:\d{2})(:\d{2})?$/, '$1')
}

const fetchQueuePage = async (pageNo: number, status?: number, patientName?: string) => {
    const today = todayStr()
    const res: any = await receptionQueue({
        status: status ?? activeStatus.value,
        start_date: today,
        end_date: today,
        patient_name: (patientName ?? searchName.value) || undefined,
        page_no: pageNo,
        page_size: PAGE_SIZE
    })
    const list: QueueRow[] = res?.lists || res?.data || []
    const count = Number(res?.count ?? res?.total ?? 0)
    return { list, count }
}

const fetchOtherCount = async (baseStatus: 1 | 4, patientName: string, requestSeq: number) => {
    const otherStatus = baseStatus === 1 ? 4 : 1
    try {
        const { count } = await fetchQueuePage(1, otherStatus, patientName || undefined)
        if (requestSeq !== queueRequestSeq || activeStatus.value !== baseStatus || searchName.value !== patientName) return
        if (otherStatus === 1) waitingTotal.value = count
        else passedTotal.value = count
    } catch { /* 统计计数失败不阻塞 */ }
}

const loadQueue = async (opts?: { silent?: boolean; autoSelect?: boolean }) => {
    const silent = opts?.silent === true
    if (silent && queueInflight) return
    const requestSeq = ++queueRequestSeq
    const statusSnapshot = activeStatus.value
    const searchSnapshot = searchName.value
    const autoSelect = opts?.autoSelect ?? !silent
    queueInflight = true
    if (!silent) queueLoading.value = true
    try {
        page.value = 1
        const { list, count } = await fetchQueuePage(1, statusSnapshot, searchSnapshot || undefined)
        if (requestSeq !== queueRequestSeq || activeStatus.value !== statusSnapshot || searchName.value !== searchSnapshot) return
        queue.value = list
        total.value = count || list.length
        if (statusSnapshot === 1) waitingTotal.value = count
        else passedTotal.value = count
        hasMore.value = list.length >= PAGE_SIZE && queue.value.length < (count || 0)

        void fetchOtherCount(statusSnapshot, searchSnapshot, requestSeq)

        if (selectedId.value && !list.some((r) => r.id === selectedId.value)) {
            selectedId.value = null
            detail.value = null
        }
        if (!selectedId.value && autoSelect && list.length > 0) {
            await selectRow(list[0])
        }
    } catch (e) {
        if (!silent) console.error('加载队列失败:', e)
    } finally {
        if (requestSeq === queueRequestSeq) {
            queueLoading.value = false
            queueInflight = false
        }
    }
}

const loadMore = async () => {
    if (loadingMore.value || !hasMore.value) return
    const statusSnapshot = activeStatus.value
    const searchSnapshot = searchName.value
    loadingMore.value = true
    try {
        const nextPage = page.value + 1
        const { list, count } = await fetchQueuePage(nextPage, statusSnapshot, searchSnapshot || undefined)
        if (activeStatus.value !== statusSnapshot || searchName.value !== searchSnapshot) return
        // 去重合并（防止轮询时上方有新插入导致重复）
        const existing = new Set(queue.value.map((r) => r.id))
        const appended = list.filter((r) => !existing.has(r.id))
        queue.value = [...queue.value, ...appended]
        page.value = nextPage
        total.value = count || total.value
        hasMore.value = list.length >= PAGE_SIZE && queue.value.length < (count || 0)
    } catch (e) {
        console.error('加载更多失败:', e)
    } finally {
        loadingMore.value = false
    }
}

const refreshDetailSilently = async () => {
    if (!selectedId.value || detailInflight) return
    const currentId = selectedId.value
    const requestSeq = ++detailRequestSeq
    detailInflight = true
    try {
        const res: any = await receptionDetail({ id: currentId })
        if (requestSeq !== detailRequestSeq || selectedId.value !== currentId) return
        detail.value = res || detail.value
    } catch (e) {
        /* 静默失败 */
    } finally {
        if (requestSeq === detailRequestSeq) detailInflight = false
    }
}

const selectRow = async (row: QueueRow) => {
    if (selectedId.value === row.id) return
    const currentId = row.id
    const requestSeq = ++detailRequestSeq
    selectedId.value = row.id
    detail.value = null
    detailLoading.value = true
    detailInflight = true
    restartPolling()
    try {
        const res: any = await receptionDetail({ id: currentId })
        if (requestSeq !== detailRequestSeq || selectedId.value !== currentId) return
        detail.value = res || null
    } catch (e) {
        console.error('加载详情失败:', e)
        feedback.msgError('加载患者详情失败')
    } finally {
        if (requestSeq === detailRequestSeq) {
            detailLoading.value = false
            detailInflight = false
        }
    }
}

const switchStatus = async (status: 1 | 4) => {
    if (activeStatus.value === status) return
    activeStatus.value = status
    selectedId.value = null
    detail.value = null
    await loadQueue()
    restartPolling()
}

const handleSearch = async () => {
    selectedId.value = null
    detail.value = null
    await loadQueue()
    restartPolling()
}

const handleCall = async (row: QueueRow) => {
    if (!row.patient_id) {
        feedback.msgWarning('患者信息不完整')
        return
    }
    try {
        const res = await getCallSignature({
            patient_id: row.patient_id,
            diagnosis_id: row.diagnosis_id || row.id
        })
        chatDialogRef.value?.open({
            patientId: row.patient_id,
            patientName: row.patient_name,
            diagnosisId: row.diagnosis_id || row.id,
            signatureData: res
        })
    } catch (error: any) {
        console.error('获取通话签名失败:', error)
        feedback.msgError(error?.message || '获取通话签名失败')
    }
}

const handleNotify = async (row: QueueRow) => {
    if (notifyingId.value) return
    notifyingId.value = row.id
    try {
        await notifyAssistant({ id: row.id })
        feedback.msgSuccess(`已通知医助 ${row.assistant_name || ''}`)
        await handleDeskOperationRefresh()
    } catch (e: any) {
        feedback.msgError(e?.message || '通知失败')
    } finally {
        notifyingId.value = null
    }
}

const handleEditDiagnosis = () => {
    if (!diag.value?.patient_id) return
    diagnosisEditRef.value?.open('edit', diag.value.patient_id)
}

const onDiagnosisEditSuccess = async () => {
    await handleDeskOperationRefresh()
}

const handleDeskOperationRefresh = async () => {
    const currentId = selectedId.value
    await loadQueue({ silent: true, autoSelect: false })
    if (currentId && selectedId.value === currentId) {
        await refreshDetailSilently()
    }
    restartPolling()
}

const handleFinish = async () => {
    if (!apt.value?.id) return
    try {
        await feedback.confirm('确认将该患者标记为「已完成接诊」？此操作不可撤销。')
    } catch {
        return
    }
    finishLoading.value = true
    try {
        await completeAppointment({ id: apt.value.id })
        feedback.msgSuccess('已完成接诊')
        selectedId.value = null
        detail.value = null
        await loadQueue({ autoSelect: true })
        restartPolling()
    } catch (e: any) {
        feedback.msgError(e?.message || '操作失败')
    } finally {
        finishLoading.value = false
    }
}

const handleSaveNote = async () => {
    const diagId = diag.value?.id
    if (!diagId) {
        feedback.msgWarning('当前患者无诊单，无法添加备注')
        return
    }
    const text = noteContent.value.trim()
    if (!text) {
        feedback.msgWarning('请输入备注内容')
        return
    }
    noteSaving.value = true
    try {
        await addDoctorNote({ diagnosis_id: diagId, content: text })
        feedback.msgSuccess('备注保存成功')
        noteContent.value = ''
        showNoteDialog.value = false
        await handleDeskOperationRefresh()
    } catch (e: any) {
        feedback.msgError(e?.message || '保存失败')
    } finally {
        noteSaving.value = false
    }
}

const resetCountdown = () => {
    countdown.value = POLL_MS / 1000
}

const startCountdown = () => {
    if (countdownTimer) clearInterval(countdownTimer)
    resetCountdown()
    countdownTimer = setInterval(() => {
        if (pollPaused.value) return
        if (countdown.value > 1) {
            countdown.value -= 1
        } else {
            countdown.value = 1
        }
    }, 1000)
}

const scheduleNextPoll = () => {
    if (pollTimer) clearTimeout(pollTimer)
    if (pollPaused.value) return
    startCountdown()
    pollTimer = setTimeout(async () => {
        await handleDeskOperationRefresh()
    }, POLL_MS)
}

const restartPolling = () => {
    scheduleNextPoll()
}

const startPolling = () => {
    stopPolling()
    scheduleNextPoll()
}

const stopPolling = () => {
    if (pollTimer) {
        clearTimeout(pollTimer)
        pollTimer = null
    }
    if (countdownTimer) {
        clearInterval(countdownTimer)
        countdownTimer = null
    }
}

const handleVisibilityChange = () => {
    pollPaused.value = document.hidden
    if (!document.hidden) {
        void handleDeskOperationRefresh()
    } else {
        stopPolling()
    }
}

/**
 * 浏览器扩展（如钱包/翻译插件）注入的 content script 在页面卸载后投递消息，
 * 常见报错 "Could not establish connection. Receiving end does not exist."
 * 与本应用无关，静默吞掉避免污染控制台。
 */
const onUnhandledRejection = (e: PromiseRejectionEvent) => {
    const msg = String(e?.reason?.message || e?.reason || '')
    if (/Receiving end does not exist|Could not establish connection/i.test(msg)) {
        e.preventDefault()
    }
}

const setupScrollObserver = () => {
    if (!sentinelRef.value || !queueScrollRef.value) return
    scrollObserver?.disconnect()
    scrollObserver = new IntersectionObserver(
        (entries) => {
            for (const entry of entries) {
                if (entry.isIntersecting) loadMore()
            }
        },
        { root: queueScrollRef.value, rootMargin: '80px', threshold: 0 }
    )
    scrollObserver.observe(sentinelRef.value)
}

watch(sentinelRef, () => setupScrollObserver())

onMounted(() => {
    loadQueue()
    startPolling()
    document.addEventListener('visibilitychange', handleVisibilityChange)
    window.addEventListener('unhandledrejection', onUnhandledRejection)
    nextTick(() => setupScrollObserver())
})

onUnmounted(() => {
    stopPolling()
    scrollObserver?.disconnect()
    document.removeEventListener('visibilitychange', handleVisibilityChange)
    window.removeEventListener('unhandledrejection', onUnhandledRejection)
})
</script>

<style scoped lang="scss">
.reception-page {
    --rx-bg: #f5f7fb;
    --rx-bg-grad: linear-gradient(180deg, #f5f7fb 0%, #eef2f8 100%);
    --rx-surface: #ffffff;
    --rx-ink: #1f2937;
    --rx-muted: #4b5563;
    --rx-soft: #94a3b8;
    --rx-line: #eef0f5;
    --rx-line-strong: #e2e6ee;
    --rx-accent: #4f8cff;
    --rx-accent-strong: #2563eb;
    --rx-accent-soft: #eef3ff;
    --rx-accent-glow: rgba(79, 140, 255, 0.18);
    --rx-success: #22c55e;
    --rx-warning: #f59e0b;
    --rx-warning-soft: #fff7ed;
    --rx-success-soft: #f0fdf4;
    --rx-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
    --rx-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
    --rx-shadow-lg: 0 8px 30px rgba(15, 23, 42, 0.08);
    --rx-font-data: 'IBM Plex Mono', Consolas, Menlo, monospace;

    min-height: calc(100vh - 80px);
    padding: 18px 22px 28px;
    background: var(--rx-bg-grad);
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* 页头 */
.reception-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 16px;
}
.header-title {
    margin: 0 0 4px;
    font-size: 24px;
    font-weight: 700;
    color: var(--rx-ink);
    letter-spacing: -0.02em;
    background: linear-gradient(120deg, #1f2937 0%, #4f8cff 120%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}
.header-desc {
    color: var(--rx-muted);
    font-size: 13px;
}
.header-meta {
    margin-top: 4px;
    color: var(--rx-soft);
    font-family: var(--rx-font-data);
    font-size: 12px;
}
.header-right {
    display: flex;
    gap: 10px;
    align-items: center;
}
.live-pill {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 7px 14px;
    border-radius: 999px;
    background: var(--rx-surface);
    border: 1px solid var(--rx-line);
    color: var(--rx-ink);
    font-family: var(--rx-font-data);
    font-size: 12px;
    min-width: 124px;
    justify-content: center;
    box-shadow: var(--rx-shadow-sm);
}
.live-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--rx-success);
    animation: pulse 1.6s infinite;
}
.live-dot.paused {
    background: var(--rx-soft);
    animation: none;
}
@keyframes pulse {
    0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5); }
    70% { box-shadow: 0 0 0 8px rgba(34, 197, 94, 0); }
    100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}

/* 筛选条 */
.filter-shell {
    background: var(--rx-surface);
    border: 1px solid var(--rx-line);
    border-radius: 14px;
    padding: 10px 16px;
    display: flex;
    align-items: center;
    gap: 16px;
    box-shadow: var(--rx-shadow-sm);
}
.search-input {
    width: 280px;
    border: 1px solid var(--rx-line);
    border-radius: 10px;
    background: #fff;
    transition: border-color 0.2s ease;
}
.search-input:hover {
    border-color: #cfd9ee;
}
.search-input:focus-within {
    border-color: var(--rx-accent);
}
.search-input :deep(.el-input__wrapper) {
    background: transparent;
    border-radius: 10px;
    border: none !important;
    box-shadow: none !important;
    outline: none !important;
}
.search-input :deep(.el-input__inner) {
    border: none !important;
    outline: none;
    box-shadow: none !important;
}
.filter-tabs {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
}
.filter-tab {
    color: var(--rx-soft);
    cursor: pointer;
    padding: 6px 14px;
    border-radius: 999px;
    transition: all 0.2s ease;
    user-select: none;
    font-weight: 500;
}
.filter-tab:hover {
    color: var(--rx-accent-strong);
    background: var(--rx-accent-soft);
}
.filter-tab.active {
    color: #fff;
    background: linear-gradient(135deg, #4f8cff 0%, #2563eb 100%);
    font-weight: 600;
    box-shadow: 0 2px 8px var(--rx-accent-glow);
}
.filter-sep {
    color: var(--rx-line-strong);
    font-size: 12px;
}

/* 主体 */
.main-area {
    display: flex;
    gap: 14px;
    flex: 1;
    min-height: 0;
    align-items: flex-start;
}
.queue-col {
    width: 312px;
    flex-shrink: 0;
}
.queue-panel {
    background: var(--rx-surface);
    border: 1px solid var(--rx-line);
    border-radius: 14px;
    padding: 14px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    height: calc(100vh - 170px);
    box-shadow: var(--rx-shadow-sm);
}
.queue-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 4px 10px;
    border-bottom: 1px solid var(--rx-line);
}
.queue-title {
    font-size: 15px;
    font-weight: 700;
    color: var(--rx-ink);
    display: flex;
    align-items: center;
    gap: 8px;
}
.queue-title::before {
    content: '';
    width: 3px;
    height: 14px;
    border-radius: 2px;
    background: linear-gradient(180deg, #4f8cff 0%, #2563eb 100%);
}
.queue-meta {
    color: var(--rx-muted);
    font-family: var(--rx-font-data);
    font-size: 12px;
    padding: 2px 10px;
    background: var(--rx-accent-soft);
    color: var(--rx-accent-strong);
    border-radius: 999px;
    font-weight: 600;
}
.queue-list {
    flex: 1;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-right: 2px;
    padding-bottom: 4px;
}
.queue-list::-webkit-scrollbar {
    width: 6px;
}
.queue-list::-webkit-scrollbar-thumb {
    background: var(--rx-line-strong);
    border-radius: 3px;
}
.queue-list::-webkit-scrollbar-thumb:hover {
    background: var(--rx-soft);
}
.queue-row {
    position: relative;
    border: 1px solid var(--rx-line);
    border-radius: 12px;
    padding: 14px 14px 14px 18px;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    cursor: pointer;
    transition: all 0.2s ease;
    background: #fff;
    overflow: hidden;
    min-height: 84px;
    gap: 12px;
}
.queue-row::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--rx-accent);
    opacity: 0;
    transition: opacity 0.2s ease;
}
.queue-row:hover {
    border-color: #cfd9ee;
    background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
    transform: translateY(-1px);
    box-shadow: var(--rx-shadow);
}
.queue-row:hover::before {
    opacity: 1;
    background: #b9c8ea;
}
.queue-row.active {
    background: linear-gradient(135deg, #eef4ff 0%, #e4edff 100%);
    border-color: var(--rx-accent);
    box-shadow: 0 4px 14px var(--rx-accent-glow);
}
.queue-row.active:hover {
    border-color: var(--rx-accent);
    background: linear-gradient(135deg, #ebf2ff 0%, #dde9ff 100%);
}
.queue-row.active::before {
    opacity: 1;
    background: linear-gradient(180deg, #4f8cff 0%, #2563eb 100%);
}
.queue-row.passed {
    opacity: 0.82;
}
.queue-row.passed:hover {
    background: linear-gradient(180deg, #fffdfa 0%, #fff7ed 100%);
    border-color: #f5d7a6;
}
.queue-row.passed.active {
    background: linear-gradient(135deg, #fff7eb 0%, #ffefcf 100%);
    border-color: var(--rx-warning);
    box-shadow: 0 4px 14px rgba(245, 158, 11, 0.18);
}
.queue-row.passed::before {
    background: var(--rx-warning);
}
.queue-row.passed:hover::before,
.queue-row.passed.active::before {
    opacity: 1;
}
.row-name {
    flex: 1;
    min-width: 0;
    padding-top: 1px;
}
.name-line {
    font-size: 14px;
    font-weight: 600;
    color: var(--rx-ink);
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    line-height: 1.45;
}
.name-meta {
    font-size: 12px;
    color: var(--rx-soft);
    font-weight: 400;
}
.time-line {
    margin-top: 8px;
    font-size: 12px;
    color: var(--rx-soft);
    font-family: var(--rx-font-data);
    display: flex;
    align-items: center;
    gap: 4px;
    line-height: 1.5;
    flex-wrap: wrap;
}
.row-actions {
    display: flex;
    align-items: center;
    align-self: center;
    gap: 6px;
    flex-shrink: 0;
}
.row-icon-btn {
    width: 32px;
    height: 32px;
    border-radius: 8px;
    border: 1px solid transparent;
    background: transparent;
    color: var(--rx-soft);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
    font-size: 15px;
    padding: 0;
}
.row-icon-btn:hover {
    background: var(--rx-accent-soft);
    color: var(--rx-accent-strong);
    border-color: var(--rx-accent);
    transform: translateY(-1px);
}
.row-icon-btn:active {
    transform: scale(0.92);
}
.row-icon-btn.notify:hover {
    background: #fff7e6;
    color: var(--rx-warning);
    border-color: var(--rx-warning);
}
.row-icon-btn:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}
.scroll-sentinel {
    padding: 12px 0 4px;
    text-align: center;
    color: var(--rx-soft);
    font-size: 12px;
    font-family: var(--rx-font-data);
}
.scroll-sentinel .no-more {
    color: #c0c4cc;
}

/* 右侧 */
.sidebar {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-width: 0;
    overflow-y: auto;
    max-height: calc(100vh - 170px);
    padding-right: 4px;
}
.sidebar::-webkit-scrollbar {
    width: 6px;
}
.sidebar::-webkit-scrollbar-thumb {
    background: var(--rx-line-strong);
    border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover {
    background: var(--rx-soft);
}
.placeholder-card {
    flex: 1;
    background: var(--rx-surface);
    border: 1px solid var(--rx-line);
    border-radius: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 400px;
    box-shadow: var(--rx-shadow-sm);
}
.card {
    background: var(--rx-surface);
    border: 1px solid var(--rx-line);
    border-radius: 14px;
    padding: 18px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    box-shadow: var(--rx-shadow-sm);
    transition: box-shadow 0.2s ease;
}
.card:hover {
    box-shadow: var(--rx-shadow);
}
.card-title {
    font-size: 15px;
    font-weight: 700;
    color: var(--rx-ink);
    display: flex;
    align-items: center;
    gap: 10px;
    padding-left: 10px;
    position: relative;
}
.card-title::before {
    content: '';
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 16px;
    border-radius: 2px;
    background: linear-gradient(180deg, #4f8cff 0%, #2563eb 100%);
}
.case-sub {
    font-size: 12px;
    font-weight: 400;
    color: var(--rx-soft);
    font-family: var(--rx-font-data);
}
.patient-hero {
    background: linear-gradient(135deg, #f5f8ff 0%, #eef3ff 100%);
    border: 1px solid #dde7ff;
    border-radius: 12px;
    padding: 16px 18px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    position: relative;
    overflow: hidden;
}
.patient-hero::after {
    content: '';
    position: absolute;
    right: -20px;
    top: -20px;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(79, 140, 255, 0.08) 0%, transparent 70%);
}
.patient-name {
    font-size: 22px;
    font-weight: 700;
    color: var(--rx-ink);
    letter-spacing: -0.01em;
}
.patient-meta {
    color: var(--rx-muted);
    font-size: 13px;
    line-height: 1.7;
    display: flex;
    flex-direction: column;
    gap: 2px;
    position: relative;
    z-index: 1;
}

/* 病例紧凑网格 */
.grid-block {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-top: 12px;
    border-top: 1px dashed var(--rx-line);
}
.grid-block:first-of-type {
    border-top: none;
    padding-top: 0;
}
.grid-title {
    font-size: 13px;
    font-weight: 600;
    color: var(--rx-accent-strong);
    letter-spacing: 0.02em;
    display: flex;
    align-items: center;
    gap: 6px;
}
.grid-title::before {
    content: '';
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--rx-accent);
}
.kv-grid {
    display: grid;
    gap: 8px 16px;
}
.kv-grid.one { grid-template-columns: 1fr; }
.kv-grid.two { grid-template-columns: repeat(2, 1fr); }
.kv-grid.three { grid-template-columns: repeat(3, 1fr); }
.kv-grid.four { grid-template-columns: repeat(4, 1fr); }

:deep(.kv-item) {
    display: flex;
    gap: 6px;
    font-size: 12.5px;
    line-height: 1.55;
    min-width: 0;
}
:deep(.kv-item.multiline) {
    flex-direction: column;
    gap: 2px;
}
:deep(.kv-item .kv-label) {
    color: var(--rx-soft);
    flex-shrink: 0;
    min-width: 58px;
}
:deep(.kv-item .kv-value) {
    color: var(--rx-ink);
    word-break: break-word;
    flex: 1;
    font-weight: 500;
}
:deep(.metric-kv-item .metric-kv-value) {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
:deep(.metric-kv-item .metric-kv-value.high) {
    color: #dc2626;
    font-weight: 700;
}
:deep(.kv-item.empty .kv-value) {
    color: #c0c4cc;
    font-weight: 400;
}
:deep(.kv-item.multiline .kv-value) {
    white-space: pre-wrap;
    font-weight: 400;
}

.rx-note {
    font-size: 13px;
    color: var(--rx-muted);
    line-height: 1.6;
    padding: 12px 14px;
    background: linear-gradient(135deg, #fafbfd 0%, #f5f7fb 100%);
    border-radius: 8px;
    border-left: 3px solid var(--rx-accent);
}

.metric-viewport {
    border: 1px solid var(--rx-line);
    border-radius: 10px;
    overflow: hidden;
}
.metric-table :deep(.el-table__header) th {
    background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
    color: var(--rx-ink);
    font-weight: 600;
}
.metric-value {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-weight: 500;
    color: var(--rx-ink);
}
.metric-value.high {
    color: #dc2626;
    font-weight: 700;
}
.metric-up {
    font-size: 14px;
    line-height: 1;
    color: #dc2626;
}

.float-bar {
    position: fixed;
    right: 80px;
    bottom: 18px;
    z-index: 20;
    display: flex;
    gap: 10px;
}

.float-action {
    min-width: 104px;
    border: none;
    box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
}

.float-action.note {
    color: #fff;
    background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
}

.float-action.note:hover,
.float-action.note:focus-visible {
    color: #fff;
    background: linear-gradient(135deg, #ea580c 0%, #c2410c 100%);
}

.float-action.edit {
    color: #fff;
    background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
}

.float-action.edit:hover,
.float-action.edit:focus-visible {
    color: #fff;
    background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
}

.float-action.finish {
    color: #fff;
    background: linear-gradient(135deg, #4f8cff 0%, #2563eb 100%);
}

.float-action.finish:hover,
.float-action.finish:focus-visible {
    color: #fff;
    background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
}

.float-action:deep(.el-icon),
.float-action :deep(.el-icon) {
    color: inherit;
}

.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.2s ease, transform 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
    opacity: 0;
    transform: translateY(8px);
}

@media (max-width: 1100px) {
    .main-area {
        flex-direction: column;
    }
    .queue-col {
        width: 100%;
    }
    .queue-panel {
        height: auto;
        max-height: 360px;
    }
    .float-bar {
        right: 20px;
        bottom: 20px;
    }
    .kv-grid.three,
    .kv-grid.four {
        grid-template-columns: repeat(2, 1fr);
    }
}
</style>
