<!--
  文件对象储存 — 表格列表页

  功能：
  - 表格展示所有文件（缩略图、原文件名、储存文件名、类型、大小、上传者、时间）
  - 图片缩略图 50x50，点击弹窗预览大图
  - 操作：下载 / 复制下载链接 / 删除

  管理后台接口（Cookie 登录态）：
  - GET  /zgapi/v1/admin/file/{id}/view    预览
  - GET  /zgapi/v1/admin/file/{id}/download 下载
  - DELETE /zgapi/v1/admin/file/{id}         删除
  - POST /zgapi/v1/admin/file/page           分页

  开放 API（需外部系统自行获取 token）：
  {gatewayUrl}/zgapi/v1/open/file/{id}/download?token=YOUR_TOKEN
-->
<template>
  <div class="page-wrapper">
    <!-- 搜索+操作栏 -->
    <el-card shadow="never">
      <template #header>
        <div class="card-header-title">
          <el-icon><Search /></el-icon>
          <span>搜索条件</span>
        </div>
      </template>
      <el-form :model="query" inline>
        <el-form-item label="原文件名">
          <el-input v-model="query.fileName" placeholder="请输入文件名" clearable @keyup.enter="handleSearch" />
        </el-form-item>
        <el-form-item label="上传者">
          <el-input v-model="query.uploadUserName" placeholder="请输入上传者" clearable />
        </el-form-item>
        <el-form-item label="文件类型">
          <el-select v-model="query.fileType" placeholder="全部" clearable style="width:120px">
            <el-option label="头像" value="avatar" />
            <el-option label="图标" value="icon" />
            <el-option label="文档" value="doc" />
            <el-option label="其他" value="other" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">
            <el-icon><Search /></el-icon>搜索
          </el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
      <div class="toolbar">
        <el-button type="primary" @click="uploadVisible = true" v-permission="'system:file:upload'">
          <el-icon><Upload /></el-icon>上传文件
        </el-button>
      </div>
    </el-card>

    <!-- 上传弹窗 -->
    <el-dialog v-model="uploadVisible" width="640px" destroy-on-close @closed="resetUpload" class="upload-dialog">
      <template #header>
        <div class="dialog-header-custom">
          <div class="header-icon-box">
            <el-icon :size="22"><UploadFilled /></el-icon>
          </div>
          <div>
            <div class="header-title">上传文件</div>
            <div class="header-sub">{{ uploadQueue.length ? uploadQueue.length + ' 个文件待上传' : '支持批量选择，逐个上传' }}</div>
          </div>
        </div>
      </template>

      <!-- 拖拽区域 -->
      <div
        class="drop-zone"
        :class="{ 'drag-over': dragOver, 'has-files': uploadQueue.length > 0 }"
        @click="triggerUpload"
        @dragover.prevent="dragOver = true"
        @dragleave="dragOver = false"
        @drop.prevent="onDrop"
      >
        <!-- 空状态 -->
        <div v-if="uploadQueue.length === 0" class="drop-empty">
          <div class="drop-icon-ring">
            <el-icon :size="36"><Cloudy /></el-icon>
          </div>
          <p class="drop-title">将文件拖拽到此处</p>
          <p class="drop-hint">或点击此区域选择文件（支持多选）</p>
        </div>

        <!-- 文件队列 -->
        <div v-else class="queue-list">
          <div
            v-for="(f, i) in uploadQueue" :key="i"
            class="queue-card"
            :class="f._status"
          >
            <!-- 状态图标 -->
            <div class="q-icon" :class="f._status">
              <el-icon v-if="f._status === 'done'" :size="22"><Check /></el-icon>
              <el-icon v-else-if="f._status === 'fail'" :size="22"><Close /></el-icon>
              <div v-else-if="f._status === 'uploading'" class="spinner"></div>
              <el-icon v-else :size="22"><Document /></el-icon>
            </div>

            <!-- 文件信息 -->
            <div class="q-body">
              <div class="q-name">{{ f.file.name }}</div>
              <div class="q-meta">
                <span>{{ formatSize(f.file.size) }}</span>
              </div>
              <!-- 进度条 -->
              <div v-if="f._status === 'uploading'" class="q-progress">
                <el-progress :percentage="uploadPercent" :stroke-width="8" :show-text="true" :color="'#6366f1'" />
                <div class="q-speed">{{ uploadSpeed }}</div>
              </div>
            </div>

            <!-- 操作 -->
            <el-button
              v-if="f._status === 'wait'"
              class="q-remove" circle size="small" text
              @click.stop="removeQueueItem(i)"
            >
              <el-icon><Close /></el-icon>
            </el-button>
            <el-tag v-else-if="f._status === 'done'" type="success" size="small" effect="light" round>完成</el-tag>
            <el-tag v-else-if="f._status === 'fail'" type="danger" size="small" effect="light" round>失败</el-tag>
          </div>
        </div>
        <input ref="fileInputRef" type="file" multiple style="display:none" @change="onFileSelected" />
      </div>

      <!-- 底部操作栏 -->
      <div class="upload-footer">
        <div class="footer-left">
          <el-select v-model="uploadForm.fileType" style="width:110px" :disabled="uploading" size="default">
            <el-option label="🖼 头像" value="avatar" />
            <el-option label="⭐ 图标" value="icon" />
            <el-option label="📄 文档" value="doc" />
            <el-option label="📦 其他" value="other" />
          </el-select>
          <el-button @click="triggerUpload" :disabled="uploading" size="default">
            <el-icon><FolderAdd /></el-icon>添加文件
          </el-button>
          <el-button v-if="uploadQueue.length > 0" @click="uploadQueue = []" :disabled="uploading" size="default" text>
            清空列表
          </el-button>
        </div>
        <el-button
          type="primary" :loading="uploading" size="large"
          :disabled="uploadQueue.length === 0 || allDone"
          @click="startUpload"
          class="btn-start"
        >
          <template v-if="!uploading">
            <el-icon><Upload /></el-icon>
            开始上传（{{ uploadQueue.filter(f => f._status !== 'done').length }} 个文件）
          </template>
          <template v-else>
            {{ doneCount }}/{{ uploadQueue.length }}
          </template>
        </el-button>
      </div>
    </el-dialog>

    <!-- 表格 -->
    <el-card shadow="never">
      <el-table :data="fileList" border stripe v-loading="loading" style="width:100%">
        <!-- 压缩图 -->
        <el-table-column label="压缩图" width="80" align="center">
          <template #default="{ row }">
            <div class="thumb-box" @click="preview(row)" v-if="isImage(row.mimeType)">
              <img :src="getViewUrl(row.id)" class="thumb-img" loading="lazy" />
            </div>
            <el-icon v-else :size="28" color="#94a3b8"><Document /></el-icon>
          </template>
        </el-table-column>

        <!-- 原文件名 -->
        <el-table-column prop="fileName" label="原文件名" min-width="180" show-overflow-tooltip />

        <!-- 储存文件名 -->
        <el-table-column prop="storageFileName" label="储存文件名" width="200" show-overflow-tooltip>
          <template #default="{ row }">
            <code class="storage-name-cell">{{ row.storageFileName || '-' }}</code>
          </template>
        </el-table-column>

        <!-- 文件类型 -->
        <el-table-column prop="fileType" label="文件类型" width="90" align="center">
          <template #default="{ row }">
            <el-tag :type="fileTypeTag(row.fileType)" size="small" effect="dark">
              {{ fileTypeLabel(row.fileType) }}
            </el-tag>
          </template>
        </el-table-column>

        <!-- 文件大小 -->
        <el-table-column label="文件大小" width="100" align="center">
          <template #default="{ row }">
            {{ formatSize(row.fileSize) }}
          </template>
        </el-table-column>

        <!-- 上传者 -->
        <el-table-column prop="uploadUserName" label="上传者" width="120" show-overflow-tooltip>
          <template #default="{ row }">
            {{ row.uploadUserName || '-' }}
          </template>
        </el-table-column>

        <!-- 上传时间 -->
        <el-table-column label="上传时间" width="170">
          <template #default="{ row }">
            {{ row.createdAt ? new Date(row.createdAt).toLocaleString('zh-CN') : '-' }}
          </template>
        </el-table-column>

        <!-- 操作 -->
        <el-table-column label="操作" width="220" align="center" class-name="ops-col" fixed="right">
          <template #default="{ row }">
            <div class="actions">
              <el-button size="small" @click="downloadFile(row)">
                <el-icon><Download /></el-icon>下载
              </el-button>
              <el-button size="small" @click="copyDownloadUrl(row)">
                <el-icon><Link /></el-icon>复制链接
              </el-button>
              <el-popconfirm title="确定删除此文件？" @confirm="handleDelete(row.id)">
                <template #reference>
                  <el-button size="small" type="danger">
                    <el-icon><Delete /></el-icon>删除
                  </el-button>
                </template>
              </el-popconfirm>
            </div>
          </template>
        </el-table-column>

        <template #empty>
          <el-empty description="暂无文件" :image-size="100" />
        </template>
      </el-table>

      <div class="pagination-wrapper" v-if="total > 0">
        <el-pagination
          v-model:current-page="page"
          v-model:page-size="pageSize"
          :total="total"
          :page-sizes="[10, 20, 50, 100]"
          layout="total, sizes, prev, pager, next, jumper"
          background
          @current-change="fetchData"
          @size-change="fetchData"
        />
      </div>
    </el-card>

    <!-- 预览弹窗 -->
    <el-dialog v-model="previewVisible" title="文件预览" width="720px" destroy-on-close>
      <div class="preview-body">
        <template v-if="previewItem && isImage(previewItem.mimeType)">
          <img :src="getViewUrl(previewItem.id)" style="max-width:100%;max-height:65vh;display:block;margin:0 auto" />
        </template>
        <template v-else>
          <el-empty description="非图片文件，无法预览" />
        </template>
        <div class="preview-meta" v-if="previewItem">
          <div class="meta-line"><span class="meta-label">原文件名：</span>{{ previewItem.fileName }}</div>
          <div class="meta-line"><span class="meta-label">储存文件名：</span>{{ previewItem.storageFileName || '-' }}</div>
          <div class="meta-line"><span class="meta-label">类型：</span>{{ previewItem.mimeType || '-' }}</div>
          <div class="meta-line"><span class="meta-label">大小：</span>{{ formatSize(previewItem.fileSize) }}</div>
          <div class="meta-line"><span class="meta-label">上传者：</span>{{ previewItem.uploadUserName || '-' }}</div>
          <div class="meta-line"><span class="meta-label">上传时间：</span>{{ previewItem.createdAt ? new Date(previewItem.createdAt).toLocaleString('zh-CN') : '-' }}</div>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import {
  Search, Document, Delete, Upload, UploadFilled, Close, Download, Link,
  Cloudy, FolderAdd, Check,
} from '@element-plus/icons-vue'
import request from '@/utils/request'
import { useUploadStore } from '@/stores/upload'

interface FileItem {
  id: string
  fileName: string
  storageFileName: string
  fileType: string
  fileSize: number
  mimeType: string
  md5: string
  uploadUserName: string
  createdAt: string
}

const loading = ref(false)
const fileList = ref<FileItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL || ''

// 搜索条件
const query = reactive({
  fileName: '',
  uploadUserName: '',
  fileType: '' as string,
})

const previewVisible = ref(false)
const previewItem = ref<FileItem | null>(null)

// ===== 上传队列 =====
interface QueueItem { file: File; _status: 'wait' | 'uploading' | 'done' | 'fail' }

const uploadStore = useUploadStore()
const uploadVisible = ref(false)
const uploading = ref(false)
const uploadPercent = ref(0)
const uploadSpeed = ref('')
const dragOver = ref(false)
const fileInputRef = ref<HTMLInputElement>()
const uploadQueue = ref<QueueItem[]>([])
const uploadForm = reactive({ fileType: 'other' })
let lastLoaded = 0
let lastTime = 0

const doneCount = computed(() => uploadQueue.value.filter(f => f._status === 'done').length)
const allDone = computed(() => uploadQueue.value.length > 0 && uploadQueue.value.every(f => f._status === 'done' || f._status === 'fail'))

function resetUpload() {
  if (uploading.value) return // 上传中不重置
  uploading.value = false
  uploadPercent.value = 0
  uploadSpeed.value = ''
  uploadQueue.value = []
  dragOver.value = false
}

function triggerUpload() { fileInputRef.value?.click() }

function addFiles(files: FileList | File[]) {
  for (let i = 0; i < files.length; i++) {
    const file = files[i]
    uploadQueue.value.push({ file, _status: 'wait' })
    // 根据第一个文件自动判断类型
    if (uploadQueue.value.length === 1) {
      const ext = file.name.split('.').pop()?.toLowerCase()
      if (ext && ['png','jpg','jpeg','gif','webp','svg','ico'].includes(ext)) uploadForm.fileType = 'icon'
      else if (ext && ['pdf','doc','docx','xls','xlsx','txt','md'].includes(ext)) uploadForm.fileType = 'doc'
    }
  }
}

function removeQueueItem(i: number) {
  uploadQueue.value.splice(i, 1)
}

function onFileSelected(e: Event) {
  const files = (e.target as HTMLInputElement).files
  if (files && files.length > 0) addFiles(files)
  // 重置 input 以允许重复选同一个文件
  if (fileInputRef.value) fileInputRef.value.value = ''
}

function onDrop(e: DragEvent) {
  dragOver.value = false
  const files = e.dataTransfer?.files
  if (files && files.length > 0) addFiles(files)
}

/** 逐个上传队列中的文件（串行） */
async function startUpload() {
  if (uploadQueue.value.length === 0) return
  uploading.value = true

  for (let i = 0; i < uploadQueue.value.length; i++) {
    const item = uploadQueue.value[i]
    if (item._status === 'done') continue

    item._status = 'uploading'
    uploadPercent.value = 0
    uploadSpeed.value = '准备上传...'
    lastLoaded = 0; lastTime = Date.now()

    // 注册到全局上传面板
    const taskId = Date.now().toString(36) + i
    uploadStore.addTask({
      id: taskId, fileName: item.file.name, fileSize: item.file.size,
      fileType: uploadForm.fileType, file: item.file,
      status: 'uploading', progress: 0, speed: '', startedAt: Date.now(),
    })

    try {
      const fd = new FormData()
      fd.append('file', item.file)
      fd.append('fileType', uploadForm.fileType)

      const axios = (await import('axios')).default
      const baseURL = import.meta.env.VITE_GATEWAY_URL || ''
      const token = document.cookie.match(/(?:^|;\s*)token=([^;]*)/)?.[1] || ''

      const res = await axios.post(baseURL + '/zgapi/v1/admin/file/upload', fd, {
        timeout: 43200000, // 12小时
        headers: { 'Content-Type': 'multipart/form-data', ...(token ? { Authorization: 'Bearer ' + token } : {}) },
        onUploadProgress: (e: any) => {
          if (e.total > 0) {
            uploadPercent.value = Math.round((e.loaded / e.total) * 100)
            const now = Date.now()
            const elapsed = (now - lastTime) / 1000
            if (elapsed >= 1) {
              const speed = (e.loaded - lastLoaded) / elapsed
              uploadSpeed.value = formatSpeed(speed)
              lastLoaded = e.loaded; lastTime = now
            }
            uploadStore.updateTask(taskId, { progress: uploadPercent.value, speed: uploadSpeed.value })
          }
        },
      })

      // 检查业务状态码
      if (res.data?.code && res.data.code !== 200) {
        throw new Error(res.data.msg || '服务器返回错误')
      }

      item._status = 'done'
      uploadStore.updateTask(taskId, { status: 'completed', progress: 100 })
    } catch (e: any) {
      item._status = 'fail'
      // 提取错误信息
      let errMsg = '上传失败'
      if (e.response?.data?.msg) {
        errMsg = e.response.data.msg
      } else if (e.response?.status) {
        errMsg = `服务器错误(${e.response.status})`
      } else if (e.message && e.message !== '上传失败') {
        errMsg = e.message
      }
      uploadStore.updateTask(taskId, { status: 'failed', error: errMsg })
      console.error(`[上传失败] ${item.file.name}:`, errMsg, e)
    }
  }

  uploading.value = false
  ElMessage.success(`上传完成: ${doneCount.value}/${uploadQueue.value.length} 个文件成功`)
  page.value = 1
  fetchData()
}

function formatSpeed(bytesPerSec: number) {
  if (bytesPerSec < 1024) return bytesPerSec.toFixed(0) + ' B/s'
  if (bytesPerSec < 1048576) return (bytesPerSec / 1024).toFixed(1) + ' KB/s'
  return (bytesPerSec / 1048576).toFixed(1) + ' MB/s'
}

// ===== 数据 =====
function getViewUrl(id: string) { return `${gatewayUrl}/zgapi/v1/admin/file/${id}/view` }
function getDownloadUrl(id: string) { return `${gatewayUrl}/zgapi/v1/admin/file/${id}/download` }
function isImage(mime: string | null | undefined) { return mime ? mime.startsWith('image/') : false }

function formatSize(bytes: number | null) {
  if (!bytes) return '0 B'
  if (bytes < 1024) return bytes + ' B'
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
  return (bytes / 1048576).toFixed(1) + ' MB'
}

function fileTypeTag(type: string) {
  const map: Record<string, string> = { avatar: 'success', icon: 'warning', doc: '', other: 'info' }
  return map[type] || 'info'
}

function fileTypeLabel(type: string) {
  const map: Record<string, string> = { avatar: '头像', icon: '图标', doc: '文档', other: '其他' }
  return map[type] || type || '其他'
}

async function fetchData() {
  loading.value = true
  try {
    const params: Record<string, unknown> = {
      page: page.value,
      pageSize: pageSize.value,
    }
    if (query.fileName) params.fileName = query.fileName
    if (query.uploadUserName) params.uploadUserName = query.uploadUserName
    if (query.fileType) params.fileType = query.fileType
    const res = await request.post('/zgapi/v1/admin/file/page', params)
    fileList.value = res.data?.records || []
    total.value = res.data?.total || 0
  } catch { /* ignore */ } finally {
    loading.value = false
  }
}

function handleSearch() {
  page.value = 1
  fetchData()
}

function handleReset() {
  query.fileName = ''
  query.uploadUserName = ''
  query.fileType = ''
  page.value = 1
  fetchData()
}

// ===== 预览 =====
function preview(item: FileItem) {
  previewItem.value = item
  previewVisible.value = true
}

// ===== 下载（fetch + blob，支持跨域） =====
async function downloadFile(item: FileItem) {
  try {
    const res = await fetch(getDownloadUrl(item.id), { credentials: 'include' })
    if (!res.ok) { ElMessage.error('下载失败'); return }
    const blob = await res.blob()
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = item.fileName
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(url)
  } catch {
    ElMessage.error('下载失败')
  }
}

// ===== 复制下载链接（自动获取一次性 token） =====
async function copyDownloadUrl(item: FileItem) {
  try {
    // 自动获取开放 API Token
    const tokenRes = await request.post('/zgapi/v1/open/token', { systemCode: 'gateway' })
    const token = tokenRes.data?.token
    if (!token) { ElMessage.error('获取下载Token失败'); return }

    const url = `${gatewayUrl}/zgapi/v1/open/file/${item.id}/download?token=${token}`
    await navigator.clipboard.writeText(url)
    ElMessage.success('已复制下载链接（Token 12小时有效，仅可使用一次）')
  } catch {
    ElMessage.error('复制失败')
  }
}

// ===== 删除 =====
async function handleDelete(id: string) {
  try {
    await request.delete(`/zgapi/v1/admin/file/${id}`)
    ElMessage.success('已删除')
    if (fileList.value.length === 1 && page.value > 1) page.value--
    fetchData()
  } catch { /* ignore */ }
}

// 防误刷：上传中刷新/关闭页面时弹提示
function beforeUnload(e: BeforeUnloadEvent) {
  if (uploading.value) {
    e.preventDefault()
    e.returnValue = ''
    return ''
  }
}

onMounted(() => {
  fetchData()
  window.addEventListener('beforeunload', beforeUnload)
})

onBeforeUnmount(() => {
  window.removeEventListener('beforeunload', beforeUnload)
})
</script>

<style scoped>
.page-wrapper { display:flex; flex-direction:column; gap:16px; }

/* 搜索卡片 */
.card-header-title {
  display:flex; align-items:center; gap:6px; font-weight:600; color:#303133;
}

/* 工具栏 */
.toolbar { display:flex; justify-content:flex-start; margin-bottom:0; }
.toolbar-left { display:flex; gap:12px; }
.toolbar-right { display:flex; gap:8px; }

/* 缩略图 */
.thumb-box {
  width:50px; height:50px; border-radius:6px; overflow:hidden;
  cursor:pointer; border:1px solid var(--border-color);
}
.thumb-box:hover { border-color:var(--color-primary); }
.thumb-img {
  width:100%; height:100%; object-fit:cover; display:block;
}

/* 储存文件名 */
.storage-name-cell {
  font-family:'SF Mono','Fira Code',monospace;
  font-size:12px; color:var(--text-muted);
  background:var(--bg-page); padding:2px 6px; border-radius:4px;
}

/* 分页 */
.pagination-wrapper { display:flex; justify-content:flex-end; margin-top:20px; }

/* 操作 */
.actions { display:flex; gap:4px; flex-wrap:wrap; justify-content:center; }

/* ===== 上传弹窗 ===== */
.upload-dialog :deep(.el-dialog__header) { display:none; }
.upload-dialog :deep(.el-dialog__body) { padding:0; }

/* 自定义标题 */
.dialog-header-custom {
  display:flex; align-items:center; gap:14px;
  padding:20px 28px 0;
}
.header-icon-box {
  width:44px; height:44px; border-radius:var(--radius-lg);
  background:linear-gradient(135deg, #6366f1, #818cf8);
  display:flex; align-items:center; justify-content:center; color:#fff; flex-shrink:0;
}
.header-title { font-size:17px; font-weight:700; color:var(--text-primary); }
.header-sub { font-size:12px; color:var(--text-muted); margin-top:2px; }

/* 拖拽区域 */
.drop-zone {
  margin:20px 28px; padding:24px;
  border:2px dashed var(--border-color); border-radius:var(--radius-xl);
  cursor:pointer; transition:all 0.25s; position:relative;
  min-height:160px; max-height:360px; overflow-y:auto;
}
.drop-zone:hover, .drop-zone.drag-over {
  border-color:var(--color-primary); background:linear-gradient(135deg, rgba(99,102,241,0.03), rgba(129,140,248,0.05));
}
.drop-zone.drag-over { border-style:solid; box-shadow:0 0 0 4px rgba(99,102,241,0.1); }
.drop-zone.has-files { border-style:solid; border-color:var(--border-color); }

/* 空状态 */
.drop-empty { text-align:center; padding:20px 0; }
.drop-icon-ring {
  width:72px; height:72px; border-radius:50%;
  background:linear-gradient(135deg, #eef2ff, #e0e7ff);
  display:inline-flex; align-items:center; justify-content:center;
  margin-bottom:16px; color:var(--color-primary);
}
.drop-title { font-size:16px; font-weight:600; color:var(--text-primary); margin-bottom:4px; }
.drop-hint { font-size:13px; color:var(--text-muted); }

/* 文件卡片 */
.queue-list { display:flex; flex-direction:column; gap:10px; }
.queue-card {
  display:flex; align-items:center; gap:14px;
  padding:14px 16px; border-radius:var(--radius-lg);
  background:#fff; border:1px solid var(--border-color);
  transition:all 0.2s;
}
.queue-card:hover { box-shadow:var(--shadow-sm); }
.queue-card.done { border-color:#bbf7d0; background:#f0fdf4; }
.queue-card.fail { border-color:#fecaca; background:#fef2f2; }
.queue-card.uploading { border-color:#c7d2fe; background:#eef2ff; }

/* 状态图标 */
.q-icon {
  width:44px; height:44px; border-radius:var(--radius-md);
  display:flex; align-items:center; justify-content:center; flex-shrink:0;
  background:var(--bg-page); color:var(--text-muted);
}
.q-icon.done { background:#dcfce7; color:#16a34a; }
.q-icon.fail { background:#fecaca; color:#dc2626; }
.q-icon.uploading { background:#e0e7ff; }

/* 旋转动画 */
.spinner {
  width:20px; height:20px; border:2.5px solid #c7d2fe;
  border-top-color:var(--color-primary); border-radius:50%;
  animation:spin 0.8s linear infinite;
}
@keyframes spin { to { transform:rotate(360deg); } }

/* 文件信息 */
.q-body { flex:1; min-width:0; }
.q-name { font-size:14px; font-weight:500; color:var(--text-primary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.q-meta { font-size:12px; color:var(--text-muted); margin-top:2px; }
.q-progress { margin-top:8px; }
.q-speed { font-size:11px; color:var(--text-muted); text-align:right; margin-top:2px; }

.q-remove { flex-shrink:0; color:var(--text-muted); }
.q-remove:hover { color:var(--color-danger); background:#fef2f2; }

/* 底部栏 */
.upload-footer {
  display:flex; align-items:center; justify-content:space-between;
  padding:16px 28px 24px; border-top:1px solid var(--border-light);
  gap:12px;
}
.footer-left { display:flex; align-items:center; gap:8px; }
.btn-start { min-width:180px; }

/* 移动端 */
@media (max-width:768px) {
  .upload-dialog :deep(.el-dialog) { width:100vw !important; max-width:100vw !important; margin:0 !important; border-radius:0; }
  .drop-zone { margin:12px 16px; padding:16px; min-height:120px; max-height:240px; }
  .dialog-header-custom { padding:16px 16px 0; }
  .upload-footer { flex-direction:column; padding:12px 16px 20px; }
  .footer-left { width:100%; flex-wrap:wrap; }
  .btn-start { width:100%; }
}

/* 预览弹窗 */
.preview-meta { margin-top:20px; padding:16px; background:var(--bg-page); border-radius:var(--radius-md); }
.meta-line { font-size:13px; color:var(--text-secondary); line-height:2; }
.meta-label { color:var(--text-muted); }

/* 移动端 */
@media (max-width:768px) {
  .toolbar { flex-direction:column; }
  .toolbar .el-button { width:100%; }

  .search-card :deep(.el-form-item) { width:100%; margin-right:0; }
  .search-card :deep(.el-input),
  .search-card :deep(.el-select) { width:100% !important; }

  .storage-name-cell { font-size:10px; }
}
</style>
