<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";

import {
  emptyListingPublishOptions,
  type ListingPublishOptions,
} from "@/api/listingOptions";
import { fetchListings, type Listing } from "@/api/listings";
import {
  defaultHomeAnnouncements,
  defaultHomeBanners,
  fetchMobileHomeConfig,
  type HomeBannerSlide,
} from "@/api/homeConfig";
import {
  assetRegions,
  formatHafCoinM,
  getCoinM,
  getCoinWan,
  getListingChips,
  getListingDisplayPrice,
  getListingSubtitle,
  getListingTitle,
  getLoginMethod,
  getResourceQuantity,
  getServerRegion,
  getSkinGroup,
  hasAcceleratedSaleRatio,
  hasGiftResources,
  readAssetNumber,
  readAssetString,
} from "@/utils/listingDisplay";
import { listingStatusLabel } from "@/utils/statusLabels";

/* ------------------------------------------------------------------ */
/*  数据加载                                                            */
/* ------------------------------------------------------------------ */
const loading = ref(false);
const loadFailed = ref(false);
const listings = ref<Listing[]>([]);
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions);
const announcements = ref<string[]>(defaultHomeAnnouncements);
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners);

onMounted(() => {
  loadListings();
  loadHomeConfig();
});

async function loadListings() {
  loading.value = true;
  loadFailed.value = false;
  try {
    listings.value = await fetchListings();
  } catch {
    loadFailed.value = true;
  } finally {
    loading.value = false;
  }
}

async function loadHomeConfig() {
  try {
    const config = await fetchMobileHomeConfig();
    announcements.value = config.announcements;
    bannerSlides.value = config.banners;
    publishOptions.value = config.publish_options;
  } catch {
    announcements.value = defaultHomeAnnouncements;
    bannerSlides.value = defaultHomeBanners;
    publishOptions.value = emptyListingPublishOptions;
  }
}

/* ------------------------------------------------------------------ */
/*  排序                                                                */
/* ------------------------------------------------------------------ */
const sortBy = ref("comprehensive");

const sortOptions = [
  { key: "comprehensive", label: "综合排序" },
  { key: "published", label: "发布时间" },
  { key: "awmDesc", label: "AWM数量" },
  { key: "priceAsc", label: "价格最低" },
  { key: "priceDesc", label: "价格最高" },
];

const activeSortLabel = computed(
  () => sortOptions.find((o) => o.key === sortBy.value)?.label || "综合排序"
);

function selectSort(key: string) {
  sortBy.value = key;
  sortOpen.value = false;
}

/* ------------------------------------------------------------------ */
/*  筛选 — 复用移动端 FilterSection 体系                                  */
/* ------------------------------------------------------------------ */
type SelectedFilters = Record<string, string[]>;
type RangeFilters = Record<string, { min: string; max: string }>;
type RangePresets = Record<
  string,
  Array<{ label: string; min: string; max: string }>
>;

const selectedFilters = ref<SelectedFilters>({});
const rangeFilters = ref<RangeFilters>({});
const searchValue = ref("");
const sortOpen = ref(false);
const filterOpen = ref(false);

const rangePresets: RangePresets = {
  coin: [
    { label: "50-100", min: "50", max: "100" },
    { label: "100-200", min: "100", max: "200" },
    { label: "200-300", min: "200", max: "300" },
    { label: "300-500", min: "300", max: "500" },
    { label: "500以上", min: "500", max: "" },
  ],
  resource_awmAmmo: [
    { label: "0-20", min: "0", max: "20" },
    { label: "20-50", min: "20", max: "50" },
    { label: "50-100", min: "50", max: "100" },
    { label: "100-200", min: "100", max: "200" },
    { label: "200以上", min: "200", max: "" },
  ],
};

const serverFilterOptions = computed(() =>
  uniqueOptions(
    publishOptions.value.server_options
      .map((item) => item.trim())
      .filter(Boolean)
  )
);

const loginMethodFilterOptions = computed(() =>
  uniqueOptions(
    publishOptions.value.login_method_options
      .map((item) => item.trim())
      .filter(Boolean)
  )
);

type FilterSection =
  | {
      key: string;
      title: string;
      type: "range";
      unit?: string;
      minPlaceholder?: string;
      maxPlaceholder?: string;
    }
  | { key: string; title: string; type: "chips"; options: string[] };

const filterSections = computed<FilterSection[]>(() => [
  {
    key: "price",
    title: "价格区间",
    type: "range",
    unit: "元",
    minPlaceholder: "最低价",
    maxPlaceholder: "最高价",
  },
  {
    key: "coin",
    title: "哈夫币数量",
    type: "range",
    unit: "M",
    minPlaceholder: "最低",
    maxPlaceholder: "最高",
  },
  {
    key: "server",
    title: "区服",
    type: "chips",
    options: serverFilterOptions.value,
  },
  {
    key: "login",
    title: "上号方式",
    type: "chips",
    options: loginMethodFilterOptions.value,
  },
  {
    key: "insurance",
    title: "保险",
    type: "chips",
    options: publishOptions.value.insurance_options,
  },
  {
    key: "stamina",
    title: "体力",
    type: "chips",
    options: publishOptions.value.level_options,
  },
  {
    key: "load",
    title: "负重",
    type: "chips",
    options: publishOptions.value.level_options,
  },
  ...publishOptions.value.quantity_items.map((item) => ({
    key: `resource_${item.key}`,
    title: item.label,
    type: "range" as const,
    unit: parseQuantityUnit(item.price),
    minPlaceholder: "最低",
    maxPlaceholder: "最高",
  })),
  ...publishOptions.value.skin_groups.map((group) => ({
    key: group.key,
    title: group.title,
    type: "chips" as const,
    options: group.options,
  })),
  {
    key: "secretKd",
    title: "绝密KD",
    type: "range",
    minPlaceholder: "最低",
    maxPlaceholder: "最高",
  },
  {
    key: "rank",
    title: "段位",
    type: "chips",
    options: publishOptions.value.rank_options,
  },
  {
    key: "deposit",
    title: "押金",
    type: "range",
    unit: "元",
    minPlaceholder: "最低",
    maxPlaceholder: "最高",
  },
]);

/* 筛选交互 */
const activeFilterSection = ref("price");
const filterContentRef = ref<HTMLElement | null>(null);
const filterSectionRefs = new Map<string, HTMLElement>();
const filterTabRefs = new Map<string, HTMLElement>();

function toggleChip(sectionKey: string, value: string) {
  const selected = selectedFilters.value[sectionKey] || [];
  const next = selected.includes(value)
    ? selected.filter((item) => item !== value)
    : [...selected, value];
  selectedFilters.value = { ...selectedFilters.value, [sectionKey]: next };
}

function updateRange(sectionKey: string, side: "min" | "max", value: string) {
  const current = rangeFilters.value[sectionKey] || { min: "", max: "" };
  rangeFilters.value = {
    ...rangeFilters.value,
    [sectionKey]: { ...current, [side]: value },
  };
}

function applyRangePreset(sectionKey: string, min: string, max: string) {
  rangeFilters.value = {
    ...rangeFilters.value,
    [sectionKey]: { min, max },
  };
}

function isRangePresetActive(sectionKey: string, min: string, max: string) {
  const range = rangeFilters.value[sectionKey];
  return range?.min === min && range?.max === max;
}

function isSkinGroupKey(key: string) {
  return publishOptions.value.skin_groups.some((group) => group.key === key);
}

function setFilterSectionRef(key: string, el: Element | null) {
  if (el instanceof HTMLElement) filterSectionRefs.set(key, el);
  else filterSectionRefs.delete(key);
}

function setFilterTabRef(key: string, el: Element | null) {
  if (el instanceof HTMLElement) filterTabRefs.set(key, el);
  else filterTabRefs.delete(key);
}

function scrollToFilterSection(key: string) {
  const container = filterContentRef.value;
  const target = filterSectionRefs.get(key);
  if (!container || !target) return;
  activeFilterSection.value = key;
  container.scrollTo({
    top: Math.max(target.offsetTop - 8, 0),
    behavior: "smooth",
  });
}

function handleFilterScroll() {
  const container = filterContentRef.value;
  if (!container) return;
  const anchorTop = container.scrollTop + 16;
  let activeKey = filterSections.value[0]?.key || "";
  for (const section of filterSections.value) {
    const el = filterSectionRefs.get(section.key);
    if (el && el.offsetTop <= anchorTop) activeKey = section.key;
  }
  if (activeKey && activeKey !== activeFilterSection.value) {
    activeFilterSection.value = activeKey;
  }
}

function toggleSortPanel() {
  sortOpen.value = !sortOpen.value;
}

function clearFilters() {
  selectedFilters.value = {};
  rangeFilters.value = {};
  searchValue.value = "";
}

/* ------------------------------------------------------------------ */
/*  筛选 + 排序后的列表                                                  */
/* ------------------------------------------------------------------ */
const activeFilterCount = computed(() => {
  const chipCount = Object.values(selectedFilters.value).reduce(
    (sum, values) => sum + values.length,
    0
  );
  const rangeCount = Object.values(rangeFilters.value).filter(
    (range) => range.min || range.max
  ).length;
  return chipCount + rangeCount;
});

const filteredListings = computed(() => {
  const keyword = searchValue.value.trim().toLowerCase();
  const filtered = listings.value.filter((item) => {
    if (!matchesFilters(item)) return false;
    if (!keyword) return true;
    return searchText(item).includes(keyword);
  });
  return sortListings(filtered);
});

function matchesFilters(item: Listing) {
  const chipOk = Object.entries(selectedFilters.value).every(
    ([key, values]) => {
      if (!values.length) return true;
      if (key === "server") return values.includes(getServerRegion(item));
      if (key === "login") return values.includes(getLoginMethod(item));
      if (key === "insurance")
        return values.includes(readAssetString(item, "season_insurance"));
      if (key === "stamina")
        return values.includes(readAssetString(item, "stamina_level"));
      if (key === "load")
        return values.includes(readAssetString(item, "load_level"));
      if (key === "rank") return values.includes(item.rank_level);
      if (isSkinGroupKey(key)) {
        return getSkinGroup(item, key).some((skin) => values.includes(skin));
      }
      return true;
    }
  );
  if (!chipOk) return false;

  return Object.entries(rangeFilters.value).every(([key, range]) => {
    if (!range.min && !range.max) return true;
    let value = 0;
    if (key === "price") value = getListingDisplayPrice(item);
    if (key === "coin") value = getCoinM(item);
    if (key === "secretKd") value = readAssetNumber(item, "secret_kd");
    if (key === "deposit") value = Number(item.deposit_amount || 0);
    if (key.startsWith("resource_")) {
      value = getResourceQuantity(item, key.replace("resource_", ""));
    }
    return inRange(value, range);
  });
}

function inRange(value: number, range: { min: string; max: string }) {
  const min = range.min === "" ? undefined : Number(range.min);
  const max = range.max === "" ? undefined : Number(range.max);
  if (min !== undefined && Number.isFinite(min) && value < min) return false;
  if (max !== undefined && Number.isFinite(max) && value > max) return false;
  return true;
}

function searchText(item: Listing) {
  return [
    item.title,
    item.description,
    getServerRegion(item),
    getLoginMethod(item),
    item.rank_level,
    readAssetString(item, "season_insurance"),
    readAssetString(item, "stamina_level"),
    readAssetString(item, "load_level"),
    formatHafCoinM(getCoinWan(item)),
    assetRegions(item).join(" "),
    ...publishOptions.value.skin_groups.map((group) =>
      getSkinGroup(item, group.key).join(" ")
    ),
  ]
    .join(" ")
    .toLowerCase();
}

function sortListings(items: Listing[]) {
  const sorted = [...items];
  if (sortBy.value === "comprehensive") return sorted;
  if (sortBy.value === "priceAsc") {
    return sorted.sort(
      (a, b) => getListingDisplayPrice(a) - getListingDisplayPrice(b)
    );
  }
  if (sortBy.value === "priceDesc") {
    return sorted.sort(
      (a, b) => getListingDisplayPrice(b) - getListingDisplayPrice(a)
    );
  }
  if (sortBy.value === "awmDesc") {
    return sorted.sort(
      (a, b) =>
        getResourceQuantity(b, "awmAmmo") - getResourceQuantity(a, "awmAmmo") ||
        getCoinWan(b) - getCoinWan(a)
    );
  }
  return sorted.sort((a, b) => {
    const bTime = Date.parse(b.published_at || b.created_at || "") || 0;
    const aTime = Date.parse(a.published_at || a.created_at || "") || 0;
    return bTime - aTime || b.id - a.id;
  });
}

function uniqueOptions(values: string[]) {
  return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
}

function parseQuantityUnit(price: string) {
  const unit = price.split("/")[1]?.trim();
  return unit || undefined;
}
</script>

<template>
  <section class="pc-listings">
    <!-- 防骗提示 -->
    <div class="fraud-tip">
      <span class="fraud-dot"></span>
      <span
        v-for="(text, idx) in announcements"
        :key="idx"
        class="fraud-text"
        >{{ text }}</span
      >
    </div>

    <!-- 页头 -->
    <div class="listings-header">
      <h1>租号大厅</h1>
      <span class="result-count">
        <strong>{{ filteredListings.length }}</strong> 个可租账号
      </span>
    </div>

    <!-- 搜索 + 排序 + 筛选入口 -->
    <div class="toolbar">
      <div class="search-box">
        <svg class="search-icon" viewBox="0 0 20 20" fill="currentColor">
          <path
            fill-rule="evenodd"
            d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
            clip-rule="evenodd"
          />
        </svg>
        <input v-model="searchValue" placeholder="搜区服 / 段位 / 哈夫币数量" />
      </div>

      <button
        type="button"
        class="toolbar-btn sort-btn"
        @click="toggleSortPanel"
      >
        <span>{{ activeSortLabel }}</span>
        <svg
          :class="{ rotated: sortOpen }"
          viewBox="0 0 20 20"
          fill="currentColor"
          width="14"
          height="14"
        >
          <path
            fill-rule="evenodd"
            d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
            clip-rule="evenodd"
          />
        </svg>
      </button>

      <button
        type="button"
        class="toolbar-btn filter-btn"
        @click="filterOpen = !filterOpen"
      >
        <svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16">
          <path
            fill-rule="evenodd"
            d="M3 3a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-.293.707L12 10.414V15a1 1 0 01-.553.894l-2 1A1 1 0 018 16v-5.586L3.293 5.707A1 1 0 013 5V3z"
            clip-rule="evenodd"
          />
        </svg>
        <span>筛选</span>
        <em v-if="activeFilterCount">{{ activeFilterCount }}</em>
      </button>
    </div>

    <!-- 排序下拉 -->
    <Transition name="sort-panel">
      <div v-if="sortOpen" class="sort-panel">
        <button
          v-for="option in sortOptions"
          :key="option.key"
          type="button"
          :class="{ active: sortBy === option.key }"
          @click="selectSort(option.key)"
        >
          <span>{{ option.label }}</span>
          <svg
            v-if="sortBy === option.key"
            viewBox="0 0 20 20"
            fill="currentColor"
            width="16"
            height="16"
          >
            <path
              fill-rule="evenodd"
              d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
              clip-rule="evenodd"
            />
          </svg>
        </button>
      </div>
    </Transition>

    <!-- ================== 筛选侧栏 ================== -->
    <Transition name="filter-sidebar">
      <aside v-if="filterOpen" class="filter-sidebar">
        <header class="filter-sidebar-header">
          <h2>筛选</h2>
          <button
            type="button"
            class="filter-close-btn"
            @click="filterOpen = false"
          >
            ✕
          </button>
        </header>

        <div class="filter-sidebar-body">
          <nav class="filter-tabs">
            <button
              v-for="section in filterSections"
              :key="section.key"
              :ref="(el) => setFilterTabRef(section.key, el as Element | null)"
              type="button"
              :class="{ active: activeFilterSection === section.key }"
              @click="scrollToFilterSection(section.key)"
            >
              {{ section.title }}
            </button>
          </nav>

          <section
            ref="filterContentRef"
            class="filter-content"
            @scroll.passive="handleFilterScroll"
          >
            <div
              v-for="section in filterSections"
              :key="section.key"
              :ref="(el) => setFilterSectionRef(section.key, el as Element | null)"
              class="filter-block"
            >
              <h3>{{ section.title }}</h3>
              <p
                v-if="section.type === 'range' && section.unit"
                class="filter-unit"
              >
                单位：{{ section.unit }}
              </p>

              <template v-if="section.type === 'range'">
                <div class="range-editor">
                  <input
                    :value="rangeFilters[section.key]?.min || ''"
                    type="number"
                    inputmode="decimal"
                    :placeholder="section.minPlaceholder || '最低'"
                    @input="
                      updateRange(
                        section.key,
                        'min',
                        ($event.target as HTMLInputElement).value
                      )
                    "
                  />
                  <span class="range-sep">—</span>
                  <input
                    :value="rangeFilters[section.key]?.max || ''"
                    type="number"
                    inputmode="decimal"
                    :placeholder="section.maxPlaceholder || '最高'"
                    @input="
                      updateRange(
                        section.key,
                        'max',
                        ($event.target as HTMLInputElement).value
                      )
                    "
                  />
                </div>
                <div
                  v-if="rangePresets[section.key]?.length"
                  class="chip-grid range-preset-grid"
                >
                  <button
                    v-for="preset in rangePresets[section.key]"
                    :key="`${section.key}-${preset.label}`"
                    type="button"
                    :class="{
                      active: isRangePresetActive(
                        section.key,
                        preset.min,
                        preset.max
                      ),
                    }"
                    @click="
                      applyRangePreset(section.key, preset.min, preset.max)
                    "
                  >
                    {{ preset.label }}
                  </button>
                </div>
              </template>

              <div v-else class="chip-grid">
                <button
                  v-for="option in section.options"
                  :key="`${section.key}-${option}`"
                  type="button"
                  :class="{
                    active: selectedFilters[section.key]?.includes(option),
                  }"
                  @click="toggleChip(section.key, option)"
                >
                  {{ option }}
                </button>
              </div>
            </div>
          </section>
        </div>

        <footer class="filter-sidebar-footer">
          <button type="button" class="reset-btn" @click="clearFilters">
            重置
          </button>
          <button type="button" class="confirm-btn" @click="filterOpen = false">
            确定
          </button>
        </footer>
      </aside>
    </Transition>

    <!-- 筛选遮罩 -->
    <Transition name="overlay">
      <div
        v-if="filterOpen"
        class="filter-overlay"
        @click="filterOpen = false"
      ></div>
    </Transition>

    <!-- 加载 / 错误 / 空状态 -->
    <div v-if="loading" class="state-loading">正在加载优质账号...</div>
    <div v-else-if="loadFailed" class="state-error">
      接口暂不可用，请稍后刷新。
    </div>
    <div v-else-if="filteredListings.length === 0" class="state-empty">
      <p>没有符合条件的账号</p>
      <button type="button" class="reset-btn" @click="clearFilters">
        重置条件
      </button>
    </div>

    <!-- 列表 -->
    <div v-else class="pc-resource-list">
      <RouterLink
        v-for="item in filteredListings"
        :key="item.id"
        class="resource-card"
        :to="`/listings/${item.id}`"
      >
        <div class="resource-cover">
          <img
            v-if="item.cover_url"
            :src="item.cover_url"
            :alt="getListingTitle(item)"
            loading="lazy"
            decoding="async"
          />
          <span v-else>图</span>
          <div
            v-if="hasGiftResources(item) || hasAcceleratedSaleRatio(item)"
            class="cover-labels"
          >
            <em v-if="hasGiftResources(item)">有赠送</em>
            <em v-if="hasAcceleratedSaleRatio(item)">特惠</em>
          </div>
        </div>
        <div class="resource-main">
          <h2 class="card-title">{{ getListingTitle(item) }}</h2>
          <p class="card-subtitle">{{ getListingSubtitle(item) }}</p>
          <div class="resource-badge-row">
            <span class="trust-badge">押金秒退</span>
            <span class="server-badge">{{ getServerRegion(item) }}</span>
            <span v-if="getLoginMethod(item)" class="server-badge">{{
              getLoginMethod(item)
            }}</span>
          </div>
          <div class="card-chip-row">
            <span
              v-for="chip in getListingChips(item)"
              :key="`${item.id}-${chip.label}`"
              >{{ chip.label }}:{{ chip.value }}</span
            >
          </div>
        </div>
        <div class="resource-price-box">
          <strong>¥{{ getListingDisplayPrice(item) }}</strong>
          <span class="rent-sub">押金¥{{ item.deposit_amount }}</span>
        </div>
      </RouterLink>
    </div>
  </section>
</template>

<style scoped>
/* ================================================================== */
/*  页面容器                                                            */
/* ================================================================== */
.pc-listings {
  position: relative;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px 40px;
  color: #1a202c;
}

/* ================================================================== */
/*  防骗提示                                                            */
/* ================================================================== */
.fraud-tip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  margin-bottom: 12px;
  border-radius: 10px;
  background: #fffdf0;
  border: 1px solid #f5e6a3;
  font-size: 13px;
  color: #8a6d1b;
  overflow: hidden;
  white-space: nowrap;
}

.fraud-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #f5a623;
  flex-shrink: 0;
  animation: pulse 2s ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}

.fraud-text + .fraud-text::before {
  content: "  |  ";
  color: #d4c47a;
}

/* ================================================================== */
/*  页头                                                                */
/* ================================================================== */
.listings-header {
  display: flex;
  align-items: baseline;
  gap: 14px;
  margin-bottom: 14px;
}

.listings-header h1 {
  margin: 0;
  font-size: 24px;
  font-weight: 900;
  color: #17233d;
}

.result-count {
  color: #8b9cb5;
  font-size: 14px;
}

.result-count strong {
  color: #1477ff;
  font-weight: 800;
}

/* ================================================================== */
/*  工具栏：搜索 + 排序 + 筛选                                           */
/* ================================================================== */
.toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}

.search-box {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 1 360px;
  height: 38px;
  padding: 0 12px;
  border-radius: 10px;
  background: #f4f6f8;
  border: 1px solid transparent;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.search-box:focus-within {
  border-color: #1477ff;
  box-shadow: 0 0 0 3px rgba(20, 119, 255, 0.1);
  background: #fff;
}

.search-icon {
  width: 16px;
  height: 16px;
  color: #a0aab6;
  flex-shrink: 0;
}

.search-box input {
  border: none;
  outline: none;
  background: transparent;
  color: #17233d;
  font-size: 13px;
  width: 100%;
}

.search-box input::placeholder {
  color: #a0aab6;
}

.toolbar-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 0 14px;
  height: 38px;
  border: 1px solid #e1e7ef;
  border-radius: 10px;
  background: #fff;
  color: #3d4e63;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.toolbar-btn:hover {
  border-color: #1477ff;
}

.sort-btn svg {
  transition: transform 0.2s;
}

.sort-btn svg.rotated {
  transform: rotate(180deg);
}

.filter-btn {
  position: relative;
}

.filter-btn em {
  position: absolute;
  top: -6px;
  right: -6px;
  display: grid;
  width: 18px;
  height: 18px;
  place-items: center;
  border-radius: 50%;
  background: #1477ff;
  color: #fff;
  font-size: 11px;
  font-weight: 800;
  font-style: normal;
}

/* ================================================================== */
/*  排序面板                                                            */
/* ================================================================== */
.sort-panel {
  display: flex;
  gap: 4px;
  padding: 8px 0;
  margin-bottom: 12px;
  border-radius: 10px;
  background: #fff;
  border: 1px solid #e1e7ef;
}

.sort-panel button {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  border: none;
  border-radius: 8px;
  background: none;
  color: #5a6577;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}

.sort-panel button:hover {
  background: #f0f5ff;
}

.sort-panel button.active {
  background: #1477ff;
  color: #fff;
}

.sort-panel button.active svg {
  color: #fff;
}

.sort-panel svg {
  color: #1477ff;
}

.sort-panel-enter-active,
.sort-panel-leave-active {
  transition: opacity 0.15s ease, transform 0.15s ease;
}

.sort-panel-enter-from,
.sort-panel-leave-to {
  opacity: 0;
  transform: translateY(-4px);
}

/* ================================================================== */
/*  筛选侧栏                                                            */
/* ================================================================== */
.filter-sidebar {
  position: fixed;
  top: 0;
  right: 0;
  z-index: 300;
  display: grid;
  grid-template-rows: 52px minmax(0, 1fr) 60px;
  width: min(480px, 90vw);
  height: 100vh;
  background: #fff;
  border-left: 1px solid #eef1f5;
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.1);
}

.filter-sidebar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 18px;
  border-bottom: 1px solid #eef1f5;
}

.filter-sidebar-header h2 {
  margin: 0;
  font-size: 17px;
  font-weight: 800;
  color: #17233d;
}

.filter-close-btn {
  display: grid;
  width: 36px;
  height: 36px;
  place-items: center;
  border: none;
  border-radius: 10px;
  background: #f4f6f8;
  color: #64748b;
  font-size: 16px;
  cursor: pointer;
}

.filter-sidebar-body {
  display: grid;
  grid-template-columns: 110px minmax(0, 1fr);
  min-height: 0;
}

/* ---- 左侧 Tab 导航 ---- */
.filter-tabs {
  overflow-y: auto;
  background: #f6f7f9;
  border-right: 1px solid #eef1f5;
}

.filter-tabs button {
  display: block;
  width: 100%;
  min-height: 44px;
  border: none;
  background: transparent;
  color: #3d4e63;
  padding: 8px 12px;
  text-align: left;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}

.filter-tabs button:hover {
  background: #edf1f7;
}

.filter-tabs button.active {
  background: #fff;
  color: #ff7a00;
  font-weight: 800;
}

/* ---- 右侧内容 ---- */
.filter-content {
  overflow-y: auto;
  padding: 20px 18px 32px;
}

.filter-block {
  scroll-margin-top: 8px;
}

.filter-block + .filter-block {
  margin-top: 28px;
}

.filter-block h3 {
  margin: 0 0 12px;
  font-size: 16px;
  font-weight: 800;
  color: #17233d;
}

.filter-unit {
  margin: -6px 0 10px;
  color: #8a96a8;
  font-size: 12px;
}

/* ---- Range 编辑器 ---- */
.range-editor {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 10px;
}

.range-sep {
  color: #cfd6df;
  font-size: 14px;
}

.range-editor input {
  height: 40px;
  border: 1px solid #e1e7ef;
  border-radius: 8px;
  background: #fff;
  color: #17233d;
  padding: 0 12px;
  text-align: center;
  font-size: 13px;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.range-editor input:focus {
  border-color: #1477ff;
  box-shadow: 0 0 0 3px rgba(20, 119, 255, 0.08);
}

/* ---- Chip 网格 ---- */
.chip-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.chip-grid button {
  min-width: 0;
  padding: 7px 14px;
  border: none;
  border-radius: 8px;
  background: #f4f5f7;
  color: #2d3748;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, box-shadow 0.12s;
}

.chip-grid button:hover {
  background: #eaeff5;
}

.chip-grid button.active {
  background: #fff3d1;
  color: #cc7a00;
  font-weight: 800;
  box-shadow: inset 0 0 0 1px #ffd15c;
}

.range-preset-grid {
  margin-top: 10px;
}

/* ---- Footer ---- */
.filter-sidebar-footer {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  align-items: center;
  padding: 10px 18px;
  border-top: 1px solid #eef1f5;
}

.filter-sidebar-footer button {
  height: 42px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
}

.reset-btn {
  border: 1px solid #e1e7ef;
  background: #fff;
  color: #2d3748;
}

.confirm-btn {
  border: none;
  background: #1477ff;
  color: #fff;
}

.confirm-btn:hover {
  background: #0d5fd4;
}

/* ---- 侧栏动画 ---- */
.filter-sidebar-enter-active,
.filter-sidebar-leave-active {
  transition: transform 0.25s ease;
}

.filter-sidebar-enter-from,
.filter-sidebar-leave-to {
  transform: translateX(100%);
}

/* ---- 遮罩 ---- */
.filter-overlay {
  position: fixed;
  inset: 0;
  z-index: 299;
  background: rgba(0, 0, 0, 0.3);
}

.overlay-enter-active,
.overlay-leave-active {
  transition: opacity 0.2s ease;
}

.overlay-enter-from,
.overlay-leave-to {
  opacity: 0;
}

/* ================================================================== */
/*  状态提示                                                            */
/* ================================================================== */
.state-loading,
.state-error,
.state-empty {
  text-align: center;
  padding: 48px 20px;
  color: #8b9cb5;
  font-size: 14px;
}

.state-empty button {
  margin-top: 12px;
}

/* ================================================================== */
/*  列表卡片                                                            */
/* ================================================================== */
.pc-resource-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 14px;
}

.resource-card {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr) auto;
  gap: 14px;
  padding: 14px;
  border-radius: 14px;
  background: #fff;
  border: 1px solid #eef1f5;
  text-decoration: none;
  color: inherit;
  transition: box-shadow 0.15s, border-color 0.15s;
}

.resource-card:hover {
  border-color: #c5d5ea;
  box-shadow: 0 4px 16px rgba(23, 35, 61, 0.08);
}

.resource-cover {
  position: relative;
  width: 120px;
  height: 90px;
  border-radius: 10px;
  overflow: hidden;
  background: #f0f5ff;
}

.resource-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.resource-cover span {
  display: grid;
  width: 100%;
  height: 100%;
  place-items: center;
  color: #1477ff;
  font-size: 20px;
  font-weight: 900;
}

.cover-labels {
  position: absolute;
  top: 6px;
  left: 6px;
  display: flex;
  gap: 4px;
}

.cover-labels em {
  font-style: normal;
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 10px;
  font-weight: 700;
  color: #fff;
  background: #ff6a00;
}

.cover-labels em + em {
  background: #e74c3c;
}

.resource-main {
  min-width: 0;
}

.card-title {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 700;
  color: #17233d;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-subtitle {
  margin: 0 0 8px;
  font-size: 13px;
  color: #1477ff;
  font-weight: 600;
}

.resource-badge-row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 6px;
}

.trust-badge,
.server-badge {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 600;
}

.trust-badge {
  background: #e8faf0;
  color: #0d9a5f;
}

.server-badge {
  background: #f0f5ff;
  color: #3d6fcc;
}

.card-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.card-chip-row span {
  color: #8b9cb5;
  font-size: 11px;
}

.resource-price-box {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: 2px;
  white-space: nowrap;
}

.resource-price-box strong {
  font-size: 20px;
  font-weight: 900;
  color: #e74c3c;
}

.rent-sub {
  color: #8b9cb5;
  font-size: 11px;
}
</style>
