<template>
  <div class="mod-weather__real-time-monitoring" v-loading="state.loading">
    <div class="page-header">
      <div>
        <h2 class="page-title">气候实时监测</h2>
      </div>
      <div class="page-header-actions">
        <el-tag type="info">层级 {{ state.selectedPath.length }} 级</el-tag>
        <el-tag type="success">文件 {{ state.imageItems.length }}</el-tag>
        <el-button type="primary" plain icon="Refresh" @click="refresh">刷新</el-button>
      </div>
    </div>

    <el-alert v-if="state.errorMessage" :title="state.errorMessage" type="error" :closable="false" show-icon class="mb-16" />

    <div class="dept-bar" v-if="levelRows.length">
      <div class="level-row" v-for="(row, rowIdx) in levelRows" :key="rowIdx">
        <span class="level-label" v-if="row.label">{{ row.label }}</span>
        <div class="level-tabs">
          <div v-for="tab in row.tabs" :key="tab.id" class="level-tab" :class="[{ 'is-active': row.selectedId === tab.id }, `level-${rowIdx}`]" @click="selectTab(rowIdx, tab)">
            {{ tab.name }}
          </div>
        </div>
      </div>
    </div>

    <div class="page-body" v-if="state.imageItems.length">
      <aside class="file-list-panel">
        <div v-for="(item, index) in state.imageItems" :key="item.anchorId" class="file-list-item" :class="{ 'is-active': state.selectedFileIndex === index }" @click="selectFile(index)">
          <div class="file-list-item__thumb">
            <img v-if="blobUrlCache[item.fileId]" :src="blobUrlCache[item.fileId]" loading="lazy" class="file-list-item__img" />
            <span v-else-if="isImageFile(item.type)" class="file-list-item__icon file-list-item__icon--img">图</span>
            <span v-else-if="isTextFile(item.type)" class="file-list-item__icon file-list-item__icon--txt">文</span>
            <span v-else class="file-list-item__icon file-list-item__icon--other">—</span>
          </div>
          <span class="file-list-item__name">{{ item.displayName || `文件-${item.fileId}` }}</span>
        </div>
      </aside>

      <section class="preview-panel" v-if="previewItem">
        <div class="preview-panel__header">
          <h3 class="preview-panel__title">{{ previewItem.displayName || `文件-${previewItem.fileId}` }}</h3>
          <div class="preview-panel__actions">
            <el-button v-if="fileMode === 'image'" type="primary" plain size="small" :loading="state.copyingId === previewItem.anchorId" @click="copyImage(previewItem)"> 复制图片 </el-button>
            <el-button v-else-if="fileMode === 'text'" type="primary" plain size="small" :loading="state.copyingId === previewItem.anchorId" @click="copyText(previewItem)"> 复制全部文本 </el-button>
            <el-button v-if="fileMode === 'image'" type="success" plain size="small" @click="downloadFile(previewItem)"> 下载原图 </el-button>
          </div>
        </div>

        <div class="preview-panel__body" v-if="fileMode === 'image'">
          <div v-if="!previewBlobUrl && selectedFileId" class="preview-panel__loading">加载中...</div>
          <div v-show="previewBlobUrl" class="preview-zoom-area" @wheel.prevent="onWheel" @mousedown="onPanStart" @dblclick="resetZoom">
            <img :src="previewBlobUrl" :alt="previewItem.displayName" :style="zoomStyle" class="preview-panel__img" draggable="false" loading="eager" />
          </div>
          <div v-if="previewItem.content" class="preview-panel__text">{{ previewItem.content }}</div>
        </div>

        <div class="preview-panel__body" v-else-if="fileMode === 'text'">
          <div class="preview-panel__text preview-panel__text--full">{{ previewItem.content || "暂无文本内容" }}</div>
        </div>

        <div class="preview-panel__body" v-else-if="fileMode === 'unsupported'">
          <div class="image-card__unsupported">当前文件类型暂不支持预览</div>
        </div>
      </section>

      <div class="preview-panel preview-panel--empty" v-else>
        <el-empty description="请在左侧选择要预览的文件" :image-size="120" />
      </div>
    </div>

    <el-empty v-else-if="!state.loading && !state.errorMessage" description="暂无可展示图片" />
  </div>
</template>

<script lang="ts" setup>
import app from "@/constants/app";

import baseService from "@/service/baseService";
import { getToken } from "@/utils/cache";
import { copyToClipboard } from "@/utils/utils";
import { ElMessage } from "element-plus";
import axios from "axios";
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";

interface DeptNode {
  id: string;
  name: string;
  children?: DeptNode[];
}

interface DeptTab {
  id: string;
  name: string;
}

interface DeptFileItemVO {
  displayName: string;
  fileId: string;
  type: string;
  content: string;
}

interface DeptFileGroupVO {
  deptId: string;
  deptName: string;
  fileList: DeptFileItemVO[];
}

interface ImageItem {
  deptId: string;
  deptName: string;
  displayName: string;
  fileId: string;
  type: string;
  content: string;
  anchorId: string;
}

const state = reactive({
  loading: true,
  errorMessage: "",
  imageItems: [] as ImageItem[],
  copyingId: "",
  deptTreeData: [] as DeptNode[],
  selectedPath: [] as string[],
  selectedFileIndex: 0
});

const blobUrlCache = {} as Record<string, string>;
const blobPromises = {} as Record<string, Promise<string> | undefined>;
const previewBlobUrl = ref("");

const loadBlobUrl = (fileId: string): Promise<string> => {
  if (blobUrlCache[fileId]) return Promise.resolve(blobUrlCache[fileId]);
  if (blobPromises[fileId]) return blobPromises[fileId];

  const base = (app.api || "").replace(/\/$/, "");
  const url = `${base}/filescan/file/display/${fileId}`;
  const token = getToken();
  const headers: Record<string, string> = {};
  if (token) headers.token = token;

  const promise = axios
    .get(url, { responseType: "blob", headers })
    .then((res) => {
      const objectUrl = URL.createObjectURL(res.data as Blob);
      blobUrlCache[fileId] = objectUrl;
      return objectUrl;
    })
    .finally(() => {
      delete blobPromises[fileId];
    });

  blobPromises[fileId] = promise;
  return promise;
};

const activeDeptId = computed(() => {
  if (state.selectedPath.length > 0) {
    return state.selectedPath[state.selectedPath.length - 1];
  }
  return null;
});

interface LevelRow {
  tabs: DeptTab[];
  selectedId: string | null;
  label: string;
}

const LEVEL_LABELS = ["一级", "二级", "三级", "四级", "五级", "六级"];

const levelRows = computed<LevelRow[]>(() => {
  const rows: LevelRow[] = [];
  let nodes: DeptNode[] = state.deptTreeData;

  for (let i = 0; i < state.selectedPath.length; i++) {
    const selId = state.selectedPath[i];
    rows.push({
      tabs: nodes.map((n) => ({ id: n.id, name: n.name })),
      selectedId: selId,
      label: LEVEL_LABELS[i] || `${i + 1}级`
    });
    const selNode = nodes.find((n) => n.id === selId);
    if (selNode?.children?.length) {
      nodes = selNode.children;
    } else {
      nodes = [];
      break;
    }
  }

  if (nodes.length > 0) {
    rows.push({
      tabs: nodes.map((n) => ({ id: n.id, name: n.name })),
      selectedId: null,
      label: LEVEL_LABELS[rows.length] || `${rows.length + 1}级`
    });
  }

  return rows;
});

const previewItem = computed(() => {
  if (state.selectedFileIndex >= 0 && state.selectedFileIndex < state.imageItems.length) {
    return state.imageItems[state.selectedFileIndex];
  }
  return state.imageItems.length > 0 ? state.imageItems[0] : null;
});

const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"];
const TEXT_EXTS = ["txt", "md", "log", "csv", "json", "xml", "html", "htm", "yaml", "yml"];

const isImageFile = (type: string) => {
  if (!type) return true;
  const lower = type.toLowerCase();
  return IMAGE_EXTS.some((ext) => lower.includes(ext));
};

const isTextFile = (type: string) => {
  if (!type) return false;
  const lower = type.toLowerCase();
  return TEXT_EXTS.some((ext) => lower.includes(ext));
};

const fileMode = computed(() => {
  const item = previewItem.value;
  if (!item) return "none";
  if (isImageFile(item.type)) return "image";
  if (isTextFile(item.type)) return "text";
  return "unsupported";
});

const zoomScale = ref(1);
const panX = ref(0);
const panY = ref(0);
const isPanning = ref(false);
let panStartX = 0;
let panStartY = 0;
let panOriginX = 0;
let panOriginY = 0;

const zoomStyle = computed(() => ({
  transform: `translate(${panX.value}px, ${panY.value}px) scale(${zoomScale.value})`,
  cursor: isPanning.value ? "grabbing" : "grab",
  transition: isPanning.value ? "none" : "transform 0.15s ease"
}));

const resetZoom = () => {
  zoomScale.value = 1;
  panX.value = 0;
  panY.value = 0;
};

const onWheel = (e: WheelEvent) => {
  e.preventDefault();
  const delta = e.deltaY > 0 ? -0.15 : 0.15;
  const newScale = Math.max(0.2, Math.min(5, zoomScale.value + delta));
  zoomScale.value = newScale;
  if (newScale <= 1) {
    panX.value = 0;
    panY.value = 0;
  }
};

const onPanStart = (e: MouseEvent) => {
  if (zoomScale.value <= 1) return;
  isPanning.value = true;
  panStartX = e.clientX;
  panStartY = e.clientY;
  panOriginX = panX.value;
  panOriginY = panY.value;
  document.addEventListener("mousemove", onPanMove);
  document.addEventListener("mouseup", onPanEnd);
};

const onPanMove = (e: MouseEvent) => {
  if (!isPanning.value) return;
  panX.value = panOriginX + (e.clientX - panStartX);
  panY.value = panOriginY + (e.clientY - panStartY);
};

const onPanEnd = () => {
  isPanning.value = false;
  document.removeEventListener("mousemove", onPanMove);
  document.removeEventListener("mouseup", onPanEnd);
};

/** 跨部门缓存：{ deptId: ImageItem[] }，切换 tab 时瞬间命中 */
const deptFileCache = {} as Record<string, ImageItem[]>;

const transformGroups = (list: DeptFileGroupVO[]): ImageItem[] => {
  const imageItems: ImageItem[] = [];
  list.forEach((group) => {
    (group.fileList || []).forEach((file) => {
      imageItems.push({
        deptId: group.deptId,
        deptName: group.deptName,
        displayName: file.displayName,
        fileId: file.fileId,
        type: file.type,
        content: file.content || "",
        anchorId: `img-${file.fileId}`
      });
    });
  });
  return imageItems;
};

const loadData = async (deptId: string) => {
  // 命中跨部门缓存 → 直接渲染
  const cached = deptFileCache[deptId];
  if (cached) {
    state.imageItems = cached;
    state.selectedFileIndex = 0;
    selectFile(0);
    return;
  }

  state.loading = true;
  state.errorMessage = "";
  try {
    const res = await baseService.get("/filescan/record/tree", { deptId });
    const items = transformGroups((res.data || []) as DeptFileGroupVO[]);
    deptFileCache[deptId] = items;
    state.imageItems = items;
    state.selectedFileIndex = 0;

    // 加载当前部门全部图片 blob
    selectFile(0);
    const preloads = items
      .filter((item) => isImageFile(item.type))
      .map((item) => loadBlobUrl(item.fileId));
    await Promise.allSettled(preloads);
  } catch (error: any) {
    state.errorMessage = error?.message || "加载实时监测图片失败";
    state.imageItems = [];
  } finally {
    state.loading = false;
  }
};

/** 收集部门树中所有叶子节点 ID */
const collectLeafDeptIds = (nodes: DeptNode[]): string[] => {
  const ids: string[] = [];
  const walk = (list: DeptNode[]) => {
    for (const n of list) {
      if (n.children?.length) {
        walk(n.children);
      } else {
        ids.push(n.id);
      }
    }
  };
  walk(nodes);
  return ids;
};

const loadDeptTree = async (): Promise<string | null> => {
  try {
    const res = await baseService.get("/sys/dept/list");
    const treeData = (res.data || []) as DeptNode[];
    state.deptTreeData = treeData;

    if (!treeData.length) return null;

    const firstRoot = treeData[0];
    const firstChild = firstRoot.children?.length ? firstRoot.children[0] : firstRoot;

    const buildPath = (nodes: DeptNode[], targetId: string, path: string[]): string[] | null => {
      for (const node of nodes) {
        const newPath = [...path, node.id];
        if (node.id === targetId) return newPath;
        if (node.children?.length) {
          const found = buildPath(node.children, targetId, newPath);
          if (found) return found;
        }
      }
      return null;
    };

    const path = buildPath(treeData, firstChild.id, []);
    state.selectedPath = path || [firstRoot.id];
    return firstChild.id;
  } catch {
    state.deptTreeData = [];
    return null;
  }
};

const selectTab = (levelIdx: number, tab: DeptTab) => {
  state.selectedPath = [...state.selectedPath.slice(0, levelIdx), tab.id];
  state.selectedFileIndex = 0;
  state.imageItems = [];

  // 跨部门缓存命中 → 瞬间渲染 + 加载全部图片 blob
  if (deptFileCache[tab.id]) {
    state.imageItems = deptFileCache[tab.id];
    state.selectedFileIndex = 0;
    state.loading = false;
    selectFile(0);
    // 确保当前部门全部图片 blob 已加载
    const preloads = state.imageItems
      .filter((item) => isImageFile(item.type))
      .filter((item) => !blobUrlCache[item.fileId])
      .map((item) => loadBlobUrl(item.fileId));
    Promise.allSettled(preloads);
  } else {
    loadData(tab.id);
  }
};

const refresh = () => {
  if (activeDeptId.value !== null) {
    loadData(activeDeptId.value);
  }
};

const selectedFileId = ref("");

const selectFile = async (index: number) => {
  state.selectedFileIndex = index;
  const item = state.imageItems[index];
  if (!item) return;

  selectedFileId.value = item.fileId;
  resetZoom();

  if (isImageFile(item.type)) {
    previewBlobUrl.value = "";
    const objectUrl = await loadBlobUrl(item.fileId);
    previewBlobUrl.value = objectUrl;
  } else {
    previewBlobUrl.value = "";
  }
};

const downloadFile = async (item: ImageItem) => {
  const filename = item.displayName || `file-${item.fileId}`;
  await loadBlobUrl(item.fileId);
  const a = document.createElement("a");
  a.href = blobUrlCache[item.fileId];
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
};

const copyText = (item: ImageItem) => {
  state.copyingId = item.anchorId;
  copyToClipboard(item.content || "");
  ElMessage.success("文本已复制成功");
  state.copyingId = "";
};

const copyImage = async (item: ImageItem) => {
  if (!isImageFile(item.type)) {
    ElMessage.warning("当前文件不是图片类型，无法复制");
    return;
  }
  state.copyingId = item.anchorId;
  try {
    await loadBlobUrl(item.fileId);
    const blobUrl = blobUrlCache[item.fileId];
    const res = await fetch(blobUrl);
    const blob = await res.blob();
    try {
      await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
    } catch (e: any) {
      if (e.name === "NotAllowedError" || e.message?.includes?.("not supported")) {
        await convertToPngAndCopy(blob);
      } else {
        throw e;
      }
    }
    ElMessage.success("图片已复制成功");
  } catch (error: any) {
    ElMessage.error(error?.message || "复制图片失败");
  } finally {
    state.copyingId = "";
  }
};

const convertToPngAndCopy = (blob: Blob): Promise<void> => {
  return new Promise((resolve, reject) => {
    const url = URL.createObjectURL(blob);
    const img = new Image();
    img.onload = () => {
      URL.revokeObjectURL(url);
      const canvas = document.createElement("canvas");
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      const ctx = canvas.getContext("2d");
      if (!ctx) return reject(new Error("Canvas 不可用"));
      ctx.drawImage(img, 0, 0);
      canvas.toBlob((pngBlob) => {
        if (pngBlob) {
          navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]).then(resolve).catch(reject);
        } else {
          reject(new Error("转 PNG 失败"));
        }
      }, "image/png");
    };
    img.onerror = () => {
      URL.revokeObjectURL(url);
      reject(new Error("图片加载失败"));
    };
    img.src = url;
  });
};

const onKeyDown = (e: KeyboardEvent) => {
  if (state.imageItems.length <= 1) return;
  if (e.key === "ArrowDown" || e.key === "ArrowRight") {
    e.preventDefault();
    const next = (state.selectedFileIndex + 1) % state.imageItems.length;
    selectFile(next);
  } else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
    e.preventDefault();
    const prev = (state.selectedFileIndex - 1 + state.imageItems.length) % state.imageItems.length;
    selectFile(prev);
  }
};

onUnmounted(() => {
  Object.values(blobUrlCache).forEach((url) => URL.revokeObjectURL(url));
  document.removeEventListener("keydown", onKeyDown);
});

onMounted(async () => {
  const deptId = await loadDeptTree();
  if (deptId !== null) {
    // 立即加载首个部门 → 用户看到内容
    await loadData(deptId);
    // 后台预载所有其他叶子部门的文件列表 → 切换 tab 秒开
    preloadAllDepts(deptId);
  }
  document.addEventListener("keydown", onKeyDown);
});

/** 后台预载所有部门：文件元数据 + 全部图片 blob（排除当前已加载的部门） */
const preloadAllDepts = (currentDeptId: string) => {
  const leafIds = collectLeafDeptIds(state.deptTreeData);
  const remaining = leafIds.filter((id) => id !== currentDeptId && !deptFileCache[id]);
  if (remaining.length === 0) return;

  // 逐个部门串行预载：先拉元数据，再拉全部图片 blob
  const preloadNext = async () => {
    const id = remaining.shift();
    if (!id) return;
    try {
      const res = await baseService.get("/filescan/record/tree", { deptId: id });
      const items = transformGroups((res.data || []) as DeptFileGroupVO[]);
      deptFileCache[id] = items;
      // 预载该部门全部图片 blob
      const blobTasks = items
        .filter((item) => isImageFile(item.type))
        .map((item) => loadBlobUrl(item.fileId));
      await Promise.allSettled(blobTasks);
    } catch {
      // 预载失败静默忽略，用户切换时走正常 loadData 流程
    }
    // 继续下一个，50ms 间隔避免阻塞主线程
    if (remaining.length > 0) {
      setTimeout(preloadNext, 50);
    }
  };
  preloadNext();
};

onUnmounted(() => {
  Object.values(blobUrlCache).forEach((url) => URL.revokeObjectURL(url));
  document.removeEventListener("keydown", onKeyDown);
});
</script>

<style scoped>
.mod-weather__real-time-monitoring {
  min-height: calc(100vh - 140px);
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  margin-bottom: 16px;
}

.page-title {
  margin: 0;
  font-size: 24px;
  color: #303133;
}

.page-desc {
  margin: 8px 0 0;
  color: #909399;
  line-height: 1.6;
}

.page-header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.mb-16 {
  margin-bottom: 16px;
}

/* ==== 部门选择栏 ==== */
.dept-bar {
  margin-bottom: 16px;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  padding: 10px 16px 8px;
}

.level-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.level-row:last-child {
  margin-bottom: 4px;
}

.level-label {
  font-size: 13px;
  color: #909399;
  flex-shrink: 0;
  min-width: 32px;
}

.level-tabs {
  display: flex;
  gap: 0;
  flex-wrap: wrap;
}

.level-tab {
  padding: 4px 16px;
  font-size: 13px;
  color: #606266;
  background: #f5f7fa;
  border: 1px solid #e4e7ed;
  border-radius: 6px;
  cursor: pointer;
  user-select: none;
  transition: color 0.2s, background 0.2s, border-color 0.2s;
}

.level-tab + .level-tab {
  margin-left: 6px;
}

.level-tab:hover {
  color: var(--el-color-primary);
  background: #ecf5ff;
}

.level-tab.is-active {
  color: #fff;
  font-weight: 500;
}

.level-tab.is-active.level-0 {
  background: var(--el-color-primary);
  border-color: var(--el-color-primary);
}

.level-tab.is-active.level-1 {
  background: var(--el-color-primary-light-3);
  border-color: var(--el-color-primary-light-3);
}

.level-tab.is-active.level-2 {
  background: var(--el-color-success);
  border-color: var(--el-color-success);
}

.level-tab.is-active.level-3 {
  background: var(--el-color-warning);
  border-color: var(--el-color-warning);
}

.level-tab.is-active.level-4 {
  background: var(--el-color-danger);
  border-color: var(--el-color-danger);
}

.level-tab.is-active.level-5 {
  background: #8b5cf6;
  border-color: #8b5cf6;
}

/* ==== 主体两栏 ==== */
.page-body {
  display: grid;
  grid-template-columns: 25% minmax(0, 1fr);
  gap: 12px;
}

/* ==== 左侧文件列表 ==== */
.file-list-panel {
  position: sticky;
  top: 16px;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  padding: 4px;
  max-height: calc(100vh - 320px);
  overflow-y: auto;
}

.file-list-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  font-size: 13px;
  color: #606266;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.file-list-item:hover {
  background: #f5f7fa;
}

.file-list-item.is-active {
  background: var(--el-color-primary-light-9);
  color: var(--el-color-primary);
  font-weight: 500;
}

.file-list-item__thumb {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 6px;
  overflow: hidden;
  background: #f0f2f5;
  display: flex;
  align-items: center;
  justify-content: center;
}

.file-list-item__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.file-list-item__icon {
  font-size: 13px;
  font-weight: 600;
  color: #fff;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.file-list-item__icon--img {
  background: var(--el-color-success);
}

.file-list-item__icon--txt {
  background: var(--el-color-warning);
}

.file-list-item__icon--other {
  background: #c0c4cc;
}

.file-list-item__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.5;
}

/* ==== 右侧预览面板 ==== */
.preview-panel {
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 10px;
  padding: 20px;
}

.preview-panel--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 400px;
}

.preview-panel__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  margin-bottom: 16px;
}

.preview-panel__actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.preview-panel__title {
  margin: 0;
  font-size: 20px;
  color: #303133;
  word-break: break-all;
}

.preview-panel__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.preview-panel__loading {
  text-align: center;
  color: #909399;
  padding: 60px 0;
  font-size: 14px;
}

.preview-zoom-area {
  overflow: hidden;
  border-radius: 8px;
  background: #f5f7fa;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.preview-panel__img {
  display: block;
  max-width: 100%;
  max-height: 60vh;
  border-radius: 8px;
  user-select: none;
  pointer-events: auto;
}

.preview-panel__text--full {
  min-height: 200px;
  max-height: 60vh;
  overflow-y: auto;
}

.preview-panel__text {
  font-size: 13px;
  color: #606266;
  line-height: 1.8;
  white-space: pre-wrap;
  word-break: break-word;
  padding: 12px 14px;
  background: #f5f7fa;
  border-radius: 8px;
}

.image-card__error,
.image-card__unsupported {
  color: #e6a23c;
  font-size: 14px;
  line-height: 1.6;
  padding: 12px 14px;
  background: #fdf6ec;
  border-radius: 8px;
}

/* ==== 响应式 ==== */
@media (max-width: 992px) {
  .page-body {
    grid-template-columns: 1fr;
  }

  .file-list-panel {
    position: static;
    max-height: 200px;
  }
}

@media (max-width: 768px) {
  .page-header,
  .preview-panel__header,
  .preview-panel__actions {
    flex-direction: column;
    align-items: stretch;
  }

  .level-tab {
    padding: 4px 10px;
    font-size: 12px;
  }
}
</style>
